--[[ The ISC License Copyright (c) Varun Ramesh Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ]]-- local console = {} -- Utilty functions for manipulating tables. local function map(tbl, f) local t = {} for k,v in pairs(tbl) do t[k] = f(v) end return t end local function filter(tbl, f) local t, i = {}, 1 for _, v in ipairs(tbl) do if f(v) then t[i], i = v, i + 1 end end return t end local function push(tbl, ...) for _, v in ipairs({...}) do table.insert(tbl, v) end end console.HORIZONTAL_MARGIN = 10 -- Horizontal margin between the text and window. console.VERTICAL_MARGIN = 0 -- Vertical margins between components. console.PROMPT = "> " -- The prompt symbol. console.MAX_LINES = 2000 -- How many lines to store in the buffer. console.HISTORY_SIZE = 2000 -- How much of history to store. -- Color configurations. console.BACKGROUND_COLOR = {0, 0, 0, 0.4} console.TEXT_COLOR = {1, 1, 1, 1} console.ERROR_COLOR = {1, 0, 0, 1} console.FONT_SIZE = 12 console.FONT = love.graphics.newFont(console.FONT_SIZE) -- The scope in which lines in the console are executed. console.ENV = setmetatable({}, {__index = _G}) -- The default help text shown. console.HELP_TEXT = [[==== Welcome to the In-Game Console ==== - Type any expression or statement to evaluate it. - Type a built-in command to run it (type `commands` to list all commands).]] -- Builtin commands. console.COMMANDS = { clear = function() console.clear() end, quit = function() love.event.quit(0) end, exit = function() love.event.quit(0) end, help = function() print(console.HELP_TEXT) end, commands = function() print ("=== Available Commands ===") for k, _ in pairs(console.COMMANDS) do if console.COMMAND_HELP[k] then print(k .. " - " .. console.COMMAND_HELP[k]) else print(k) end end end } console.COMMAND_HELP = { clear = "Clear the sceen.", quit = "Quit the game.", exit = "Quit the game.", help = "Print help text.", commands = "List all commands." } function console.inspect(val) if type(val) == "table" then -- If this table has a tostring function, just use that. local mt = getmetatable(val) if mt and mt.__tostring then return tostring(val) end local result = "{ " -- First print out array-like keys, keeping track of which keys we've seen. local seen = {} for k, v in ipairs(val) do result = result .. tostring(v) .. ", " seen[k] = true end -- Now print out the reset of the keys. for k, v in pairs(val) do if seen[k] ~= true then result = result .. tostring(k) .. " = " .. tostring(v) .. ", " end end result = result .. "}" return result else return tostring(val) end end -- Overrideable function that is used for formatting return values. console.INSPECT_FUNCTION = function(...) local args = {...} if #args == 0 then return "nil" else return table.concat(map(args, console.inspect), "\t") end end -- Store global state for whether or not the console is enabled / disabled. local enabled = false function console.isEnabled() return enabled end -- Store the printed lines in a buffer. local lines = {} function console.clear() lines = {} end -- Store previously executed commands in a history buffer. local history = {} function console.addHistory(command) table.insert(history, 1, command) end -- Print a colored text to the console. Colored text is simply represented -- as a table of values that alternate between an {r, g, b, a} object and a -- string value. function console.colorprint(coloredtext) table.insert(lines, coloredtext) end -- Wrap the print function and redirect it to store into the line buffer. local normal_print = print _G.print = function(...) normal_print(...) -- Call original print function. local args = {...} local line = table.concat(map({...}, tostring), "\t") push(lines, line) while #lines > console.MAX_LINES do table.remove(lines, 1) end end -- Helper object that encapuslates operations on the current command. local command = { clear = function(self) -- Clear the current command. self.text, self.cursor, self.history_index = "", 0, 0 end, insert = function(self, input) -- Inert text at the cursor. self.text = self.text:sub(0, self.cursor) .. input .. self.text:sub(self.cursor + 1) self.cursor = self.cursor + 1 end, delete_backward = function(self) -- Delete the character before the cursor. if self.cursor > 0 then self.text = self.text:sub(0, self.cursor - 1) .. self.text:sub(self.cursor + 1) self.cursor = self.cursor - 1 end end, forward_character = function(self) self.cursor = math.min(self.cursor + 1, self.text:len()) end, backward_character = function(self) self.cursor = math.max(self.cursor - 1, 0) end, beginning_of_line = function(self) self.cursor = 0 end, end_of_line = function(self) self.cursor = self.text:len() end, forward_word = function(self) local word = self.text:match('%W*%w*', self.cursor + 1) self.cursor = math.min(self.cursor + word:len()) end, backward_word = function(self) local word = self.text:reverse():match('%W*%w*', self.text:len() - self.cursor + 1) self.cursor = math.max(self.cursor - word:len(), 0) end, previous = function(self) -- If there is no more history, don't do anything. if self.history_index + 1 > #history then return end -- If this is the first time, then save the command in case the user -- navigates back to the present command. if self.history_index == 0 then self.saved_command = self.text end self.history_index = math.min(self.history_index + 1, #history) self.text = history[self.history_index] self.cursor = self.text:len() end, next = function(self) -- If there is no more history, don't do anything. if self.history_index - 1 < 0 then return end self.history_index = math.max(self.history_index - 1, 0) if self.history_index == 0 then self.text = self.saved_command else self.text = history[self.history_index] end self.cursor = self.text:len() end } command:clear() function console.draw() -- Only draw the console if enabled. if not enabled then return end -- Fill the background color. love.graphics.setColor(unpack(console.BACKGROUND_COLOR)) love.graphics.rectangle('fill', 0, 0, love.graphics.getWidth(), love.graphics.getHeight()) love.graphics.setColor(255, 255, 255, 255) love.graphics.setFont(console.FONT) local line_start = love.graphics.getHeight() - console.VERTICAL_MARGIN*3 - console.FONT:getHeight() local wraplimit = love.graphics.getWidth() - console.HORIZONTAL_MARGIN*2 for i = #lines, 1, -1 do local textonly = lines[i] if type(lines[i]) == "table" then textonly = table.concat(filter(lines[i], function(val) return type(val) == "string" end), "") end width, wrapped = console.FONT:getWrap(textonly, wraplimit) love.graphics.printf( lines[i], console.HORIZONTAL_MARGIN, line_start - #wrapped * console.FONT:getHeight(), wraplimit, "left") line_start = line_start - #wrapped * console.FONT:getHeight() end --[[ love.graphics.setLineWidth(1) love.graphics.line(0, love.graphics.getHeight() - console.VERTICAL_MARGIN - console.FONT:getHeight() - console.VERTICAL_MARGIN, love.graphics.getWidth(), love.graphics.getHeight() - console.VERTICAL_MARGIN - console.FONT:getHeight() - console.VERTICAL_MARGIN) love.graphics.printf( console.PROMPT .. command.text, console.HORIZONTAL_MARGIN, love.graphics.getHeight() - console.VERTICAL_MARGIN - console.FONT:getHeight(), love.graphics.getWidth() - console.HORIZONTAL_MARGIN*2, "left") if love.timer.getTime() % 1 > 0.5 then local cursorx = console.HORIZONTAL_MARGIN + console.FONT:getWidth(console.PROMPT .. command.text:sub(0, command.cursor)) love.graphics.line( cursorx, love.graphics.getHeight() - console.VERTICAL_MARGIN - console.FONT:getHeight(), cursorx, love.graphics.getHeight() - console.VERTICAL_MARGIN) end ]] end function console.textinput(input) -- Use the "~" key to enable / disable the console. -- If disabled, ignore the input, otherwise insert at the cursor. if not enabled then return end command:insert(input) end function console.execute(command) -- If this is a builtin command, execute it and return immediately. if console.COMMANDS[command] then console.COMMANDS[command]() return end -- Reprint the command + the prompt string. print(console.PROMPT .. command) local chunk, error = load("return " .. command) if not chunk then chunk, error = load(command) end if chunk then setfenv(chunk, console.ENV) local values = { pcall(chunk) } if values[1] then table.remove(values, 1) print(console.INSPECT_FUNCTION(unpack(values))) -- Bind '_' to the first returned value, and bind 'last' to a list -- of returned values. console.ENV._ = values[1] console.ENV.last = values else console.colorprint({console.ERROR_COLOR, values[2]}) end else console.colorprint({console.ERROR_COLOR, error}) end end function console.keypressed(key, scancode, isrepeat) if love.keyboard.isDown("f10") then enabled = not enabled return end -- Ignore if the console isn't enabled. if not enabled then return end local ctrl = love.keyboard.isDown("lctrl", "lgui") local shift = love.keyboard.isDown("lshift") local alt = love.keyboard.isDown("lalt") if key == 'backspace' then command:delete_backward() elseif key == "up" then command:previous() elseif key == "down" then command:next() elseif alt and key == "left" then command:backward_word() elseif alt and key == "right" then command:forward_word() elseif ctrl and key == "left" then command:beginning_of_line() elseif ctrl and key == "right" then command:end_of_line() elseif key == "left" then command:backward_character() elseif key == "right" then command:forward_character() elseif key == "c" and ctrl then command:clear() elseif key == "=" and shift and ctrl then console.FONT_SIZE = console.FONT_SIZE + 1 console.FONT = love.graphics.newFont(console.FONT_SIZE) elseif key == "-" and ctrl then console.FONT_SIZE = math.max(console.FONT_SIZE - 1, 1) console.FONT = love.graphics.newFont(console.FONT_SIZE) elseif key == "return" then console.addHistory(command.text) console.execute(command.text) command:clear() end end return console