SMODS.GUI = {}
SMODS.GUI.DynamicUIManager = {}

-- used to properly truncate overflow content inside another overflow content
SMODS.stencil_stack = {}

function SMODS.push_to_stencil_stack(stencil_fn)
    assert(type(stencil_fn) == "function", "No stencil function passed to SMODS.push_to_stencil_stack")
    local old_level = #SMODS.stencil_stack
	local new_level = old_level + 1

    love.graphics.setStencilTest("equal", old_level)
	love.graphics.stencil(function()
		stencil_fn(false)
	end, "increment", 1, true)
	love.graphics.setStencilTest("equal", new_level)

	SMODS.stencil_stack[new_level] = stencil_fn
end
function SMODS.pop_from_stencil_stack()
    local old_level = #SMODS.stencil_stack
	local new_level = old_level - 1

    local stencil_fn = SMODS.stencil_stack[old_level]
	if not stencil_fn then
		return
	end

    love.graphics.setStencilTest("equal", old_level)
	love.graphics.stencil(function()
		stencil_fn(true)
	end, "decrement", 1, true)
	love.graphics.setStencilTest("equal", new_level)

	SMODS.stencil_stack[old_level] = nil
end
function SMODS.reset_stencil_stack()
    EMPTY(SMODS.stencil_stack)
    love.graphics.setStencilTest()
    love.graphics.stencil(function() end)
end
function SMODS.reload_stencil_stack()
    local stack_snapshot = SMODS.shallow_copy(SMODS.stencil_stack)
    SMODS.reset_stencil_stack()
    for _, stencil_fn in ipairs(stack_snapshot) do
        SMODS.push_to_stencil_stack(stencil_fn)
    end
end

local gameDrawRef = Game.draw
function Game:draw(...)
    SMODS.reset_stencil_stack()
    gameDrawRef(self, ...)
end

--

local uieDrawChildrenRef = UIElement.draw_children
function UIElement:draw_children(...)
	local stenciled = false
	if self.states.visible and self.config and self.config.no_overflow then
        -- draw stencil for overflow container
		stenciled = true
		SMODS.push_to_stencil_stack(function(exit)
			prep_draw(self, 1)
			love.graphics.scale(1 / G.TILESIZE)
			love.graphics.setColor(0, 0, 0, 1)

			if self.config.r and self.VT.w > 0.01 then
				self:draw_pixellated_rect("fill", 0)
			else
				love.graphics.rectangle("fill", 0, 0, self.VT.w * G.TILESIZE, self.VT.h * G.TILESIZE)
			end

			love.graphics.pop()
		end)
	end
	uieDrawChildrenRef(self, ...)
    -- cancel stencil for overflow container
	if stenciled then SMODS.pop_from_stencil_stack() end
end


-- collision check
function Node:inside_overflow_boundaries(point)
    -- Use cached value if present for current point
	if self.overflow_check_timer == G.TIMERS.REAL and self.overflow_check_point == point then
		return self.overflow_check_result or false
	end
	self.overflow_check_timer = G.TIMERS.REAL
    self.overflow_check_point = point
    local r = true

    -- No parent = no overflow can be done so collide as usual
    if not self.parent then r = true
    -- If parent has overflow then we should check do we collide with it and if not, all children in it cannot be collided too
    elseif self.parent.config and self.parent.config.no_overflow and not Node.collides_with_point(self.parent, point) then r = false
    -- Otherwise process all parents looking for first non-collideable overflow
    else r = Node.inside_overflow_boundaries(self.parent, point) end

    self.overflow_check_result = r
    return r
end

--

SMODS.UIScrollBox = UIBox:extend()
function SMODS.UIScrollBox:init(args)
    args = SMODS.merge_defaults(args, { content = {}, container = {}, overflow = {}, sync_mode = "progress" })
    args.progress = SMODS.merge_defaults(args.progress, { x = 0, y = 0 })
    args.offset = SMODS.merge_defaults(args.offset, { x = 0, y = 0 })

	self.scroll_args = args
	self.scroll_progress = args.progress
	self.scroll_offset = args.offset
	self.scroll_sync_mode = args.sync_mode

	if args.content and args.content.is and args.content:is(Object) then
		self.content = args.content
	else
		self.content = UIBox(args.content)
	end

    args.container.config = SMODS.merge_defaults(args.container.config, { align = "cm", offset = { x = 0, y = 0 } })
    args.container.node_config = SMODS.merge_defaults(args.container.node_config, { colour = G.C.CLEAR })
	args.container.definition = {
		n = G.UIT.ROOT,
		config = args.container.node_config,
		nodes = {
			{
				n = G.UIT.O,
				config = {
					object = self.content,
				},
			},
		},
	}
	self.content_container = UIBox(args.container)

    args.overflow.config = SMODS.merge_defaults(args.overflow.config, {})
    args.overflow.node_config = SMODS.merge_defaults(args.overflow.node_config, { colour = G.C.CLEAR, no_overflow = true })
	args.overflow.definition = {
		n = G.UIT.ROOT,
		config = args.overflow.node_config,
		nodes = {
			{
				n = G.UIT.O,
				config = {
					object = self.content_container,
				},
			},
		},
	}

	UIBox.init(self, args.overflow)

	self:sync_scroll(0, true)
end
function SMODS.UIScrollBox:get_scroll_distance()
	return math.max(0, self.content_container.T.w - self.T.w), math.max(0, self.content_container.T.h - self.T.h)
end
function SMODS.UIScrollBox:sync_scroll_offset()
	local dx, dy = self:get_scroll_distance()
	self.scroll_offset.x = dx * (self.scroll_progress.x or 0)
	self.scroll_offset.y = dy * (self.scroll_progress.y or 0)
end
function SMODS.UIScrollBox:sync_scroll_progress()
	local dx, dy = self:get_scroll_distance()
	self.scroll_progress.x = (dx == 0 and 0) or ((self.scroll_offset.x or 0) / dx)
	self.scroll_progress.y = (dy == 0 and 0) or ((self.scroll_offset.y or 0) / dy)
end
function SMODS.UIScrollBox:set_scroll_offset(t)
	self.scroll_offset = SMODS.merge_defaults(t, { x = 0, y = 0 })
	self:sync_scroll_progress()
end
function SMODS.UIScrollBox:set_scroll_progress(t)
	self.scroll_progress = SMODS.merge_defaults(t, { x = 0, y = 0 })
	self:sync_scroll_offset()
end
function SMODS.UIScrollBox:sync_scroll(dt, init)
	if self.scroll_sync_mode == "progress" then
        self:sync_scroll_offset()
	elseif self.scroll_sync_mode == "offset" then
		self:sync_scroll_progress()
	end
	self.content_container.config.offset.x = -(self.scroll_offset.x or 0)
	self.content_container.config.offset.y = -(self.scroll_offset.y or 0)
end
function SMODS.UIScrollBox:update(dt)
	if self.scroll_args.scroll_move then
		self.scroll_args.scroll_move(self, dt)
	end
	self:sync_scroll(dt)
	UIBox.update(self, dt)
end

--

function STR_UNPACK(str)
    local chunk, err = loadstring(str)
    if chunk then
        setfenv(chunk, {}) -- Use an empty environment to prevent access to potentially harmful functions
        local success, result = pcall(chunk)
        if success then
            return result
        else
            print("Error unpacking string: " .. result)
            return nil
        end
    else
        print("Error loading string: " .. err)
        return nil
    end
end

-- used for icons for correct scaling
SMODS.pixels_to_unit = function(value) return value/(G.TILESCALE*G.TILESIZE) end
SMODS.trim_string = function (s)
    local l = 1
    while string.sub(s,l,l) == ' ' do
        l = l+1
    end
    local r = string.len(s)
    while string.sub(s,r,r) == ' ' do
        r = r-1
    end
    return string.sub(s,l,r)
end
SMODS.smart_line_splitter = function(phrase, length, always_new_line)
    local words = {}
    local current_line = ""
    for word in phrase:gmatch("%S+") do
        -- concat string if it is not at the limit
        if string.len(current_line .. word) <= length then
            current_line = current_line .. word .. " "
        -- this will only happen if word is longer than specified length
        else
            -- if it's longer than half length then put it in new line
            if string.len(current_line) >= length/2 or always_new_line then
                SMODS.trim_string(current_line)
                table.insert(words,current_line)
                current_line = word .. " "

            else
                current_line = current_line .. word
                table.insert(words,current_line)
                current_line = ""
            end
        end
    end
    if current_line ~= "" and current_line ~= " " then
        table.insert(words,current_line)
    end
    return words
end

