--[[ Copyright 2023-2025 Gustaf "Hanicef" Alhäll Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ]] local VERSION = 1 if quickmenu then if quickmenu.version < VERSION then error("You have already loaded an older version of quickmenu! Load the newest version first!") end return -- already loaded end rawset(_G, "quickmenu", {version = VERSION}) rawset(_G, "Entry", function (name, command, flags) if type(name) ~= "string" then error("Bad argument 1 to Entry, expected string but got " .. type(name), 2) end if type(command) ~= "string" and type(command) ~= "function" then error("Bad argument 2 to Entry, expected string or function but got " .. type(command), 2) end flags = flags or {} if type(flags) ~= "table" then error("Bad argument 3 to Entry, expected table but got " .. type(flags), 2) end return {name = name, command = command, flags = flags} end) rawset(_G, "Submenu", function (name, entries, flags) if type(name) ~= "string" then error("Bad argument 1 to Submenu, expected string but got " .. type(name), 2) end if type(entries) == "function" then if type(entries()) ~= "table" then error("Bad argument 2 to Submenu, function must return a table", 2) end elseif type(entries) ~= "table" then error("Bad argument 2 to Submenu, expected table or function but got " .. type(entries), 2) end flags = flags or {} if type(flags) ~= "table" then error("Bad argument 3 to Submenu, expected table but got " .. type(flags), 2) end return {name = name, entries = entries, flags = flags} end) local main_menu = {} function quickmenu.add_menu(submenu) -- FIXME: make sure this function is only called during lua load! if type(submenu) ~= "table" or (type(submenu.entries) ~= "table" and type(submenu.entries) ~= "function") then error("Bad argument 1 to add_menu, expected Submenu but got " .. type(submenu), 2) end table.insert(main_menu, submenu) end -- obligatory key constants steal from engine local KEY_DOWNARROW = (0x80+110) local KEY_UPARROW = (0x80+102) local KEY_BACKSPACE = 8 local KEY_ENTER = 13 local KEY_ESCAPE = 27 local KEY_F6 = (0x80+0x40) local quickmenu_open = false local current_menu local parent_menus local selected_menu = 1 addHook("MapChange", function () quickmenu_open = false end) local function filter_menu(menu) local entries = {} for _, entry in ipairs(menu) local ok = true if not multiplayer and entry.flags.multiplayer then ok = false elseif not (multiplayer and (not consoleplayer or #consoleplayer == 0 or IsPlayerAdmin(consoleplayer))) and entry.flags.admin then ok = false elseif multiplayer and entry.flags.singleplayer then ok = false end if ok then table.insert(entries, entry) end end return entries end local function do_key(key) if key == KEY_F6 then parent_menus = {} current_menu = filter_menu(main_menu) quickmenu_open = not quickmenu_open selected_menu = 1 return true end if quickmenu_open and key == KEY_ESCAPE then quickmenu_open = false return true end if not quickmenu_open then return end if key == KEY_DOWNARROW then selected_menu = selected_menu + 1 if selected_menu > #current_menu then selected_menu = 1 end S_StartSound(nil, sfx_menu1) return true end if key == KEY_UPARROW then selected_menu = selected_menu - 1 if selected_menu < 1 then selected_menu = #current_menu end S_StartSound(nil, sfx_menu1) return true end if key == KEY_ENTER then local menu = current_menu[selected_menu] if menu.command then if type(menu.command) == "function" then menu.command() elseif type(menu.command) == "string" then COM_BufAddText(consoleplayer, menu.command) end quickmenu_open = false else table.insert(parent_menus, {menu = current_menu, pos = selected_menu}) if type(menu.entries) == "function" then current_menu = filter_menu(menu.entries()) else current_menu = filter_menu(menu.entries) end assert(type(current_menu) == "table") selected_menu = 1 end S_StartSound(nil, sfx_menu1) return true end if key == KEY_BACKSPACE then if #parent_menus > 0 then current_menu = parent_menus[#parent_menus].menu selected_menu = parent_menus[#parent_menus].pos table.remove(parent_menus) end return true end end if not BASEVIDWIDTH then -- compat rawset(_G, "BASEVIDWIDTH", 320) rawset(_G, "BASEVIDHEIGHT", 200) end -- NOTE: most of the stuff is done on the client-side, so avoid sync here! if EvalMath ~= nil then -- i guess we doin 2.1 local has_setup = false COM_AddCommand("quickmenu_do_key", function (player, key) if consoleplayer and #player ~= #consoleplayer then return -- we have to sync due to 2.1 limitations, but we only need the current client end if key == "F6" then do_key(KEY_F6) elseif key == "DOWNARROW" then do_key(KEY_DOWNARROW) elseif key == "UPARROW" then do_key(KEY_UPARROW) elseif key == "BACKSPACE" then do_key(KEY_BACKSPACE) elseif key == "ENTER" then do_key(KEY_ENTER) end end, 0) addHook("PlayerQuit", function (player) if consoleplayer and #player == #consoleplayer then rawset(_G, "consoleplayer", nil) end end) addHook("ThinkFrame", function () if consoleplayer ~= nil and not has_setup then -- don't bother on servers COM_BufAddText(consoleplayer, "bind m \"quickmenu_do_key F6\"") COM_BufAddText(consoleplayer, "bind \"DOWN ARROW\" \"quickmenu_do_key DOWNARROW\"") COM_BufAddText(consoleplayer, "bind \"UP ARROW\" \"quickmenu_do_key UPARROW\"") COM_BufAddText(consoleplayer, "bind BACKSPACE \"quickmenu_do_key BACKSPACE\"") COM_BufAddText(consoleplayer, "bind ENTER \"quickmenu_do_key ENTER\"") has_setup = true end end) -- built-in menus, primarily for testing but can still be useful to end users quickmenu.add_menu(Submenu("Built-in mechanisms", { Entry("Enable devmode", "devmode 1", {singleplayer = true}), Entry("Disable devmode", "devmode 0", {singleplayer = true}), Entry("Exit level", "exitlevel", {admin = true}), Submenu("Kick player", function () local menu = {} for player in players.iterate() do table.insert(menu, Entry(player.name, "kick \"" .. player.name .. "\"", {admin = true})) end return menu end, {admin = true}), Submenu("Ban player", function () local menu = {} for player in players.iterate() do table.insert(menu, Entry(player.name, "ban \"" .. player.name .. "\"", {admin = true})) end return menu end, {admin = true}), Entry("Toggle mute", "toggle mute", {admin = true}), Entry("Toggle player collision", "toggle tailspickup", {admin = true}), })) else addHook("KeyDown", function (keyevent) if chatactive or gamestate ~= GS_LEVEL then return end return do_key(keyevent.num) end) -- built-in menus, primarily for testing but can still be useful to end users quickmenu.add_menu(Submenu("Built-in mechanisms", { Entry("Toggle input panel", "toggle showinput"), Entry("Enable devmode", "devmode 1", {singleplayer = true}), Entry("Disable devmode", "devmode 0", {singleplayer = true}), Entry("Exit level", "exitlevel", {admin = true}), Submenu("Kick player", function () local menu = {} for player in players.iterate() do table.insert(menu, Entry(player.name, "kick \"" .. player.name .. "\"", {admin = true})) end return menu end, {admin = true}), Submenu("Ban player", function () local menu = {} for player in players.iterate() do table.insert(menu, Entry(player.name, "ban \"" .. player.name .. "\"", {admin = true})) end return menu end, {admin = true}), Submenu("Mute player", function () local menu = {} for player in players.iterate() do table.insert(menu, Entry(player.name, "muteplayer \"" .. player.name .. "\"", {admin = true})) end return menu end, {admin = true}), Submenu("Unmute player", function () local menu = {} for player in players.iterate() do table.insert(menu, Entry(player.name, "unmuteplayer \"" .. player.name .. "\"", {admin = true})) end return menu end, {admin = true}), Entry("Toggle mute", "toggle mute", {admin = true}), Entry("Toggle player collision", "toggle tailspickup", {admin = true}), })) end hud.add(function (v, stplyr, cam) if consoleplayer == nil then -- 2.1, figure out who we are rawset(_G, "consoleplayer", stplyr) end if quickmenu_open then for i, menu in ipairs(current_menu) do local s = menu.name local y = BASEVIDHEIGHT-32-(#current_menu-i+1)*5 if menu.entries then s = s .. "..." end local map = 0 if i == selected_menu then map = V_YELLOWMAP v.drawScaled((BASEVIDWIDTH-170) << FRACBITS, y<