-- name: InputLayers -- description: Adds a simple input-layer API so multiple mods can share the same controller buttons without conflicts.\n\nVersion: 1.0.0 (ABI 1)\n\nAuthor: \\#b85469\\CrazyDany ---@class InputLayers.LayerOptions ---@field color table|nil Optional accent color for the layer: { r, g, b, a }. ---@class InputLayers.Config ---@field cycle_next_button integer Controller button that advances to the next layer (0 = disabled). ---@field cycle_prev_button integer Controller button that goes back to the previous layer (0 = disabled). ---@field hud_enabled boolean ---@field debug boolean ---@class InputLayers.API ---@field version string ---@field abi integer ---@field current_layer integer ---@field layers table ---@field config InputLayers.Config ---@field next_layer fun(): integer ---@field prev_layer fun(): integer ---@field set_layer fun(layer: string|integer): boolean ---@field is_active fun(layer: string|integer): boolean ---@field get_layer_id fun(name: string): integer? ---@field get_layer_name fun(): string? ---@field get_all_layers fun(): { id: integer, name: string, color: table|nil }[] ---@field add_layer fun(name: string, options: InputLayers.LayerOptions|nil): integer? ---@field hook fun(layer: string, callback: fun(m: MarioState, c: Controller): boolean?): integer ---@field unhook fun(layer: string, handle: integer) ---@field hook_global fun(callback: fun(m: MarioState, c: Controller): boolean?): integer ---@field unhook_global fun(handle: integer) ---@field hook_render fun(layer: string, callback: fun(m: MarioState)): integer ---@field unhook_render fun(layer: string, handle: integer) ---@field on_layer_change fun(callback: fun(old_id: integer, new_id: integer)): integer ---@field off_layer_change fun(handle: integer) -- --------------------------------------------------------------------------- -- Mod object -- --------------------------------------------------------------------------- ---@type InputLayers.API local mod = {} mod.version = "1.0.0" mod.abi = 1 mod.current_layer = 0 ---@type table mod.layers = { [0] = "CHAR", [1] = "CHAR_EXTRA", [2] = "GUI", [3] = "GUI_EXTRA", [4] = "OTHER", } ---@type InputLayers.Config mod.config = { cycle_next_button = Y_BUTTON, cycle_prev_button = X_BUTTON, hud_enabled = true, debug = false, } -- --------------------------------------------------------------------------- -- Internal state -- --------------------------------------------------------------------------- ---@type table local layer_ids = { CHAR = 0, CHAR_EXTRA = 1, GUI = 2, GUI_EXTRA = 3, OTHER = 4, } ---@type table local layer_options = {} local next_id = 5 ---@type table> local callbacks = {} ---@type table local hook_counters = {} ---@type table local global_callbacks = {} local global_counter = 0 ---@type table> local render_callbacks = {} ---@type table local render_counters = {} ---@type table local change_listeners = {} local change_counter = 0 local in_change = false -- --------------------------------------------------------------------------- -- Helpers -- --------------------------------------------------------------------------- local function lerp(a, b, t) return a + (b - a) * t end local function clamp(v, lo, hi) if v < lo then return lo end if v > hi then return hi end return v end ---@return integer[] local function sorted_ids() local ids = {} for id in pairs(mod.layers) do table.insert(ids, id) end table.sort(ids) return ids end ---@param ids integer[] ---@param id integer ---@return integer local function find_index(ids, id) for i, v in ipairs(ids) do if v == id then return i end end return 1 end -- --------------------------------------------------------------------------- -- Persistent state -- --------------------------------------------------------------------------- local STORAGE_KEY = "inputlayers_current" local function save_state() mod_storage_save_number(STORAGE_KEY, mod.current_layer) end local function load_state() local id = mod_storage_load_number(STORAGE_KEY) if id and mod.layers[id] then mod.current_layer = id end end -- --------------------------------------------------------------------------- -- Core layer logic -- --------------------------------------------------------------------------- ---@param id integer local function apply_current_layer(id) if mod.current_layer == id then return end local old = mod.current_layer mod.current_layer = id save_state() if in_change then return end in_change = true for _, cb in pairs(change_listeners) do local ok, err = pcall(cb, old, id) if not ok and mod.config.debug then print("[InputLayers] on_layer_change error: " .. tostring(err)) end end in_change = false end ---@return integer new_layer function mod.next_layer() local ids = sorted_ids() if #ids == 0 then return mod.current_layer end for i, id in ipairs(ids) do if id == mod.current_layer then local nid = ids[(i % #ids) + 1] apply_current_layer(nid) return nid end end apply_current_layer(ids[1]) return ids[1] end ---@return integer new_layer function mod.prev_layer() local ids = sorted_ids() if #ids == 0 then return mod.current_layer end for i, id in ipairs(ids) do if id == mod.current_layer then local pid = ids[((i - 2) % #ids) + 1] apply_current_layer(pid) return pid end end apply_current_layer(ids[#ids]) return ids[#ids] end ---@param layer string|integer ---@return boolean success function mod.set_layer(layer) if type(layer) == "number" then if mod.layers[layer] then apply_current_layer(layer) return true end return false end if type(layer) == "string" then local id = layer_ids[layer] if id then apply_current_layer(id) return true end end return false end ---@param layer string|integer ---@return boolean function mod.is_active(layer) if type(layer) == "number" then return mod.current_layer == layer end return layer_ids[layer] == mod.current_layer end ---@param name string ---@return integer? function mod.get_layer_id(name) if type(name) ~= "string" then return nil end return layer_ids[name] end ---@return string? function mod.get_layer_name() return mod.layers[mod.current_layer] end ---@return { id: integer, name: string, color: table|nil }[] function mod.get_all_layers() local out = {} for _, id in ipairs(sorted_ids()) do table.insert(out, { id = id, name = mod.layers[id], color = layer_options[id] and layer_options[id].color or nil, }) end return out end ---@param name string ---@param options InputLayers.LayerOptions|nil ---@return integer? id function mod.add_layer(name, options) if type(name) ~= "string" or name == "" then return nil end if layer_ids[name] then if options then local existing = layer_options[layer_ids[name]] or {} if options.color then existing.color = options.color end layer_options[layer_ids[name]] = existing end return layer_ids[name] end local id = next_id next_id = next_id + 1 mod.layers[id] = name layer_ids[name] = id callbacks[name] = callbacks[name] or {} layer_options[id] = options or {} return id end -- --------------------------------------------------------------------------- -- Hooks -- --------------------------------------------------------------------------- ---@param layer string ---@param callback fun(m: MarioState, c: Controller): boolean? ---@return integer handle function mod.hook(layer, callback) callbacks[layer] = callbacks[layer] or {} local handle = (hook_counters[layer] or 0) + 1 hook_counters[layer] = handle callbacks[layer][handle] = callback return handle end ---@param layer string ---@param handle integer function mod.unhook(layer, handle) if callbacks[layer] then callbacks[layer][handle] = nil end end ---@param callback fun(m: MarioState, c: Controller): boolean? ---@return integer handle function mod.hook_global(callback) global_counter = global_counter + 1 global_callbacks[global_counter] = callback return global_counter end ---@param handle integer function mod.unhook_global(handle) global_callbacks[handle] = nil end ---@param layer string ---@param callback fun(m: MarioState) ---@return integer handle function mod.hook_render(layer, callback) render_callbacks[layer] = render_callbacks[layer] or {} local handle = (render_counters[layer] or 0) + 1 render_counters[layer] = handle render_callbacks[layer][handle] = callback return handle end ---@param layer string ---@param handle integer function mod.unhook_render(layer, handle) if render_callbacks[layer] then render_callbacks[layer][handle] = nil end end ---@param callback fun(old_id: integer, new_id: integer) ---@return integer handle function mod.on_layer_change(callback) change_counter = change_counter + 1 change_listeners[change_counter] = callback return change_counter end ---@param handle integer function mod.off_layer_change(handle) change_listeners[handle] = nil end -- --------------------------------------------------------------------------- -- Expose API -- --------------------------------------------------------------------------- _G.InputLayers = mod load_state() -- --------------------------------------------------------------------------- -- Built-in layer cycling -- --------------------------------------------------------------------------- mod.hook_global(function(m, c) local next_btn = mod.config.cycle_next_button local prev_btn = mod.config.cycle_prev_button if next_btn ~= 0 and (c.buttonPressed & next_btn) ~= 0 then mod.next_layer() elseif prev_btn ~= 0 and (c.buttonPressed & prev_btn) ~= 0 then mod.prev_layer() end end) -- --------------------------------------------------------------------------- -- Input dispatch -- --------------------------------------------------------------------------- hook_event(HOOK_UPDATE, function() ---@type MarioState local m = gMarioStates[0] if not m or m.playerIndex ~= 0 then return end local c = m.controller if not c then return end local has_input = c.buttonPressed ~= 0 or c.buttonDown ~= 0 or c.buttonReleased ~= 0 or c.stickX ~= 0 or c.stickY ~= 0 if not has_input then return end for _, cb in pairs(global_callbacks) do local ok, absorb = pcall(cb, m, c) if not ok and mod.config.debug then print("[InputLayers] global callback error: " .. tostring(absorb)) end if ok and absorb == true then return end end local name = mod.layers[mod.current_layer] if not name then return end local list = callbacks[name] if not list then return end for _, cb in pairs(list) do local ok, absorb = pcall(cb, m, c) if not ok and mod.config.debug then print("[InputLayers] layer callback error: " .. tostring(absorb)) end if ok and absorb == true then break end end end) -- =========================================================================== -- Layer selector HUD -- =========================================================================== local UI = { panel_width = 68, panel_height = 13, gap = 3, right_padding = 5, active_text_scale = 0.42, side_text_scale = 0.30, visible_range = 2, scroll_factor = 0.28, idle_frames = 120, fade_factor = 0.10, alpha_falloff = 0.55, scale_falloff = 0.22, min_scale = 0.55, accent = { r = 184, g = 84, b = 105 }, active_bg = { r = 18, g = 16, b = 24, a = 225 }, side_bg = { r = 10, g = 9, b = 14, a = 150 }, active_text = { r = 255, g = 255, b = 255, a = 255 }, side_text = { r = 150, g = 140, b = 155, a = 190 }, border_thickness = 1, } local anim = { visual_pos = 0.0, alpha = 0.0, active_frames = 0, initialized = false, } ---@param current_visual number ---@param target_index integer ---@param count integer ---@return number local function closest_virtual(current_visual, target_index, count) local base = target_index - 1 local best = base local best_dist = math.abs(base - current_visual) local center_k = math.floor((current_visual - base) / count + 0.5) for k = center_k - 1, center_k + 1 do local candidate = base + k * count local d = math.abs(candidate - current_visual) if d < best_dist then best_dist = d best = candidate end end return best end ---@param label string ---@param cx number ---@param cy number ---@param offset number ---@param panel_w number ---@param panel_h number ---@param global_alpha number ---@param ui_scale number ---@param accent table local function render_panel_animated(label, cx, cy, offset, panel_w, panel_h, global_alpha, ui_scale, accent) djui_hud_set_font(FONT_NORMAL) local dist = math.abs(offset) local dist_alpha = clamp(1 - dist * UI.alpha_falloff, 0, 1) local dist_scale = clamp(1 - dist * UI.scale_falloff, UI.min_scale, 1) local final_alpha = global_alpha * dist_alpha if final_alpha <= 0.01 then return end local w = panel_w * dist_scale local h = panel_h * dist_scale local step = panel_h + UI.gap * ui_scale local y_center = cy + offset * step local x_right = cx + panel_w * 0.5 local x = x_right - w local y = y_center - h * 0.5 local highlight = clamp(1 - dist * 1.6, 0, 1) local bg = UI.side_bg local fg = UI.active_bg local bg_r = lerp(bg.r, fg.r, highlight) local bg_g = lerp(bg.g, fg.g, highlight) local bg_b = lerp(bg.b, fg.b, highlight) local bg_a = lerp(bg.a, fg.a, highlight) * final_alpha djui_hud_set_color(bg_r, bg_g, bg_b, bg_a) djui_hud_render_rect(x, y, w, h) if highlight > 0.01 then local bt = math.max(1, UI.border_thickness * ui_scale * dist_scale) local a = 255 * highlight * final_alpha djui_hud_set_color(accent.r, accent.g, accent.b, a) djui_hud_render_rect(x, y, w, bt) djui_hud_render_rect(x, y + h - bt, w, bt) djui_hud_render_rect(x, y, bt, h) djui_hud_render_rect(x + w - bt, y, bt, h) djui_hud_render_rect(x, y, bt * 2, h) end local text_scale = lerp(UI.side_text_scale, UI.active_text_scale, highlight) * ui_scale * dist_scale local tw = select(1, djui_hud_measure_text(label)) * text_scale local th = select(2, djui_hud_measure_text(label)) * text_scale local tx = x + (w - tw) / 2 local ty = y + (h - th) / 2 local st = UI.side_text local at = UI.active_text local tr = lerp(st.r, at.r, highlight) local tg = lerp(st.g, at.g, highlight) local tb = lerp(st.b, at.b, highlight) local ta = lerp(st.a, at.a, highlight) * final_alpha djui_hud_set_color(tr, tg, tb, ta) djui_hud_print_text(label, tx, ty, text_scale) end local function render_layer_selector() if not mod.config.hud_enabled then return end if anim.alpha <= 0.01 then return end local ids = sorted_ids() local count = #ids if count == 0 then return end local sw = djui_hud_get_screen_width() local sh = djui_hud_get_screen_height() local ui_scale = sh / 240 local panel_w = UI.panel_width * ui_scale local panel_h = UI.panel_height * ui_scale local pad = UI.right_padding * ui_scale local cx = sw - pad - panel_w * 0.5 local cy = sh * 0.5 local base = math.floor(anim.visual_pos) local to_render = {} for k = -UI.visible_range, UI.visible_range do local virtual_idx = base + k local real_idx = ((virtual_idx % count) + count) % count + 1 local offset = virtual_idx - anim.visual_pos if math.abs(offset) <= UI.visible_range + 0.5 then local real_id = ids[real_idx] local opts = layer_options[real_id] table.insert(to_render, { label = mod.layers[real_id], offset = offset, priority = math.abs(offset), accent = (opts and opts.color) or UI.accent, }) end end table.sort(to_render, function(a, b) return a.priority > b.priority end) for _, entry in ipairs(to_render) do render_panel_animated( entry.label, cx, cy, entry.offset, panel_w, panel_h, anim.alpha, ui_scale, entry.accent ) end end -- --------------------------------------------------------------------------- -- Animation update -- --------------------------------------------------------------------------- local last_current_layer = nil local function update_animation() local ids = sorted_ids() local count = #ids if count == 0 then return end local target_idx = find_index(ids, mod.current_layer) if not anim.initialized then anim.visual_pos = target_idx - 1 anim.alpha = 0 anim.active_frames = UI.idle_frames anim.initialized = true end local target_virtual = closest_virtual(anim.visual_pos, target_idx, count) anim.visual_pos = lerp(anim.visual_pos, target_virtual, UI.scroll_factor) if last_current_layer ~= mod.current_layer then last_current_layer = mod.current_layer anim.active_frames = UI.idle_frames end local m = gMarioStates[0] if m then local c = m.controller if c then local btn_mask = mod.config.cycle_next_button | mod.config.cycle_prev_button if btn_mask ~= 0 and (c.buttonPressed & btn_mask) ~= 0 then anim.active_frames = UI.idle_frames end end end anim.active_frames = anim.active_frames - 1 local target_alpha = (anim.active_frames > 0) and 1 or 0 anim.alpha = lerp(anim.alpha, target_alpha, UI.fade_factor) end hook_event(HOOK_UPDATE, update_animation) -- --------------------------------------------------------------------------- -- HUD render: user callbacks -- --------------------------------------------------------------------------- hook_event(HOOK_ON_HUD_RENDER_BEHIND, function() local m = gMarioStates[0] if not m then return end local name = mod.layers[mod.current_layer] if not name then return end local list = render_callbacks[name] if not list then return end for _, cb in pairs(list) do local ok, err = pcall(cb, m) if not ok and mod.config.debug then print("[InputLayers] render callback error: " .. tostring(err)) end end end) hook_event(HOOK_ON_HUD_RENDER_BEHIND, render_layer_selector)