local gameMainMenuRef = Game.main_menu
function Game:main_menu(change_context)
    for k, v in pairs(G.C.SUITS) do
        G.FUNCS.update_suit_colours(k, G.SETTINGS.CUSTOM_DECK.Collabs[k])
    end
    gameMainMenuRef(self, change_context)
    UIBox({
        definition = {
            n = G.UIT.ROOT,
            config = {
                align = "cm",
                colour = G.C.UI.TRANSPARENT_DARK
            },
            nodes = {
                {
                    n = G.UIT.T,
                    config = {
                        scale = 0.3,
                        text = MODDED_VERSION,
                        colour = G.C.UI.TEXT_LIGHT
                    }
                }
            }
        },
        config = {
            align = "tri",
            bond = "Weak",
            offset = {
                x = 0,
                y = 0.3
            },
            major = G.ROOM_ATTACH
        }
    })

    math.randomseed(os.time()) -- for creating a random card from a set
    local funcs = {}
    local remove_original = false
    for i, v in pairs(SMODS.Mods) do
        if not v.disabled and v.menu_cards then
            local tbl = v.menu_cards() or {}
            if tbl.func then funcs[#funcs + 1] = tbl.func end
            if tbl.remove_original then remove_original = true end
            if tbl.set or tbl.key then tbl = { tbl } end
            if not tbl[1] and not tbl.func then
                print(("Invalid return for %s.menu_cards(), ignoring"):format(i)); tbl = {}
            end
            
            for _, w in ipairs(tbl) do
                w.area = G.title_top
                w.bypass_discovery_center = true
                w.skip_materialize = true
                if not w.edition then w.no_edition = true end
                
                if w.set then
                    w.key = pseudorandom_element(G.P_CENTER_POOLS[w.set]).key
                    w.set = nil
                end
                local card = SMODS.create_card(w)
                card.mod_flag = i
                -- the magic incantation
                G.title_top.T.w = G.title_top.T.w + (1.7675 / math.max(#G.title_top.cards, 1))
                G.title_top.T.x = G.title_top.T.x - (0.885 / math.max(#G.title_top.cards, 1)) -- everyone who used -0.8 was WRONG
                card.T.w = card.T.w * 1.1 * 1.2
                card.T.h = card.T.h * 1.1 * 1.2
                -- card:hard_set_T(card.VT.x, card.VT.y, card.T.w * 1.32, card.T.h * 1.32)
                remove_all(card.children)
                card:set_sprites(card.config.center, card.base.id and card.config.card)
                
                G.title_top:emplace(card)
                card.no_ui = true
                card.states.visible = false
                
                G.E_MANAGER:add_event(Event({
                    trigger = "after",
                    delay = change_context == 'game' and 1.5 or 0,
                    blockable = false,
                    blocking = false,
                    func = function()
                        card.states.visible = true
                        if change_context == "splash" then
                            card:start_materialize({ G.C.WHITE, G.C.WHITE }, true, 2.5)
                        else
                            card:start_materialize({ G.C.WHITE, G.C.WHITE }, nil, 1.2)
                        end
                        return true
                    end
                }))
            end

        end
    end
    if remove_original then
        if not G.title_top.cards[1].mod_flag then 
            G.title_top.cards[1]:remove()
            G.title_top.T.w = G.title_top.T.w - (1.7675 / math.max(#G.title_top.cards, 1))
            G.title_top.T.x = G.title_top.T.x + (0.885 / math.max(#G.title_top.cards, 1))
        end
    end
    for i, v in ipairs(funcs) do v() end
end

local gameUpdateRef = Game.update
function Game:update(dt)
    if G.STATE ~= G.STATES.SPLASH and G.MAIN_MENU_UI then
        local node = G.MAIN_MENU_UI:get_UIE_by_ID("main_menu_play")
        
        if node and not node.children.alert then
            node.children.alert = UIBox({
                definition = create_UIBox_card_alert({
                    text = localize('b_modded_version'),
                    no_bg = true,
                    scale = 0.4,
                    text_rot = -0.2
                }),
                config = {
                    align = "tli",
                    offset = {
                        x = -0.1,
                        y = 0
                    },
                    major = node,
                    parent = node
                }
            })
            node.children.alert.states.collide.can = false
        end
    end
    gameUpdateRef(self, dt)
end

local function wrapText(text, maxChars)
    local wrappedText = ""
    local currentLineLength = 0

    for word in text:gmatch("%S+") do
        if currentLineLength + #word <= maxChars then
            wrappedText = wrappedText .. word .. ' '
            currentLineLength = currentLineLength + #word + 1
        else
            wrappedText = wrappedText .. '\n' .. word .. ' '
            currentLineLength = #word + 1
        end
    end

    return wrappedText
end

-- Helper function to concatenate author names
local function concatAuthors(authors)
    if type(authors) == "table" then
        return table.concat(authors, ", ")
    end
    return authors or localize('b_unknown')
end


SMODS.LAST_SELECTED_MOD_TAB = "mod_desc"
function create_UIBox_mods(args)
    local mod = G.ACTIVE_MOD_UI
    if not SMODS.LAST_SELECTED_MOD_TAB then SMODS.LAST_SELECTED_MOD_TAB = "mod_desc" end

    local mod_tabs = {}
    table.insert(mod_tabs, buildModDescTab(mod))
    local additions_tab = buildAdditionsTab(mod)
    if additions_tab then table.insert(mod_tabs, additions_tab) end
    local credits_func = mod.credits_tab
    if credits_func and type(credits_func) == 'function' then
        table.insert(mod_tabs, {
            label = localize("b_credits"),
            chosen = SMODS.LAST_SELECTED_MOD_TAB == "credits" or false,
            tab_definition_function = function(...)
                SMODS.LAST_SELECTED_MOD_TAB = "credits"
                return credits_func(...)
            end
        })
    end
    local config_func = mod.config_tab
    if config_func and type(config_func) == 'function' then
        table.insert(mod_tabs, {
            label = localize("b_config"),
            chosen = SMODS.LAST_SELECTED_MOD_TAB == "config" or false,
            tab_definition_function = function(...)
                SMODS.LAST_SELECTED_MOD_TAB = "config"
                return config_func(...)
            end
        })
    end

    local mod_has_achievement
    for _, v in pairs(SMODS.Achievements) do
        if v.mod.id == mod.id then mod_has_achievement = true end
    end
    if mod_has_achievement then table.insert(mod_tabs,
        {
            label = localize("b_achievements"),
            chosen = SMODS.LAST_SELECTED_MOD_TAB == "achievements" or false,
            tab_definition_function = function()
                SMODS.LAST_SELECTED_MOD_TAB = "achievements"
                return buildAchievementsTab(mod)
            end
        })
    end

    local custom_ui_func = mod.extra_tabs
    if custom_ui_func and type(custom_ui_func) == 'function' then
        local custom_tabs = custom_ui_func()
        if next(custom_tabs) and #custom_tabs == 0 then custom_tabs = { custom_tabs } end
        for i, v in ipairs(custom_tabs) do
            local id = mod.id..'_'..i
            v.chosen = (SMODS.LAST_SELECTED_MOD_TAB == id) or false
            v.label = v.label or ''
            local def = v.tab_definition_function
            assert(def, ('Custom defined mod tab with label "%s" from mod with id %s is missing definition function'):format(v.label, mod.id))
            v.tab_definition_function = function(...)
                SMODS.LAST_SELECTED_MOD_TAB = id
                return def(...)
            end
            table.insert(mod_tabs, v)
        end
    end

    return (create_UIBox_generic_options({
        colour = (mod.ui_config or {}).colour,
        bg_colour = (mod.ui_config or {}).bg_colour,
        back_colour = (mod.ui_config or {}).back_colour,
        outline_colour = (mod.ui_config or {}).outline_colour,
        back_func = "mods_button",
        contents = {
            {
                n = G.UIT.R,
                config = {
                    padding = 0,
                    align = "tm"
                },
                nodes = {
                    create_tabs({
                        snap_to_nav = true,
                        colour = (mod.ui_config or {}).tab_button_colour or G.C.BOOSTER,
                        tabs = mod_tabs
                    })
                }
            }
        }
    }))
end

function buildModDescTab(mod)
    G.E_MANAGER:add_event(Event({
        blockable = false,
        func = function()
            G.REFRESH_ALERTS = nil
            return true
        end
    }))
    local label = mod.name
    if (G.localization.descriptions.Mod or {})[mod.id] then
        label = localize { type = 'name_text', set = 'Mod', key = mod.id }
    end
    return {
        label = label,
        chosen = SMODS.LAST_SELECTED_MOD_TAB == "mod_desc" or false,
        tab_definition_function = function()
            local modNodes = {}
            local scale = 0.75 -- Scale factor for text
            local maxCharsPerLine = 50

            local wrappedDescription = wrapText(mod.description or '', maxCharsPerLine)

            local authors = localize('b_author' .. (#mod.author > 1 and 's' or '')) .. ': ' .. concatAuthors(mod.author)

            -- Authors names
            table.insert(modNodes, {
                n = G.UIT.R,
                config = {
                    align = "cm",
                    r = 0.1,
                    emboss = 0.1,
                    outline = 1,
                    padding = 0.07,
                    outline_colour = (mod.ui_config or {}).author_outline_colour,
                    colour = (mod.ui_config or {}).author_bg_colour,
                },
                nodes = {
                    {
                        n = G.UIT.T,
                        config = {
                            text = authors,
                            shadow = true,
                            scale = scale * 0.65,
                            colour = (mod.ui_config or {}).author_colour or G.C.BLUE,
                        }
                    }
                }
            })

            -- Mod description
            if (G.localization.descriptions.Mod or {})[mod.id] then
                modNodes[#modNodes + 1] = {}
                local loc_vars = mod.description_loc_vars and mod:description_loc_vars() or {}
                localize { type = 'descriptions', key = loc_vars.key or mod.id, set = 'Mod', nodes = modNodes[#modNodes], vars = loc_vars.vars, scale = loc_vars.scale, text_colour = loc_vars.text_colour, shadow = loc_vars.shadow }
                modNodes[#modNodes] = desc_from_rows(modNodes[#modNodes])
                modNodes[#modNodes].config.colour = loc_vars.background_colour or modNodes[#modNodes].config.colour
            else
                table.insert(modNodes, {
                    n = G.UIT.R,
                    config = {
                        padding = 0.2,
                        align = "cm"
                    },
                    nodes = {
                        {
                            n = G.UIT.T,
                            config = {
                                text = wrappedDescription,
                                shadow = true,
                                scale = scale * 0.5,
                                colour = G.C.UI.TEXT_LIGHT
                            }
                        }
                    }
                })
            end

            local custom_ui_func = mod.custom_ui
            if custom_ui_func and type(custom_ui_func) == 'function' then
                custom_ui_func(modNodes)
            end

            if not mod.can_load and not mod.disabled then
                local _, _, msg_key, specific_vars = getModtagInfo(mod)
                local text = localize { type = 'raw_descriptions', set = 'Other', key = msg_key, vars = specific_vars }
                local text_nodes = {}
                for _,v in ipairs(text) do
                    text_nodes[#text_nodes+1] = {
                        n = G.UIT.R, config = { align = 'cm' }, nodes = {
                            { n = G.UIT.T, config = { text = v, colour = G.SETTINGS.reduced_motion and G.C.WHITE or SMODS.Gradients.warning_text, scale = 0.35, shadow = true } }
                        }
                    }
                end
                table.insert(modNodes, { n = G.UIT.R, config = { align = "cm" }, nodes = {
                    { n = G.UIT.B, config = { w = 0.1, h = 0.1 }}
                }})
                table.insert(modNodes, {
                    n = G.UIT.R, config = { align = "cm", r = 0.1, minw = 6, minh = 0.6, colour = G.SETTINGS.reduced_motion and G.C.RED or SMODS.Gradients.warning_bg, padding = 0.1 }, nodes={
                        {
                            n = G.UIT.C, config = { align = 'cm' }, nodes = {
                                { n = G.UIT.O, config = { object = SMODS.create_sprite(0, 0, 0.8, 0.8, 'mod_tags', { x = 0, y = 0 }) } },
                            }
                        },
                        {
                            n = G.UIT.C, config = { align = 'cm' }, nodes = text_nodes
                        },
                        {
                            n = G.UIT.C, config = { align = 'cm' }, nodes = {
                                { n = G.UIT.O, config = { object = SMODS.create_sprite(0, 0, 0.8, 0.8, 'mod_tags', { x = 0, y = 0 }) } },
                            }
                        },
                    }
                })
            end

            return {
                n = G.UIT.ROOT,
                config = {
                    emboss = 0.05,
                    minh = 6,
                    r = 0.1,
                    minw = 6,
                    align = "tm",
                    padding = 0.2,
                    colour = G.C.BLACK
                },
                nodes = modNodes
            }
        end
    }
end

function buildAdditionsTab(mod)
    local consumable_nodes = {}
    for _, key in ipairs(SMODS.ConsumableType.visible_buffer) do
        local id = 'your_collection_'..key:lower()..'s'
        local tally = modsCollectionTally(G.P_CENTER_POOLS[key])
        if tally.of > 0 then
            consumable_nodes[#consumable_nodes+1] = UIBox_button({button = id, label = {localize('b_'..key:lower()..'_cards')}, count = tally, minw = 4, id = id, colour = G.C.SECONDARY_SET[key], text_colour = G.C.UI[key]})
        end
    end
    if #consumable_nodes > 3 then
        consumable_nodes = { UIBox_button({ button = 'your_collection_consumables', label = {localize('b_stat_consumables'), localize{ type = 'variable', key = 'c_types', vars = {#consumable_nodes} } }, count = modsCollectionTally(G.P_CENTER_POOLS.Consumeables), minw = 4, minh = 4, id = 'your_collection_consumables', colour = G.C.FILTER }) }
    end

    local leftside_nodes = {}
    for _, v in ipairs { { k = 'Joker', minh = 1.7, scale = 0.6 }, { k = 'Back', b = 'decks' }, { k = 'Voucher' } } do
        v.b = v.b or v.k:lower()..'s'
        v.l = v.l or v.b
        local tally = modsCollectionTally(G.P_CENTER_POOLS[v.k])
        if tally.of > 0 then
            leftside_nodes[#leftside_nodes+1] = UIBox_button({button = 'your_collection_'..v.b, label = {localize('b_'..v.l)}, count = modsCollectionTally(G.P_CENTER_POOLS[v.k]),  minw = 5, minh = v.minh, scale = v.scale, id = 'your_collection_'..v.b})
        end
    end
    if #consumable_nodes > 0 then
        leftside_nodes[#leftside_nodes + 1] = {
            n = G.UIT.R,
            config = { align = "cm", padding = 0.1, r = 0.2, colour = G.C.BLACK },
            nodes = {
                {
                    n = G.UIT.C,
                    config = { align = "cm", maxh = 2.9 },
                    nodes = {
                        { n = G.UIT.T, config = { text = localize('k_cap_consumables'), scale = 0.45, colour = G.C.L_BLACK, vert = true, maxh = 2.2 } },
                    }
                },
                { n = G.UIT.C, config = { align = "cm", padding = 0.15 }, nodes = consumable_nodes }
            }
        }
    end

    local rightside_nodes = {}
    for _, v in ipairs { { k = 'Enhanced', b = 'enhancements', l = 'enhanced_cards'}, { k = 'Seal' }, { k = 'Edition' }, { k = 'Booster', l = 'booster_packs' }, { b = 'tags', p = G.P_TAGS }, { b = 'blinds', p = G.P_BLINDS, minh = 2.0 }, } do
        v.b = v.b or v.k:lower()..'s'
        v.l = v.l or v.b
        v.p = v.p or G.P_CENTER_POOLS[v.k]
        local tally = modsCollectionTally(v.p)
        if tally.of > 0 then
            rightside_nodes[#rightside_nodes+1] = UIBox_button({button = 'your_collection_'..v.b, label = {localize('b_'..v.l)}, count = modsCollectionTally(v.p),  minw = 5, minh = v.minh, id = 'your_collection_'..v.b})
        end
    end
    local has_other_gameobjects = create_UIBox_Other_GameObjects()
    if has_other_gameobjects then
        rightside_nodes[#rightside_nodes+1] = UIBox_button({button = 'your_collection_other_gameobjects', label = {localize('k_other')}, minw = 5, id = 'your_collection_other_gameobjects', focus_args = {snap_to = true}})
    end

    local t = {n=G.UIT.R, config={align = "cm",padding = 0.2, minw = 7}, nodes={
        {n=G.UIT.C, config={align = "cm", padding = 0.15}, nodes = leftside_nodes },
      {n=G.UIT.C, config={align = "cm", padding = 0.15}, nodes = rightside_nodes }
    }}

    local modNodes = {}
    table.insert(modNodes, t)
    return (#leftside_nodes > 0 or #rightside_nodes > 0 ) and {
        label = localize("b_additions"),
        chosen = SMODS.LAST_SELECTED_MOD_TAB == "additions" or false,
        tab_definition_function = function()
            SMODS.LAST_SELECTED_MOD_TAB = "additions"
            return {
                n = G.UIT.ROOT,
                config = {
                    emboss = 0.05,
                    minh = 6,
                    r = 0.1,
                    minw = 6,
                    align = "tm",
                    padding = 0.2,
                    colour = G.C.BLACK
                },
                nodes = modNodes
            }
        end
    } or nil
end

-- Disable alerts when in Additions tab
local set_alerts_ref = set_alerts
function set_alerts()
    if G.ACTIVE_MOD_UI then
    else
        set_alerts_ref()
    end
end

G.FUNCS.your_collection_other_gameobjects = function(e)
    G.SETTINGS.paused = true
    G.FUNCS.overlay_menu{
      definition = create_UIBox_Other_GameObjects(),
    }
end

function create_UIBox_Other_GameObjects()
    local custom_gameobject_tabs = {{}}
    local curr_height = 0
    local curr_col = 1
    local other_collections_tabs = {}
    local smods_uibox_buttons = {
        {
            count = G.ACTIVE_MOD_UI and modsCollectionTally(SMODS.Stickers), --Returns nil outside of G.ACTIVE_MOD_UI but we don't use it anyways
            button = UIBox_button({button = 'your_collection_stickers', label = {localize('b_stickers')}, count = G.ACTIVE_MOD_UI and modsCollectionTally(SMODS.Stickers), minw = 5, id = 'your_collection_stickers'})
        },
        {
            count = G.ACTIVE_MOD_UI and modsCollectionTally(SMODS.PokerHands, nil, true),
            button = UIBox_button({button = 'your_collection_poker_hands', label = {localize('b_poker_hands')}, count = G.ACTIVE_MOD_UI and modsCollectionTally(SMODS.PokerHands, nil, true), minw = 5, id = 'your_collection_poker_hands'})
        },
    }

    if G.ACTIVE_MOD_UI then
        for _, tab in pairs(smods_uibox_buttons) do
            if tab.count.of > 0 then other_collections_tabs[#other_collections_tabs+1] = tab.button end
        end
        if G.ACTIVE_MOD_UI and G.ACTIVE_MOD_UI.custom_collection_tabs then
            object_tabs = G.ACTIVE_MOD_UI.custom_collection_tabs()
            for _, tab in ipairs(object_tabs) do
                other_collections_tabs[#other_collections_tabs+1] = tab
            end
        end
    else
        for _, tab in pairs(smods_uibox_buttons) do
            other_collections_tabs[#other_collections_tabs+1] = tab.button
        end
        for _, mod in pairs(SMODS.Mods) do
            if mod.custom_collection_tabs and type(mod.custom_collection_tabs) == "function" then
                object_tabs = mod.custom_collection_tabs()
                for _, tab in ipairs(object_tabs) do
                    other_collections_tabs[#other_collections_tabs+1] = tab
                end
            end
        end
    end

    local custom_gameobject_rows = {}
    if #other_collections_tabs > 0 then
        for _, gameobject_tabs in ipairs(other_collections_tabs) do
            table.insert(custom_gameobject_tabs[curr_col], gameobject_tabs)
            curr_height = curr_height + gameobject_tabs.nodes[1].config.minh
            if curr_height > 6 then --TODO: Verify that this is the ideal number
                curr_height = 0
                curr_col = curr_col + 1
                custom_gameobject_tabs[curr_col] = {}
            end
        end
        for _, v in ipairs(custom_gameobject_tabs) do
            table.insert(custom_gameobject_rows, {n=G.UIT.C, config={align = "cm", padding = 0.15}, nodes = v})
        end

        local t = {n=G.UIT.C, config={align = "cm", r = 0.1, colour = G.C.BLACK, padding = 0.1, emboss = 0.05, minw = 7}, nodes={
            {n=G.UIT.R, config={align = "cm", padding = 0.15}, nodes = custom_gameobject_rows}
        }}

        return create_UIBox_generic_options({
            colour = G.ACTIVE_MOD_UI and ((G.ACTIVE_MOD_UI.ui_config or {}).collection_colour or
            (G.ACTIVE_MOD_UI.ui_config or {}).colour),
            bg_colour = G.ACTIVE_MOD_UI and ((G.ACTIVE_MOD_UI.ui_config or {}).collection_bg_colour or
                (G.ACTIVE_MOD_UI.ui_config or {}).bg_colour),
            back_colour = G.ACTIVE_MOD_UI and ((G.ACTIVE_MOD_UI.ui_config or {}).collection_back_colour or
                (G.ACTIVE_MOD_UI.ui_config or {}).back_colour),
            outline_colour = G.ACTIVE_MOD_UI and ((G.ACTIVE_MOD_UI.ui_config or {}).collection_outline_colour or
                (G.ACTIVE_MOD_UI.ui_config or {}).outline_colour),
            back_func = G.ACTIVE_MOD_UI and "openModUI_" .. G.ACTIVE_MOD_UI.id or 'your_collection', contents = { t } }
        )
    else
        return nil
    end
end

G.FUNCS.your_collection_consumables = function(e)
    G.SETTINGS.paused = true
    G.FUNCS.overlay_menu{
      definition = create_UIBox_your_collection_consumables(),
    }
end

function create_UIBox_your_collection_consumables()
    local t = create_UIBox_generic_options({
        colour = G.ACTIVE_MOD_UI and ((G.ACTIVE_MOD_UI.ui_config or {}).collection_colour or
            (G.ACTIVE_MOD_UI.ui_config or {}).colour),
        bg_colour = G.ACTIVE_MOD_UI and ((G.ACTIVE_MOD_UI.ui_config or {}).collection_bg_colour or
            (G.ACTIVE_MOD_UI.ui_config or {}).bg_colour),
        back_colour = G.ACTIVE_MOD_UI and ((G.ACTIVE_MOD_UI.ui_config or {}).collection_back_colour or
            (G.ACTIVE_MOD_UI.ui_config or {}).back_colour),
        outline_colour = G.ACTIVE_MOD_UI and ((G.ACTIVE_MOD_UI.ui_config or {}).collection_outline_colour or
                (G.ACTIVE_MOD_UI.ui_config or {}).outline_colour),
        back_func = G.ACTIVE_MOD_UI and "openModUI_"..G.ACTIVE_MOD_UI.id or 'your_collection', contents = {
        { n = G.UIT.C, config = { align = 'cm', minw = 11.5, minh = 6 }, nodes = {
            { n = G.UIT.O, config = { id = 'consumable_collection', object = Moveable() },}
        }},
    }})
    G.E_MANAGER:add_event(Event({func = function()
        G.FUNCS.your_collection_consumables_page({ cycle_config = { current_option = 1 }})
        return true
    end}))
    return t
end

G.FUNCS.your_collection_consumables_page = function(args)
    if not args or not args.cycle_config then return end
  if G.OVERLAY_MENU then
    local uie = G.OVERLAY_MENU:get_UIE_by_ID('consumable_collection')
    if uie then
      if uie.config.object then
        uie.config.object:remove()
      end
      uie.config.object = UIBox{
        definition =  G.UIDEF.consumable_collection_page(args.cycle_config.current_option),
        config = { align = 'cm', parent = uie}
      }
    end
  end
end

G.UIDEF.consumable_collection_page = function(page)
    local nodes_per_page = 10
    local page_offset = nodes_per_page * ((page or 1) - 1)
    local type_buf = {}
    if G.ACTIVE_MOD_UI then
        for _, v in ipairs(SMODS.ConsumableType.visible_buffer) do
            if modsCollectionTally(G.P_CENTER_POOLS[v]).of > 0 then type_buf[#type_buf + 1] = v end
        end
    else
        type_buf = SMODS.ConsumableType.visible_buffer
    end
    local center_options = {}
    for i = 1, math.ceil(#type_buf / nodes_per_page) do
        table.insert(center_options,
            localize('k_page') ..
            ' ' .. tostring(i) .. '/' .. tostring(math.ceil(#type_buf / nodes_per_page)))
    end
    local option_nodes = { create_option_cycle({
        options = center_options,
        w = 4.5,
        cycle_shoulders = true,
        opt_callback = 'your_collection_consumables_page',
        focus_args = { snap_to = true, nav = 'wide' },
        current_option = page or 1,
        colour = G.ACTIVE_MOD_UI and (G.ACTIVE_MOD_UI.ui_config or {}).collection_option_cycle_colour or G.C.RED,
        no_pips = true
    }) }
    local function create_consumable_nodes(_start, _end)
        local t = {}
        for i = _start, _end do
            local key = type_buf[i]
            if not key then
                if i == _start then break end
                t[#t+1] = { n = G.UIT.R, config = { align ='cm', minh = 0.81 }, nodes = {}}
            else
                local id = 'your_collection_'..key:lower()..'s'
                t[#t+1] = UIBox_button({button = id, label = {localize('b_'..key:lower()..'_cards')}, count = G.ACTIVE_MOD_UI and modsCollectionTally(G.P_CENTER_POOLS[key]) or G.DISCOVER_TALLIES[key:lower()..'s'], minw = 4, id = id, colour = G.C.SECONDARY_SET[key], text_colour = G.C.UI[key]})
            end
        end
        return t
    end

    local t = { n = G.UIT.C, config = { align = 'cm' }, nodes = {
        {n=G.UIT.R, config = {align="cm"}, nodes = {
            {n=G.UIT.C, config={align = "tm", padding = 0.15}, nodes= create_consumable_nodes(page_offset + 1, page_offset + math.ceil(nodes_per_page/2))},
            {n=G.UIT.C, config={align = "tm", padding = 0.15}, nodes= create_consumable_nodes(page_offset+1+math.ceil(nodes_per_page/2), page_offset + nodes_per_page)},
        }},
        {n=G.UIT.R, config = {align="cm"}, nodes = option_nodes},
    }}
    return t
end

function buildAchievementsTab(mod, current_page)
    current_page = current_page or 1
    fetch_achievements()
    local achievement_matrix = {{},{}}
    local achievements_per_row = 3
    local achievements_pool = {}
    local achievement_original_order = {}
    for k, v in ipairs(SMODS.Achievement.obj_buffer) do
        local ach = SMODS.Achievements[v]
        if ach then
            if ach.mod and ach.mod.id == mod.id then achievements_pool[#achievements_pool+1] = ach end
        end
    end

    local achievement_tab = {}
    for k, v in ipairs(achievements_pool) do
        achievement_original_order[v.key] = #achievement_tab
        achievement_tab[#achievement_tab+1] = v
    end
    table.sort(achievement_tab, function(a, b) if a.order and b.order then return (a.order or 1) < (b.order or 1) else return achievement_original_order[a.key] < achievement_original_order[b.key] end end)

    local row = 1
    local max_lines = 2
    for i = 1, achievements_per_row*2 do
        local v = achievement_tab[i+((achievements_per_row*2)*(current_page-1))]
        if not v then break end
        local temp_achievement = SMODS.create_sprite(0, 0, 1.1, 1.1, v.atlas or "achievements", v.earned and v.pos or v.hidden_pos)
        temp_achievement:define_draw_steps({
            {shader = 'dissolve', shadow_height = 0.05},
            {shader = 'dissolve'}
        })
        if i == 1 then
            G.E_MANAGER:add_event(Event({
            trigger = 'immediate',
            func = (function()
                G.CONTROLLER:snap_to{node = temp_achievement}
                return true
            end)
            }))
        end
        temp_achievement.float = true
        temp_achievement.states.hover.can = true
        temp_achievement.states.drag.can = false
        temp_achievement.states.collide.can = true
        --temp_achievement.config = {blind = v, force_focus = true}
        temp_achievement.hover = function()
            if not G.CONTROLLER.dragging.target or G.CONTROLLER.using_touch then
                if not temp_achievement.hovering and temp_achievement.states.visible then
                    temp_achievement.hovering = true
                    temp_achievement.hover_tilt = 3
                    temp_achievement:juice_up(0.05, 0.02)
                    play_sound('chips1', math.random()*0.1 + 0.55, 0.12)
                    Node.hover(temp_achievement)
                    if temp_achievement.children.alert then
                        temp_achievement.children.alert:remove()
                        temp_achievement.children.alert = nil
                        v.alerted = true
                        G:save_progress()
                    end
                end
            end
            temp_achievement.stop_hover = function() temp_achievement.hovering = false; Node.stop_hover(temp_achievement); temp_achievement.hover_tilt = 0 end
        end

        -- Description
        local achievement_text = {}
        local maxCharsPerLine = 30
        local function wrapText(text, maxChars)
            local wrappedText = {""}
            local curr_line = 1
            local currentLineLength = 0

            for word in text:gmatch("%S+") do
                if currentLineLength + #word <= maxChars then
                    wrappedText[curr_line] = wrappedText[curr_line] .. word .. ' '
                    currentLineLength = currentLineLength + #word + 1
                else
                    wrappedText[curr_line] = string.sub(wrappedText[curr_line], 0, -2)
                    curr_line = curr_line + 1
                    wrappedText[curr_line] = ""
                    wrappedText[curr_line] = wrappedText[curr_line] .. word .. ' '
                    currentLineLength = #word + 1
                end
            end

            wrappedText[curr_line] = string.sub(wrappedText[curr_line], 0, -2)
            return wrappedText
        end

        local loc_target
        if (v.hidden_text and not v.earned) then
            loc_target = (localize(v.key.."_hidden", 'achievement_descriptions') ~= 'ERROR') and localize(v.key.."_hidden", 'achievement_descriptions') or {localize("hidden_achievement", 'achievement_descriptions')}
        else loc_target = localize(v.key, 'achievement_descriptions') end
        if type(loc_target) == 'string' then loc_target = wrapText(loc_target, maxCharsPerLine) end
        local loc_name
        if (v.hidden_name and not v.earned) then
            loc_name = (localize(v.key.."_hidden", 'achievement_names') ~= 'ERROR') and localize(v.key.."_hidden", 'achievement_names') or localize("hidden_achievement", 'achievement_names')
        else loc_name = localize(v.key, 'achievement_names') end

        local ability_text = {}
        if loc_target then
            for k, v in ipairs(loc_target) do
                ability_text[#ability_text + 1] = {n=G.UIT.R, config={align = "cm"}, nodes={{n=G.UIT.T, config={text = v, scale = 0.35, shadow = true, colour = G.C.WHITE}}}}
            end
        end
        max_lines = math.max(max_lines, #ability_text)
        achievement_text[#achievement_text + 1] =
        {n=G.UIT.R, config={align = "cm", emboss = 0.05, r = 0.1, minw = 4, maxw = 4, padding = 0.05, colour = G.C.WHITE, minh = 0.4*max_lines+0.1}, nodes={
            ability_text[1] and {n=G.UIT.R, config={align = "cm", padding = 0.08, colour = G.C.GREY, r = 0.1, emboss = 0.05, minw = 3.9, maxw = 3.9, minh = 0.4*max_lines}, nodes=ability_text} or nil
        }}

        table.insert(achievement_matrix[row], {
            n = G.UIT.C,
            config = { align = "cm", padding = 0.1 },
            nodes = {
                {n=G.UIT.R, config = {align = "cm"}, nodes = {
                    {n=G.UIT.R, config = {align = "cm", padding = 0.1}, nodes = {{ n = G.UIT.O, config = { object = temp_achievement, focus_with_object = true }}}},
                    {
                        n=G.UIT.R, config = {align = "cm", minw = 4, maxw = 4, padding = 0.05}, nodes = {
                            {n=G.UIT.R, config={align = "cm", emboss = 0.05, r = 0.1, padding = 0.1, minh = 0.6, colour = G.C.GREY}, nodes={
                                {n=G.UIT.O, config={align = "cm", maxw = 3.8, object = DynaText({string = loc_name, maxw = 3.8, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, spacing = 1, bump = true, scale = 0.4})}},
                            }},
                            {n=G.UIT.R, config={align = "cm"}, nodes=achievement_text},
                        },
                    },
                }},
            },
        })
        if #achievement_matrix[row] == achievements_per_row then
            row = row + 1
            achievement_matrix[row] = {}
            max_lines = 2
        end
    end

    local achievements_options = {}
    for i = 1, math.ceil(#achievements_pool/(2*achievements_per_row)) do
        table.insert(achievements_options, localize('k_page')..' '..tostring(i)..'/'..tostring(math.ceil(#achievements_pool/(2*achievements_per_row))))
    end

    local t = {
        {n=G.UIT.C, config={}, nodes={
        {n=G.UIT.C, config={align = "cm"}, nodes={
        {n=G.UIT.R, config={align = "cm"}, nodes={
            {n=G.UIT.R, config={align = "cm", padding = 0.1 }, nodes=achievement_matrix[1]},
            {n=G.UIT.R, config={align = "cm", padding = 0.1 }, nodes=achievement_matrix[2]},
            create_option_cycle({options = achievements_options, w = 4.5, cycle_shoulders = true, opt_callback = 'achievments_tab_page', focus_args = {snap_to = true, nav = 'wide'},current_option = current_page, colour = G.C.RED, no_pips = true})
        }}
        }}
    }}}
    return {
        n = G.UIT.ROOT,
        config = {
            emboss = 0.05,
            minh = 6,
            r = 0.1,
            minw = 6,
            align = "tm",
            padding = 0.2,
            colour = G.C.BLACK
        },
        nodes = t
    }
end

G.FUNCS.achievments_tab_page = function(args)
    if not args or not args.cycle_config then return end
    achievement_matrix = {{},{}}

    local tab_contents = G.OVERLAY_MENU:get_UIE_by_ID('tab_contents')
    tab_contents.config.object:remove()
    tab_contents.config.object = UIBox{
        definition = buildAchievementsTab(G.ACTIVE_MOD_UI, args.cycle_config.current_option),
        config = {offset = {x=0,y=0}, parent = tab_contents, type = 'cm'}
    }
    tab_contents.UIBox:recalculate()
end

-- TODO: Optimize this.
function modsCollectionTally(pool, set, ignore_discovered)
    local set = set or nil
    local obj_tally = {tally = 0, of = 0}

    for _, v in pairs(pool) do
        if v.mod and G.ACTIVE_MOD_UI.id == v.mod.id and not v.no_collection then
            if set then
                if v.set and v.set == set then
                    obj_tally.of = obj_tally.of+1
                    if ignore_discovered or v.discovered then
                        obj_tally.tally = obj_tally.tally+1
                    end
                end
            else
                obj_tally.of = obj_tally.of+1
                if ignore_discovered or v.discovered then
                    obj_tally.tally = obj_tally.tally+1
                end
            end
        end
    end

    return obj_tally
end

-- TODO: Make better solution
local UIBox_button_ref = UIBox_button
function UIBox_button(args)
    local button = UIBox_button_ref(args)
    button.nodes[1].config.count = args.count
    return button
end

function getModtagInfo(mod)
    local tag_pos, tag_message, tag_atlas = { x = 0, y = 0 }, "load_success", mod.prefix and mod.prefix .. '_modicon' or 'modicon'
    local specific_vars = {}

    if not mod.can_load then
        tag_message = "load_failure"
        tag_atlas = "mod_tags"
        specific_vars = {}
        if next(mod.load_issues.dependencies) then
            tag_message = tag_message..'_d'
            table.insert(specific_vars, concatAuthors(mod.load_issues.dependencies))
        end
        if next(mod.load_issues.conflicts) then
            tag_message = tag_message .. '_c'
            table.insert(specific_vars, concatAuthors(mod.load_issues.conflicts))
        end
        if mod.load_issues.outdated then tag_message = 'load_failure_o' end
        if mod.load_issues.version_mismatch then
            tag_message = 'load_failure_i'
            specific_vars = {mod.load_issues.version_mismatch, MODDED_VERSION:gsub('-STEAMODDED', '')}
        end
        if mod.load_issues.main_file_not_found then
            tag_message = 'load_failure_m'
            specific_vars = {mod.main_file}
        end
        if mod.load_issues.prefix_conflict then
            tag_message = 'load_failure_p'
            local name = mod.load_issues.prefix_conflict
            for _, m in ipairs(SMODS.mod_list) do
                if m.id == mod.load_issues.prefix_conflict then
                    name = m.name or name
                end
            end
            specific_vars = {name}
        end
        if mod.disabled then
            tag_pos = {x = 1, y = 0}
            tag_message = 'load_disabled'
        end
    end
    return tag_atlas, tag_pos, tag_message, specific_vars
end

function buildModtag(mod)
    local tag_atlas, tag_pos, tag_message, specific_vars = getModtagInfo(mod)

    local tag_sprite_tab = nil
    local units = 1.1
    local tag_sprite = SMODS.create_sprite(0, 0, 0.8*1, 0.8*1, SMODS.get_atlas(tag_atlas) or SMODS.get_atlas('tags'), tag_pos)
    tag_sprite.T.scale = 1
    tag_sprite_tab = {n= G.UIT.C, config={align = "cm", padding = 0}, nodes={
        {n=G.UIT.O, config={w=units, h=units, colour = G.C.BLUE, object = tag_sprite, focus_with_object = true}},
    }}
    tag_sprite:define_draw_steps({
        {shader = 'dissolve', shadow_height = 0.05},
        {shader = 'dissolve'},
    })
    tag_sprite.float = true
    tag_sprite.states.hover.can = true
    tag_sprite.states.click.can = true
    tag_sprite.states.drag.can = false
    tag_sprite.states.collide.can = true

    tag_sprite.hover = function(_self)
        if not G.CONTROLLER.dragging.target or G.CONTROLLER.using_touch then
            if not _self.hovering and _self.states.visible then
                _self.hovering = true
                if _self == tag_sprite then
                    _self.hover_tilt = 3
                    _self:juice_up(0.05, 0.02)
                    play_sound('paper1', math.random()*0.1 + 0.55, 0.42)
                    play_sound('tarot2', math.random()*0.1 + 0.55, 0.09)
                end
                tag_sprite.ability_UIBox_table = generate_card_ui({set = "Other", discovered = false, key = tag_message}, nil, specific_vars, 'Other', nil, false)
                _self.config.h_popup =  G.UIDEF.card_h_popup(_self)
                _self.config.h_popup_config ={align = 'tm', offset = {x= 0,y=-0.1},parent = _self}
                Node.hover(_self)
                if _self.children.alert then
                    _self.children.alert:remove()
                    _self.children.alert = nil
                    G:save_progress()
                end
            end
        end
    end
    tag_sprite.click = function(self)
        play_sound('button', 1, 0.3)
        G.ROOM.jiggle = G.ROOM.jiggle + 0.5
        G.FUNCS["openModUI_" .. mod.id](self)
    end
    tag_sprite.stop_hover = function(_self) _self.hovering = false; Node.stop_hover(_self); _self.hover_tilt = 0 end

    tag_sprite:juice_up()

    return tag_sprite_tab
end

local function createTextColNode(text, scale, colour, node)
    return { n = node or G.UIT.R, config = { padding = 0, align = "lc", maxw = 2.8, maxh = 1.5, }, nodes = {
                { n = G.UIT.T, config = { text = text, colour = colour or G.C.UI.TEXT_LIGHT, scale = scale * 0.7 } },
        }
    }
end


-- Helper function to create a clickable mod box
local function createClickableModBox(modInfo, scale)
    local function invert(c)
            return {1-c[1], 1-c[2], 1-c[3], c[4]}
        end
    local col, text_col, bg_col
    if modInfo.should_enable == nil then
        modInfo.should_enable = not modInfo.disabled
    end
    if SMODS.full_restart == nil then
        SMODS.full_restart = 0
    end

    if modInfo.can_load then
        col = mix_colours(G.C.UI.TEXT_DARK, {0.7,0.8,0.9,1}, 0.8)
    elseif modInfo.disabled then
        col = mix_colours(G.C.UI.BACKGROUND_INACTIVE, {0,0,0,1}, 0.6)
    else
        col = G.C.RED
        text_col = G.C.TEXT_DARK
    end
    bg_col = mix_colours({0.5,0.5,0.5,0.2},col,0.5)
    local label_nodes = {}
    local modname_split = SMODS.smart_line_splitter(modInfo.name,18,true)
    for _,v in ipairs(modname_split) do
        table.insert(label_nodes,createTextColNode(v, scale * 1.2, text_col))
    end
    local version_col = copy_table(G.C.WHITE)
    version_col[4] = 0.6
    if modInfo.lovely_only then
        table.insert(label_nodes,createTextColNode(localize('b_lovely_mod'), scale, version_col))
    end
    local sub_node_1 = {}
    local under_checkbox_nodes = {}
    if modInfo.version and modInfo.version ~= '0.0.0' then
        table.insert(sub_node_1, createTextColNode(('%s'):format(modInfo.version), scale, version_col, G.UIT.C))
    end
    if modInfo.config_tab then
        local is_config_func = type(modInfo.config_tab) == "function"
        table.insert(under_checkbox_nodes, {
            n = G.UIT.R,
            config = {
                page = is_config_func and "config",
                padding = 0.1,
                align = "cm",
                colour = is_config_func and G.C.BLUE,
                button = is_config_func and ("openModUI_" .. modInfo.id), shadow = is_config_func, shadow_height = 0.5, r = 0.1, hover = is_config_func },
            nodes = {
                {
                    n = G.UIT.O,
                    config = {
                        object = SMODS.create_sprite(0, 0, 0.3, 0.3, 'mod_tags', {x=2,y=0})
                    }
                }
            }
        })
    end
    if #sub_node_1 > 0 then
        table.insert(label_nodes, {
            n = G.UIT.R,
            config = {

            },
            nodes = sub_node_1
        })
    end
    if not modInfo.lovely_only then
        local tx = concatAuthors(modInfo.author, 12)
        local the_colour = mix_colours(G.C.BLACK, G.C.WHITE, 0.2)
        the_colour[4] = 0.8
        local authorDynatext = DynaText{
            string = tx,
            scale = scale * 0.7,
            colours = {the_colour},
            shadow = true,
            maxw = 2.4,
            marquee = true,
        }
        table.insert(label_nodes,
            { n = G.UIT.R, config = { padding = 0, align = "lc", maxw = 4.5, maxh = 1.5, }, nodes = {
                { n = G.UIT.T, config = {text= localize('b_by'), scale = scale*0.7, colour = the_colour}},
                {
                    n = G.UIT.O, config = {object = authorDynatext}
                }
            }
        })
    end
    if not _RELEASE_MODE and modInfo.priority then
        table.insert(label_nodes, createTextColNode(('%s%s'):format(localize('b_priority'), number_format(modInfo.priority)), scale, version_col))
    end

    return {
        n = G.UIT.C,
        config = { align = "cm", padding = 0.05 },
        nodes = {
            { n = G.UIT.C, config = { padding = 0.05, align = "cm", colour = bg_col, r = 0.1, minw = 1.5, minh = 1},
                nodes = {
                    {
                        n = G.UIT.C,
                        config = {
                            padding = 0.1,
                            align = "lc",
                            button = "openModUI_" .. modInfo.id,
                            minw = 4.25,
                            minh = 1.4,
                            maxh = 1.4,
                            r = 0.1,
                            colour = col,
                            shadow = true,
                            shadow_height = 0.5,
                            hover = true,
                        },
                        nodes = {
                            {
                                n = G.UIT.C,
                                config = { align = "cm" },
                                nodes = {
                                    buildModtag(modInfo),
                                }
                            },
                            {
                                n = G.UIT.C,
                                config = { align = "lc",},
                                nodes = label_nodes
                            },
                        }
                    },
                    {
                        n = G.UIT.C,
                        config = { padding = 0.05, align = "cm"},
                        nodes = {
                            {
                                n = G.UIT.R,
                                config = { align = "cm"},
                                nodes = {
                                    create_toggle({
                                    label = '',
                                    ref_table = modInfo,
                                    ref_value = 'should_enable',
                                    col = true,
                                    hide_label = true,
                                    w = 0,
                                    h = 0.2,
                                    scale = 1,
                                    callback = (
                                        function(_set_toggle)
                                            if not modInfo.should_enable then
                                                require"SMODS.preflight.loader".addToBlacklist(modInfo.blacklist_name)
                                                modInfo.blacklisted = true
                                            else
                                                if modInfo.lovelyIgnored then
                                                    NFS.remove(SMODS.MODS_DIR.. "/" .. modInfo.blacklist_name ..'/.lovelyignore')
                                                    modInfo.lovelyIgnored = false
                                                end
                                                if modInfo.blacklisted then
                                                    require"SMODS.preflight.loader".removeFromBlacklist(modInfo.blacklist_name)
                                                    modInfo.blacklisted = false
                                                end
                                            end
                                            local toChange = 1
                                            if modInfo.should_enable == not modInfo.disabled then
                                                toChange = -1
                                            end
                                            SMODS.full_restart = SMODS.full_restart + toChange
                                        end)
                                    })
                                }
                            },
                            unpack(under_checkbox_nodes)
                        }
                    }
                }
            }
        }
    }

end

function G.FUNCS.openModsDirectory(options)
    love.system.openURL(SMODS.MODS_DIR)
end

function G.FUNCS.mods_buttons_page(options)
    if not options or not options.cycle_config then
        return
    end
end

function SMODS.load_mod_config(mod)
    local s1, config = pcall(function()
        return load(NFS.read(('config/%s.jkr'):format(mod.id)), ('=[SMODS %s "config"]'):format(mod.id))()
    end)
    local s2, default_config = pcall(function()
        return load(NFS.read(mod.path..(mod.config_file or 'config.lua')), ('=[SMODS %s "default_config"]'):format(mod.id))()
    end)
    if not s1 or type(config) ~= 'table' then config = {} end
    if not s2 or type(default_config) ~= 'table' then default_config = {} end
    mod.config = default_config

    local function insert_saved_config(savedCfg, defaultCfg)
        for savedKey, savedVal in pairs(savedCfg) do
            local savedValType = type(savedVal)
            local defaultValType = type(defaultCfg[savedKey])
            if not defaultCfg[savedKey] then
                defaultCfg[savedKey] = savedVal
            elseif savedValType ~= defaultValType then
            elseif savedValType == "table" and defaultValType == "table" then
                insert_saved_config(savedVal, defaultCfg[savedKey])
            elseif savedVal ~= defaultCfg[savedKey] then
                defaultCfg[savedKey] = savedVal
            end

        end
    end

    insert_saved_config(config, mod.config)

    return mod.config
end
SMODS:load_mod_config()
function SMODS.save_mod_config(mod)
    local success = pcall(function()
        NFS.createDirectory('config')
        assert(mod.config and next(mod.config))
        local serialized = 'return '..serialize(mod.config)
        NFS.write(('config/%s.jkr'):format(mod.id), serialized)
    end)
    return success
end
function SMODS.save_all_config()
    SMODS:save_mod_config()
    for _, v in ipairs(SMODS.mod_list) do
        if v.can_load then
            local save_func = type(v.save_mod_config) == 'function' and v.save_mod_config or SMODS.save_mod_config
            save_func(v)
        end
    end
end

function G.FUNCS.exit_mods(e)
    G.ACTIVE_MOD_UI = nil
    SMODS.save_all_config()
    if SMODS.full_restart and SMODS.full_restart ~= 0 then
        -- launch a new instance of the game and quit the current one
        SMODS.restart_game()
    end
    SMODS.IN_MODS_TAB = nil
    if e then
        -- This is only needed when back button is pressed
        G.FUNCS.exit_overlay_menu(e)
    end
end

function create_UIBox_mods_button()
    local scale = 0.75
    SMODS.browse_search = SMODS.browse_search or ''
    return (create_UIBox_generic_options({
        back_func = 'exit_mods',
        contents = {
            {
                n = G.UIT.R,
                config = {
                    padding = 0,
                    align = "cm"
                },
                nodes = {
                    create_tabs({
                        snap_to_nav = true,
                        colour = G.C.BOOSTER,
                        tabs = {
                            {
                                label = localize('b_mods'),
                                chosen = true,
                                tab_definition_function = function()
                                    return SMODS.GUI.DynamicUIManager.initTab({
                                        updateFunctions = {
                                            modsList = G.FUNCS.update_mod_list,
                                        },
                                        staticPageDefinition = SMODS.GUI.staticModListContent()
                                    })
                                end
                            },
                            -- {
                            -- 	label = localize('b_browse'),
                            -- 	tab_definition_function = function()
                            -- 		return {
                            --             n = G.UIT.ROOT,
                            --             config = {
                            --                 align = "cm",
                            --                 padding = 0.05,
                            --                 colour = G.C.CLEAR,
                            --             },
                            --             nodes = {
                            -- 				{
                            -- 					n = G.UIT.C,
                            -- 					config = { align = 'cm' },
                            -- 					nodes = {
                            -- 						{
                            -- 							n = G.UIT.R,
                            -- 							config = { align = 'cl' },
                            -- 							nodes = {
                            -- 								create_text_input{
                            -- 									prompt_text = localize('b_search_prompt'),
                            -- 									max_length = 50,
                            -- 									text_scale = 0.6,
                            -- 									w = 6,
                            -- 									h = 1,
                            -- 									ref_table = SMODS,
                            -- 									ref_value = "browse_search",
                            -- 									extended_corpus = true,
                            -- 								},
                            -- 								UIBox_button{
                            -- 									button = 'browse_search',
                            -- 									label = {localize('b_search_button')},
                            -- 									minw = 3,
                            -- 									colour = G.C.RED
                            -- 								}
                            -- 							}
                            -- 						},
                            -- 						{
                            -- 							n = G.UIT.R,
                            -- 							config = { align = 'cm', emboss = 0.05, colour = G.C.BLACK, minh=5, minw=10.5},
                            -- 							nodes = {
                            -- 								{
                            -- 									n = G.UIT.O,
                            -- 									config = { align = 'cm', object = Moveable(), id = 'browse_mods'}
                            -- 								}
                            -- 							}
                            -- 						}
                            -- 					}
                            -- 				}
                            -- 			}
                            -- 		}
                            -- 	end,
                            -- },
                            {

                                label = localize('b_credits'),
                                tab_definition_function = function()
                                    return {
                                        n = G.UIT.ROOT,
                                        config = {
                                            emboss = 0.05,
                                            minh = 6,
                                            r = 0.1,
                                            minw = 6,
                                            align = "cm",
                                            padding = 0.2,
                                            colour = G.C.BLACK
                                        },
                                        nodes = {
                                            {
                                                n = G.UIT.R,
                                                config = {
                                                    padding = 0,
                                                    align = "cm"
                                                },
                                                nodes = {
                                                    {
                                                        n = G.UIT.T,
                                                        config = {
                                                            text = localize('b_mod_loader'),
                                                            shadow = true,
                                                            scale = scale * 0.8,
                                                            colour = G.C.UI.TEXT_LIGHT
                                                        }
                                                    }
                                                }
                                            },
                                            {
                                                n = G.UIT.R,
                                                config = {
                                                    padding = 0,
                                                    align = "cm"
                                                },
                                                nodes = {
                                                    {
                                                        n = G.UIT.T,
                                                        config = {
                                                            text = localize('b_developed_by'),
                                                            shadow = true,
                                                            scale = scale * 0.8,
                                                            colour = G.C.UI.TEXT_LIGHT
                                                        }
                                                    },
                                                    {
                                                        n = G.UIT.T,
                                                        config = {
                                                            text = "Steamo",
                                                            shadow = true,
                                                            scale = scale * 0.8,
                                                            colour = G.C.BLUE
                                                        }
                                                    }
                                                }
                                            },
                                            {
                                                n = G.UIT.R,
                                                config = {
                                                    padding = 0,
                                                    align = "cm"
                                                },
                                                nodes = {
                                                    {
                                                        n = G.UIT.T,
                                                        config = {
                                                            text = localize('b_rewrite_by'),
                                                            shadow = true,
                                                            scale = scale * 0.8,
                                                            colour = G.C.UI.TEXT_LIGHT
                                                        }
                                                    },
                                                    {
                                                        n = G.UIT.T,
                                                        config = {
                                                            text = "Aure",
                                                            shadow = true,
                                                            scale = scale * 0.8,
                                                            colour = G.C.BLUE
                                                        }
                                                    }
                                                }
                                            },
                                            {
                                                n = G.UIT.R,
                                                config = {
                                                    padding = 0.2,
                                                    align = "cm",
                                                },
                                                nodes = {
                                                    UIBox_button({
                                                        minw = 3.85,
                                                        button = "steamodded_github",
                                                        label = {localize('b_github_project')}
                                                    })
                                                }
                                            },
                                            {
                                                n = G.UIT.R,
                                                config = {
                                                    padding = 0.2,
                                                    align = "cm"
                                                },
                                                nodes = {
                                                    {
                                                        n = G.UIT.T,
                                                        config = {
                                                            text = localize('b_github_bugs_1')..'\n'..localize('b_github_bugs_2'),
                                                            shadow = true,
                                                            scale = scale * 0.5,
                                                            colour = G.C.UI.TEXT_LIGHT
                                                        }
                                                    },

                                                }
                                            },
                                        }
                                    }
                                end
                            },
                            {
                                label = localize('b_config'),
                                tab_definition_function = function()
                                    return {
                                        n = G.UIT.ROOT,
                                        config = {
                                            align = "cm",
                                            padding = 0.05,
                                            colour = G.C.CLEAR,
                                        },
                                        nodes = {
                                            create_toggle {
                                                label = localize('b_disable_mod_badges'),
                                                ref_table = SMODS.config,
                                                ref_value = 'no_mod_badges',
                                            },
                                            create_toggle {
                                                label = localize('b_seeded_unlocks'),
                                                info = {localize('b_seeded_unlocks_info')},
                                                ref_table = SMODS.config,
                                                ref_value = 'seeded_unlocks',
                                            },
                                            create_option_cycle {
                                                w = 4.5,
                                                scale = 0.8,
                                                label = localize('b_achievements'),
                                                options = localize('ml_achievement_settings'),
                                                opt_callback = 'update_achievement_settings',
                                                current_option = SMODS.config.achievements,
                                                cycle_shoulders = true,
                                            }
                                        }
                                    }
                                end
                            }
                        }
                    })
                }
            }
        }
    }))
end

G.FUNCS.update_achievement_settings = function(e)
    local opt = (e.cycle_config or {}).current_option or 1
    SMODS.config.achievements = opt
    G.F_NO_ACHIEVEMENTS = opt == 1
end

G.FUNCS.browse_search = function(e)
    SMODS.fetch_index()

end

G.FUNCS.browse_mods_page = function(args)
    local page = args.cycle_config and args.cycle_config.current_option or 1
end

function G.FUNCS.steamodded_github(e)
    love.system.openURL("https://github.com/Steamopollys/Steamodded")
end

function G.FUNCS.mods_button(e)
    G.SETTINGS.paused = true
    SMODS.LAST_SELECTED_MOD_TAB = nil
    SMODS.IN_MODS_TAB = true

    G.FUNCS.overlay_menu({
        definition = create_UIBox_mods_button()
    })
end

local create_UIBox_main_menu_buttonsRef = create_UIBox_main_menu_buttons
function create_UIBox_main_menu_buttons()
    local modsButton = UIBox_button({
        id = "mods_button",
        minh = 1.55,
        minw = 1.85,
        col = true,
        button = "mods_button",
        colour = SMODS.mod_button_alert and (G.SETTINGS.reduced_motion and G.C.RED or SMODS.Gradients.warning_bg) or G.C.BOOSTER,
        text_colour = (SMODS.mod_button_alert and not G.SETTINGS.reduced_motion) and SMODS.Gradients.warning_text or G.C.TEXT_LIGHT,
        label = {localize('b_mods_cap')},
        scale = 0.45 * 1.2
    })
    local menu = create_UIBox_main_menu_buttonsRef()
    table.insert(menu.nodes[1].nodes[1].nodes, modsButton)
    menu.nodes[1].nodes[1].config = {align = "cm", padding = 0.15, r = 0.1, emboss = 0.1, colour = G.C.L_BLACK, mid = true}
    if SMODS.mod_button_alert then
        G.E_MANAGER:add_event(Event({
            func = function()
                if G.MAIN_MENU_UI then -- Wait until the ui is rendered before spawning the alert
                    UIBox{definition = create_UIBox_card_alert(), config = {align="tri", offset = {x = 0.05, y = -0.05}, major = G.MAIN_MENU_UI:get_UIE_by_ID('mods_button'), can_collide = false}}
                    return true
                end
            end,
            blocking = false,
            blockable = false
        }))
    end
    return menu
end

local create_UIBox_profile_buttonRef = create_UIBox_profile_button
function create_UIBox_profile_button()
    local profile_menu = create_UIBox_profile_buttonRef()
    profile_menu.nodes[1].config = {align = "cm", padding = 0.11, r = 0.1, emboss = 0.1, colour = G.C.L_BLACK}
    return(profile_menu)
end

-- Disable achievments and crash report upload
function initGlobals()
    G.F_NO_ACHIEVEMENTS = SMODS.config.achievements == 1
    G.F_CRASH_REPORTS = false
end

function G.FUNCS.update_mod_list(args)
    if not args or not args.cycle_config then return end
    SMODS.GUI.DynamicUIManager.updateDynamicAreas({
        ["modsList"] = SMODS.GUI.dynamicModListContent(args.cycle_config.current_option)
    })
end

-- Same as Balatro base game code, but accepts a value to match against (rather than the index in the option list)
-- e.g. create_option_cycle({ current_option = 1 })  vs. SMODS.GUI.createOptionSelector({ current_option = "Page 1/2" })
function SMODS.GUI.createOptionSelector(args)
    args = args or {}
    args.colour = args.colour or G.C.RED
    args.options = args.options or {
        'Option 1',
        'Option 2'
    }

    local current_option_index = 1
    for i, option in ipairs(args.options) do
        if option == args.current_option then
            current_option_index = i
            break
        end
    end
    args.current_option_val = args.options[current_option_index]
    args.current_option = current_option_index
    args.opt_callback = args.opt_callback or nil
    args.scale = args.scale or 1
    args.ref_table = args.ref_table or nil
    args.ref_value = args.ref_value or nil
    args.w = (args.w or 2.5)*args.scale
    args.h = (args.h or 0.8)*args.scale
    args.text_scale = (args.text_scale or 0.5)*args.scale
    args.l = '<'
    args.r = '>'
    args.focus_args = args.focus_args or {}
    args.focus_args.type = 'cycle'

    local info = nil
    if args.info then
        info = {}
        for k, v in ipairs(args.info) do
            table.insert(info, {n=G.UIT.R, config={align = "cm", minh = 0.05}, nodes={
                {n=G.UIT.T, config={text = v, scale = 0.3*args.scale, colour = G.C.UI.TEXT_LIGHT}}
            }})
        end
        info =  {n=G.UIT.R, config={align = "cm", minh = 0.05}, nodes=info}
    end

    local disabled = #args.options < 2
    local pips = {}
    for i = 1, #args.options do
        pips[#pips+1] = {n=G.UIT.B, config={w = 0.1*args.scale, h = 0.1*args.scale, r = 0.05, id = 'pip_'..i, colour = args.current_option == i and G.C.WHITE or G.C.BLACK}}
    end

    local choice_pips = not args.no_pips and {n=G.UIT.R, config={align = "cm", padding = (0.05 - (#args.options > 15 and 0.03 or 0))*args.scale}, nodes=pips} or nil

    local t =
    {n=G.UIT.C, config={align = "cm", padding = 0.1, r = 0.1, colour = G.C.CLEAR, id = args.id and (not args.label and args.id or nil) or nil, focus_args = args.focus_args}, nodes={
        {n=G.UIT.C, config={align = "cm",r = 0.1, minw = 0.6*args.scale, hover = not disabled, colour = not disabled and args.colour or G.C.BLACK,shadow = not disabled, button = not disabled and 'option_cycle' or nil, ref_table = args, ref_value = 'l', focus_args = {type = 'none'}}, nodes={
            {n=G.UIT.T, config={ref_table = args, ref_value = 'l', scale = args.text_scale, colour = not disabled and G.C.UI.TEXT_LIGHT or G.C.UI.TEXT_INACTIVE}}
        }},
        args.mid and
                {n=G.UIT.C, config={id = 'cycle_main'}, nodes={
                    {n=G.UIT.R, config={align = "cm", minh = 0.05}, nodes={
                        args.mid
                    }},
                    not disabled and choice_pips or nil
                }}
                or {n=G.UIT.C, config={id = 'cycle_main', align = "cm", minw = args.w, minh = args.h, r = 0.1, padding = 0.05, colour = args.colour,emboss = 0.1, hover = true, can_collide = true, on_demand_tooltip = args.on_demand_tooltip}, nodes={
            {n=G.UIT.R, config={align = "cm"}, nodes={
                {n=G.UIT.R, config={align = "cm"}, nodes={
                    {n=G.UIT.O, config={object = DynaText({string = {{ref_table = args, ref_value = "current_option_val"}}, colours = {G.C.UI.TEXT_LIGHT},pop_in = 0, pop_in_rate = 8, reset_pop_in = true,shadow = true, float = true, silent = true, bump = true, scale = args.text_scale, non_recalc = true})}},
                }},
                {n=G.UIT.R, config={align = "cm", minh = 0.05}, nodes={
                }},
                not disabled and choice_pips or nil
            }}
        }},
        {n=G.UIT.C, config={align = "cm",r = 0.1, minw = 0.6*args.scale, hover = not disabled, colour = not disabled and args.colour or G.C.BLACK,shadow = not disabled, button = not disabled and 'option_cycle' or nil, ref_table = args, ref_value = 'r', focus_args = {type = 'none'}}, nodes={
            {n=G.UIT.T, config={ref_table = args, ref_value = 'r', scale = args.text_scale, colour = not disabled and G.C.UI.TEXT_LIGHT or G.C.UI.TEXT_INACTIVE}}
        }},
    }}

    if args.cycle_shoulders then
        t =
        {n=G.UIT.R, config={align = "cm", colour = G.C.CLEAR}, nodes = {
            {n=G.UIT.C, config={minw = 0.7,align = "cm", colour = G.C.CLEAR,func = 'set_button_pip', focus_args = {button = 'leftshoulder', type = 'none', orientation = 'cm', scale = 0.7, offset = {x = -0.1, y = 0}}}, nodes = {}},
            {n=G.UIT.C, config={id = 'cycle_shoulders', padding = 0.1}, nodes={t}},
            {n=G.UIT.C, config={minw = 0.7,align = "cm", colour = G.C.CLEAR,func = 'set_button_pip', focus_args = {button = 'rightshoulder', type = 'none', orientation = 'cm', scale = 0.7, offset = {x = 0.1, y = 0}}}, nodes = {}},
        }}
    else
        t =
        {n=G.UIT.R, config={align = "cm", colour = G.C.CLEAR, padding = 0.0}, nodes = {
            t
        }}
    end
    if args.label or args.info then
        t = {n=G.UIT.R, config={align = "cm", padding = 0.05, id = args.id or nil}, nodes={
            args.label and {n=G.UIT.R, config={align = "cm"}, nodes={
                {n=G.UIT.T, config={text = args.label, scale = 0.5*args.scale, colour = G.C.UI.TEXT_LIGHT}}
            }} or nil,
            t,
            info,
        }}
    end
    return t
end

local function generateBaseNode(staticPageDefinition)
    return {
        n = G.UIT.ROOT,
        config = {
            emboss = 0.05,
            minh = 6,
            r = 0.1,
            minw = 8,
            align = "cm",
            colour = G.C.BLACK
        },
        nodes = {
            staticPageDefinition
        }
    }
end

-- Initialize a tab with sections that can be updated dynamically (e.g. modifying text labels, showing additional UI elements after toggling buttons, etc.)
function SMODS.GUI.DynamicUIManager.initTab(args)
    local updateFunctions = args.updateFunctions
    local staticPageDefinition = args.staticPageDefinition

    for _, updateFunction in pairs(updateFunctions) do
        G.E_MANAGER:add_event(Event({func = function()
            updateFunction{cycle_config = {}}
            return true
        end}))
    end
    return generateBaseNode(staticPageDefinition)
end

-- Call this to trigger an update for a list of dynamic content areas
function SMODS.GUI.DynamicUIManager.updateDynamicAreas(uiDefinitions)
    for id, uiDefinition in pairs(uiDefinitions) do
        local dynamicArea = G.OVERLAY_MENU:get_UIE_by_ID(id)
        if dynamicArea and dynamicArea.config.object then
            dynamicArea.config.object:remove()
            dynamicArea.config.object = UIBox{
                definition = uiDefinition,
                config = {offset = {x=0, y=0.5}, align = 'cm', parent = dynamicArea}
            }
        end
    end
end

local function recalculateModsList(page)
    page = page or SMODS.LAST_VIEWED_MODS_PAGE or 1
    SMODS.LAST_VIEWED_MODS_PAGE = page
    local modsRowPerPage = 4
    local modsColPerRow = 3
    local startIndex = (page - 1) * modsRowPerPage * modsColPerRow + 1
    local endIndex = startIndex + modsRowPerPage * modsColPerRow - 1
    local totalPages = math.ceil(#SMODS.mod_list / (modsRowPerPage * modsColPerRow))
    local currentPage = localize('k_page') .. ' ' .. page .. "/" .. totalPages
    local pageOptions = {}
    for i = 1, totalPages do
        table.insert(pageOptions, (localize('k_page') .. ' ' .. tostring(i) .. "/" .. totalPages))
    end
    local showingList = #SMODS.mod_list > 0

    return currentPage, pageOptions, showingList, startIndex, endIndex, modsRowPerPage, modsColPerRow
end

-- Define the content in the pane that does not need to update
-- Should include OBJECT nodes that indicate where the dynamic content sections will be populated
-- EX: in this pane the 'modsList' node will contain the dynamic content which is defined in the function below
function SMODS.GUI.staticModListContent()
    local scale = 0.75
    local currentPage, pageOptions, showingList = recalculateModsList()
    return {
        n = G.UIT.R,
        config = {
            minh = 8.5,
            r = 0.1,
            minw = 17,
            align = "cm",
            padding = 0.05,
            colour = G.C.BLACK
        },
        nodes = {
            -- row container
            {
                n = G.UIT.C,
                config = { align = "cm", padding = 0.05 },
                nodes = {
                    -- column container
                    {
                        n = G.UIT.C,
                        config = { align = "cm", minw = 5, padding = 0.05, r = 0.1, colour = G.C.CLEAR },
                        nodes = {
                            -- title row
                            {
                                n = G.UIT.R,
                                config = {
                                    padding = 0.05,
                                    align = "cm"
                                },
                                nodes = {
                                    UIBox_button({
                                        label = { localize('b_mod_list') },
                                        shadow = true,
                                        scale = scale*0.85,
                                        colour = G.C.BOOSTER,
                                        button = "openModsDirectory",
                                        minh = scale,
                                        minw = 9
                                    }),
                                }
                            },

                            -- add some empty rows for spacing
                            {
                                n = G.UIT.R,
                                config = { align = "cm", padding = 0.05 },
                                nodes = {}
                            },
                            {
                                n = G.UIT.R,
                                config = { align = "cm", padding = 0.05 },
                                nodes = {}
                            },
                            {
                                n = G.UIT.R,
                                config = { align = "cm", padding = 0.05 },
                                nodes = {}
                            },
                            {
                                n = G.UIT.R,
                                config = { align = "cm", padding = 0.05 },
                                nodes = {}
                            },

                            -- dynamic content rendered in this row container
                            -- list of 4 x 4 mods on the current page
                            {
                                n = G.UIT.R,
                                config = {
                                    padding = 0.05,
                                    align = "cm",
                                    minh = 5,
                                    minw = 5
                                },
                                nodes = {
                                    {n=G.UIT.O, config={align = "cm", id = 'modsList', object = Moveable()}},
                                }
                            },

                            -- another empty row for spacing
                            {
                                n = G.UIT.R,
                                config = { align = "cm", padding = 0.8 },
                                nodes = {}
                            },
                            -- page selector
                            -- does not appear when list of mods is empty
                            showingList and SMODS.GUI.createOptionSelector({label = "", scale = 0.8, options = pageOptions, opt_callback = 'update_mod_list', no_pips = true, current_option = (
                                    currentPage
                            )}) or nil
                        }
                    },
                }
            },
        }
    }
end

function SMODS.GUI.dynamicModListContent(page)
    local scale = 0.75
    local _, __, showingList, startIndex, endIndex, modsRowPerPage, modsColPerRow = recalculateModsList(page)

    local modNodes = {}

    -- If no mods are loaded, show a default message
    if showingList == false then
        table.insert(modNodes, {
            n = G.UIT.R,
            config = {
                padding = 0,
                align = "cm"
            },
            nodes = {
                {
                    n = G.UIT.T,
                    config = {
                        text = localize('b_no_mods'),
                        shadow = true,
                        scale = scale * 0.5,
                        colour = G.C.UI.TEXT_DARK
                    }
                }
            }
        })
    else
        local modCount = 0
        local id = 0
        local current_row = {}

        for _, condition in ipairs({
            function(m) return not m.can_load and not m.disabled end,
            function(m) return m.can_load and m.config_tab end,
            function(m) return m.can_load and not m.config_tab end,
            function(m) return m.disabled end,
        }) do
            for _, modInfo in ipairs(SMODS.mod_list) do
                if modCount >= modsRowPerPage * modsColPerRow then break end
                if condition(modInfo) then
                    id = id + 1
                    if id >= startIndex and id <= endIndex then
                        table.insert(current_row, createClickableModBox(modInfo, scale * 0.5))
                        modCount = modCount + 1
                        if math.fmod(modCount, modsColPerRow) == 0 then
                            table.insert(modNodes, {
                                n = G.UIT.R,
                                config = { padding = 0, align = "lc"},
                                nodes = current_row
                            })
                            current_row = {}
                        end
                    end
                end
            end
        end
        if #current_row > 0 then
            table.insert(modNodes, {
                n = G.UIT.R,
                config = { padding = 0, align = "lc"},
                nodes = current_row
            })
        end
    end

    return {
        n = G.UIT.C,
        config = {
            r = 0.1,
            align = "cm",
            padding = 0,
        },
        nodes = modNodes
    }
end

G.FUNCS.SMODS_change_mipmap = function(args)
    SMODS.config.graphics_mipmap_level = args.to_key
    G:set_render_settings()
    SMODS:save_mod_config()
end

SMODS.card_collection_UIBox = function(_pool, rows, args)
    args = args or {}
    args.w_mod = args.w_mod or 1
    args.h_mod = args.h_mod or 1
    args.card_scale = args.card_scale or 1
    local deck_tables = {}
    local pool = SMODS.collection_pool(_pool)

    G.your_collection = {}
    local cards_per_page = 0
    local row_totals = {}
    for j = 1, #rows do
        if cards_per_page >= #pool and args.collapse_single_page then
            rows[j] = nil
        else
            row_totals[j] = cards_per_page
            cards_per_page = cards_per_page + rows[j]
            G.your_collection[j] = CardArea(
                G.ROOM.T.x + 0.2*G.ROOM.T.w/2,G.ROOM.T.h,
                (args.w_mod*rows[j]+0.25)*G.CARD_W,
                args.h_mod*G.CARD_H,
                {card_limit = rows[j], type = args.area_type or 'title', highlight_limit = 0, collection = true}
            )
            table.insert(deck_tables,
            {n=G.UIT.R, config={align = "cm", padding = 0.07, no_fill = true}, nodes={
                {n=G.UIT.O, config={object = G.your_collection[j]}}
            }})
        end
    end

    local options = {}
    for i = 1, math.ceil(#pool/cards_per_page) do
        table.insert(options, localize('k_page')..' '..tostring(i)..'/'..tostring(math.ceil(#pool/cards_per_page)))
    end

    G.FUNCS.SMODS_card_collection_page = function(e)
        if not e or not e.cycle_config then return end
        for j = 1, #G.your_collection do
            for i = #G.your_collection[j].cards, 1, -1 do
            local c = G.your_collection[j]:remove_card(G.your_collection[j].cards[i])
            c:remove()
            c = nil
            end
        end
        for j = 1, #rows do
            for i = 1, rows[j] do
            local center = pool[i+row_totals[j] + (cards_per_page*(e.cycle_config.current_option - 1))]
            if not center then break end
            local card = Card(G.your_collection[j].T.x + G.your_collection[j].T.w/2, G.your_collection[j].T.y, G.CARD_W*args.card_scale, G.CARD_H*args.card_scale, G.P_CARDS.empty, (args.center and G.P_CENTERS[args.center]) or center)
            if args.modify_card then args.modify_card(card, center, i, j) end
            if not args.no_materialize then card:start_materialize(nil, i>1 or j>1) end
            G.your_collection[j]:emplace(card)
            end
        end
        INIT_COLLECTION_CARD_ALERTS()
    end

    G.FUNCS.SMODS_card_collection_page{ cycle_config = { current_option = 1 }}

    local t = create_UIBox_generic_options({
        colour = G.ACTIVE_MOD_UI and ((G.ACTIVE_MOD_UI.ui_config or {}).collection_colour or (G.ACTIVE_MOD_UI.ui_config or {}).colour),
        bg_colour = G.ACTIVE_MOD_UI and ((G.ACTIVE_MOD_UI.ui_config or {}).collection_bg_colour or (G.ACTIVE_MOD_UI.ui_config or {}).bg_colour),
        back_colour = G.ACTIVE_MOD_UI and ((G.ACTIVE_MOD_UI.ui_config or {}).collection_back_colour or (G.ACTIVE_MOD_UI.ui_config or {}).back_colour),
        outline_colour = G.ACTIVE_MOD_UI and ((G.ACTIVE_MOD_UI.ui_config or {}).collection_outline_colour or
                (G.ACTIVE_MOD_UI.ui_config or {}).outline_colour),
        back_func = (args and args.back_func) or G.ACTIVE_MOD_UI and "openModUI_"..G.ACTIVE_MOD_UI.id or 'your_collection', snap_back = args.snap_back, infotip = args.infotip, contents = {
          {n=G.UIT.R, config={align = "cm", r = 0.1, colour = G.C.BLACK, emboss = 0.05}, nodes=deck_tables},
          (not args.hide_single_page or cards_per_page < #pool) and {n=G.UIT.R, config={align = "cm"}, nodes={
            create_option_cycle({options = options, w = 4.5, cycle_shoulders = true, opt_callback = 'SMODS_card_collection_page', current_option = 1, colour = G.ACTIVE_MOD_UI and (G.ACTIVE_MOD_UI.ui_config or {}).collection_option_cycle_colour or G.C.RED, no_pips = true, focus_args = {snap_to = true, nav = 'wide'}})
          }} or nil,
      }})
    return t
end

create_UIBox_your_collection_jokers = function()
    return SMODS.card_collection_UIBox(G.P_CENTER_POOLS.Joker, {5,5,5}, {
        no_materialize = true,
        modify_card = function(card, center) card.sticker = get_joker_win_sticker(center) end,
        h_mod = 0.95,
    })
end
create_UIBox_your_collection_boosters = function()
    return SMODS.card_collection_UIBox(G.P_CENTER_POOLS.Booster, {4,4}, {
        h_mod = 1.3,
        w_mod = 1.25,
        card_scale = 1.27,
    })
end
create_UIBox_your_collection_vouchers = function()
    return SMODS.card_collection_UIBox(G.P_CENTER_POOLS.Voucher, {4,4}, {
        area_type = 'voucher',
        modify_card = function(card, center, i, j)
            card.ability.order = i+(j-1)*4
        end,
    })
end
create_UIBox_your_collection_enhancements = function()
    return SMODS.card_collection_UIBox(G.P_CENTER_POOLS.Enhanced, {4,4}, {
        no_materialize = true,
        snap_back = true,
        h_mod = 1.03,
        infotip = localize('ml_edition_seal_enhancement_explanation'),
        hide_single_page = true,
    })
end
create_UIBox_your_collection_editions = function()
    return SMODS.card_collection_UIBox(G.P_CENTER_POOLS.Edition, {5,5}, {
        snap_back = true,
        h_mod = 1.03,
        infotip = localize('ml_edition_seal_enhancement_explanation'),
        hide_single_page = true,
        collapse_single_page = true,
        modify_card = function(card, center)
            if center.discovered then card:set_edition(center.key, true, true) end
        end,
    })
end

create_UIBox_your_collection_seals = function()
    return SMODS.card_collection_UIBox(G.P_CENTER_POOLS.Seal, {5,5}, {
        snap_back = true,
        infotip = localize('ml_edition_seal_enhancement_explanation'),
        hide_single_page = true,
        collapse_single_page = true,
        center = 'c_base',
        h_mod = 1.03,
        modify_card = function(card, center)
            card:set_seal(center.key, true)
        end,
    })
end

G.FUNCS.your_collection_stickers = function(e)
    G.SETTINGS.paused = true
    G.FUNCS.overlay_menu{
      definition = create_UIBox_your_collection_stickers(),
    }
end

create_UIBox_your_collection_stickers = function()
    return SMODS.card_collection_UIBox(SMODS.Stickers, { 5, 5 }, {
        snap_back = true,
        hide_single_page = true,
        collapse_single_page = true,
        center = 'c_base',
        h_mod = 1.03,
        back_func = 'your_collection_other_gameobjects',
        modify_card = function(card, center)
            card.ignore_pinned = true
            center:apply(card, true)
        end,
    })
end

G.FUNCS.your_collection_poker_hands = function(e)
    G.SETTINGS.paused = true
    G.FUNCS.overlay_menu{
      definition = create_UIBox_your_collection_poker_hands(),
    }
end

create_UIBox_your_collection_poker_hands = function (args)
    return create_UIBox_generic_options({
        colour = G.ACTIVE_MOD_UI and
        ((G.ACTIVE_MOD_UI.ui_config or {}).collection_colour or (G.ACTIVE_MOD_UI.ui_config or {}).colour),
        bg_colour = G.ACTIVE_MOD_UI and
        ((G.ACTIVE_MOD_UI.ui_config or {}).collection_bg_colour or (G.ACTIVE_MOD_UI.ui_config or {}).bg_colour),
        back_colour = G.ACTIVE_MOD_UI and
        ((G.ACTIVE_MOD_UI.ui_config or {}).collection_back_colour or (G.ACTIVE_MOD_UI.ui_config or {}).back_colour),
        outline_colour = G.ACTIVE_MOD_UI and ((G.ACTIVE_MOD_UI.ui_config or {}).collection_outline_colour or
            (G.ACTIVE_MOD_UI.ui_config or {}).outline_colour),
        back_func = (args and args.back_func) or G.ACTIVE_MOD_UI and "openModUI_" .. G.ACTIVE_MOD_UI.id or
            'your_collection_other_gameobjects',
        snap_back = args and args.snap_back,
        infotip = args and args.infotip,
        contents = {
            create_UIBox_current_hands(nil, true)
        }
    })
end

-- warning for updating during run
local igo = Game.init_game_object
function Game:init_game_object()
    local t = igo(self)
    t.smods_version = SMODS.version
    return t
end

local gurso = G.UIDEF.run_setup_option
function G.UIDEF.run_setup_option(_type)
    local ret = gurso(_type)
    if _type == 'Continue' and V(G.SAVED_GAME.GAME.smods_version or '0.0.0') ~= V(SMODS.version) then
        local text = localize { type = 'variable', key = 'smods_version_mismatch', vars = {G.SAVED_GAME.GAME.smods_version or '(unknown)', SMODS.version}}
        local text_nodes = {}
        for _,v in ipairs(text) do
            text_nodes[#text_nodes+1] = {
                n = G.UIT.R, config = { align = 'cm' }, nodes = {
                    { n = G.UIT.T, config = { text = v, colour = G.SETTINGS.reduced_motion and G.C.WHITE or SMODS.Gradients.warning_text, scale = 0.35, shadow = true } }
                }
            }
        end
        table.insert(ret.nodes[1].nodes, 1, {
            n = G.UIT.R, config = { align = "cm", r = 0.1, minw = 6, minh = 0.6, colour = G.SETTINGS.reduced_motion and G.C.RED or SMODS.Gradients.warning_bg, padding = 0.1 }, nodes={
                {
                    n = G.UIT.C, config = { align = 'cm' }, nodes = {
                        { n = G.UIT.O, config = { object = SMODS.create_sprite(0, 0, 0.8, 0.8, 'mod_tags', { x = 0, y = 0 }) } },
                    }
                },
                {
                    n = G.UIT.C, config = { align = 'cm' }, nodes = text_nodes
                },
                {
                    n = G.UIT.C, config = { align = 'cm' }, nodes = {
                        { n = G.UIT.O, config = { object = SMODS.create_sprite(0, 0, 0.8, 0.8, 'mod_tags', { x = 0, y = 0 }) } },
                    }
                },
            }
        })
    end
    return ret
end

-- Hand Score UI Utils

function SMODS.GUI.hand_score_display_ui(scale)
    return
    -- Outer shell container
    {n=G.UIT.R, config={align = "cm", id = 'hand_text_area', func = 'SMODS_scoring_calculation_function', colour = darken(G.C.BLACK, 0.1), r = 0.1, emboss = 0.05, padding = 0.03}, nodes={
        -- Inner shell container
        {n=G.UIT.C, config={align = "cm"}, nodes={
            -- Hand type display container
            SMODS.GUI.current_hand_ui(scale),
            -- Chips X Mult container
            SMODS.GUI.hand_chips_container(scale),
        }}
    }}
end

function SMODS.GUI.current_hand_ui(scale)
    return
    {n=G.UIT.R, config={align = "cm", minh = 1.1}, nodes={
        {n=G.UIT.O, config={id = 'hand_name', func = 'hand_text_UI_set',object = DynaText({string = {{ref_table = G.GAME.current_round.current_hand, ref_value = "handname_text"}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, float = true, scale = scale*1.4})}},
        {n=G.UIT.O, config={id = 'cry_asc', func = 'cry_asc_UI_set',object = DynaText({string = {{ref_table = G.GAME.current_round.current_hand, ref_value = "cry_asc_num_text"}}, colours = {G.C.GOLD}, shadow = true, float = true, scale = scale*1})}},
        {n=G.UIT.O, config={id = 'hand_chip_total', func = 'hand_chip_total_UI_set',object = DynaText({string = {{ref_table = G.GAME.current_round.current_hand, ref_value = "chip_total_text"}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, float = true, scale = scale*1.4})}},
        {n=G.UIT.T, config={ref_table = G.GAME.current_round.current_hand, ref_value='hand_level', scale = scale, colour = G.C.UI.TEXT_LIGHT, id = 'hand_level', shadow = true}},
    }}
end

function SMODS.GUI.hand_chips_container(scale)
    return
    {n=G.UIT.R, config={align = "cm", minh = 1, padding = 0.1}, nodes={
        -- Chips container
        SMODS.GUI.chips_container(scale),
        -- Operator
        SMODS.GUI.operator(scale),
        -- Mult container
        SMODS.GUI.mult_container(scale)
    }}
end

function SMODS.GUI.chips_container(scale)
    return
    {n=G.UIT.C, config={align = 'cm', id = 'hand_chips_container'}, nodes = {
        SMODS.GUI.score_container({
            type = 'chips',
            text = 'chip_text',
            align = 'cr',
        })
    }}
end

function SMODS.GUI.operator(scale)
    return
    {n=G.UIT.C, config={align = "cm", id = 'hand_operator_container'}, nodes={
        {n=G.UIT.T, config={text = "X", lang = G.LANGUAGES['en-us'], scale = scale*2, colour = G.C.UI_MULT, shadow = true}},
    }}
end

function SMODS.GUI.mult_container(scale)
    return
    {n=G.UIT.C, config={align = 'cm', id = 'hand_mult_container'}, nodes = {
        SMODS.GUI.score_container({
            type = 'mult'
        })
    }}
end

function SMODS.GUI.score_container(args)
    local scale = args.scale or 0.4
    local type = args.type
    local colour = args.colour or SMODS.Scoring_Parameters[type].colour
    local align = args.align or 'cl'
    local func = args.func or 'hand_type_UI_set'
    local text = args.text or type..'_text'
    local w = args.w or 2
    local h = args.h or 1
    return
    {n=G.UIT.R, config={align = align, minw = w, minh = h, r = 0.1, colour = colour, id = 'hand_'..type..'_area', emboss = 0.05}, nodes={
        {n=G.UIT.O, config={func = 'flame_handler', no_role = true, id = 'flame_'..type, object = Moveable(0,0,0,0), w = 0, h = 0, _w = w * 1.25, _h = h * 2.5}},
        -- TODO padding should depend only on 2nd letter of alignment?
        align == 'cl' and {n=G.UIT.B, config={w = 0.1, h = 0.1}} or nil,
        {n=G.UIT.O, config={id = 'hand_'..type, func = func, text = text, type = type, scale = scale*2.3, object = DynaText({
            string = {{ref_table = G.GAME.current_round.current_hand, ref_value = text}},
            colours = {G.C.UI.TEXT_LIGHT}, font = G.LANGUAGES['en-us'].font, shadow = true, float = true, scale = scale*2.3
        })}},
        align ~= 'cl' and {n=G.UIT.B, config={w = 0.1, h = 0.1}} or nil,
    }}
end

-- Internal function to automatically update UI boxes for new scoring parameters
G.FUNCS.hand_type_UI_set = function(e)
  local new_mult_text = number_format(G.GAME.current_round.current_hand[e.config.type] or SMODS.Scoring_Parameters[e.config.type].default_value)
  if new_mult_text ~= G.GAME.current_round.current_hand[e.config.text] then
    G.GAME.current_round.current_hand[e.config.text] = new_mult_text
    e.config.object.scale = scale_number(G.GAME.current_round.current_hand[e.config.type], e.config.scale, 1000)
    e.config.object:update_text()
    if not G.TAROT_INTERRUPT_PULSE then G.FUNCS.text_super_juice(e, math.max(0,math.floor(math.log10(type(G.GAME.current_round.current_hand[e.config.type]) == 'number' and math.abs(G.GAME.current_round.current_hand[e.config.type]) or 1)))) end
  end
end

function G.UIDEF.custom_deck_tab(_suit)
    local t = {}

    local rankCount = 0
    local lookup = {}
    for i, s in ipairs(SMODS.Suit:obj_list(true)) do
        local options = G.COLLABS.options[s.key]
        for i = 1, #options do
            local skin = SMODS.DeckSkins[options[i]]
            if skin.palettes and not (skin.display_ranks or skin.ranks) then
                for _, p in ipairs(skin.palettes) do
                    local p_ranks = p.display_ranks or p.ranks
                    for j = 1, #p_ranks do
                        if not lookup[p_ranks[j]] then
                            lookup[p_ranks[j]] = true
                            rankCount = rankCount + 1
                        end
                    end
                end
            elseif not skin.palettes and (skin.display_ranks or skin.ranks) then
                local ranks = skin.display_ranks or skin.ranks
                for j = 1, #ranks do
                    if not lookup[skin.ranks[j]] then
                        lookup[skin.ranks[j]] = true
                        rankCount = rankCount + 1
                    end
                end
            end
        end
    end

    G.cdds_cards = CardArea(
        0,0,
        math.min(math.max(rankCount*G.CARD_W*0.6, 4*G.CARD_W), 10*G.CARD_W),
        1.4*G.CARD_H,
        {card_limit = rankCount, type = 'title', highlight_limit = 0})
    G.cdds_cards.rankCount = rankCount

    table.insert(t, {n=G.UIT.R, config={align = "cm", colour = G.C.BLACK, r = 0.1, padding = 0.07, no_fill = true}, nodes={
        {n=G.UIT.O, config={object = G.cdds_cards}}
    }})

    local r = {n=G.UIT.R, config={align = "tm", minh = 1.35}, nodes={{n=G.UIT.R, config={align = "cm", minh = 0.3}, nodes = {}}}}
    local loc_options = localize(_suit, 'collabs')
    local conv_loc_options = {}
    for k, v in pairs(loc_options) do
      conv_loc_options[tonumber(k)] = v
    end

    loc_options = conv_loc_options

    local current_option = 1
    for k, v in pairs(G.COLLABS.options[_suit]) do
      if G.SETTINGS.CUSTOM_DECK.Collabs[_suit] == v then current_option = k end
    end

    local collab_cycle = create_option_cycle({options = loc_options, w = 5.5, cycle_shoulders = true, curr_suit = _suit, opt_callback = 'change_collab', current_option = current_option, colour = G.C.RED, focus_args = {snap_to = true, nav = 'wide'}})
    collab_cycle.nodes[2].config.padding = 0
    collab_cycle.nodes[2].nodes[1].config.padding = 0.085

    table.insert(r.nodes, {n=G.UIT.R, config={align = "cm"}, nodes={
        collab_cycle
    }})

    local deckskin_key = G.COLLABS.options[_suit][current_option]
    local palette_loc_options = SMODS.DeckSkin.get_palette_loc_options(deckskin_key, _suit)
    local selected_palette = 1
    for i, v in ipairs(G.COLLABS.colour_palettes[deckskin_key]) do
        if G.SETTINGS.colour_palettes[_suit] == v then
            selected_palette = i
        end
    end

    local palette_cycle =  #palette_loc_options > 1 and create_option_cycle({options = palette_loc_options, w = 4.5, h = 0.5, text_scale = 0.3, cycle_shoulders = false, curr_suit = _suit, curr_skin = deckskin_key, opt_callback = 'change_colour_palette', current_option = selected_palette, colour = G.C.ORANGE, focus_args = {snap_to = true, nav = 'wide'}}) or nil
    if palette_cycle then palette_cycle.nodes[1].config.padding = 0.085 end

    table.insert(r.nodes, {n=G.UIT.R, config={align = "cm", id = 'palette_selector', minh = 0.85}, nodes={
        palette_cycle
    }})
    table.insert(t, r)

    G.FUNCS.update_collab_cards(current_option, _suit, true)

    return {n=G.UIT.ROOT, config={align = "cm", padding = 0, colour = G.C.CLEAR, r = 0.1, minw = 7, minh = 4.2}, nodes=t}
end
