From 2c53ac12bfeb99120e76a66fc1dae1593dec6f9d Mon Sep 17 00:00:00 2001 From: Moopli Date: Mon, 7 Jul 2014 20:50:27 -0400 Subject: [PATCH] Adds a long-overdue console window to Thrive (Issue #17). It's available everywhere, toggled with ` (backtick), has command history which is persistent per run (and could easily be extended to save to file also) and can be scrolled through with up/down, and evaluates each inputted line as a lua snippet in the same namespace as all the scripts -- important for testing functions. It currently does not eat key events, which is mildly inconvenient. Calls to print() are redirected to the console window. --- .gitignore | 2 +- assets/gui/layouts/Console.layout | 29 +++ scripts/console.lua | 358 +++++++++++++++++++++++++++ scripts/manifest.txt | 3 + src/engine/engine.cpp | 20 +- src/engine/engine.h | 9 + src/scripting/script_initializer.cpp | 7 - 7 files changed, 418 insertions(+), 10 deletions(-) create mode 100644 assets/gui/layouts/Console.layout create mode 100644 scripts/console.lua diff --git a/.gitignore b/.gitignore index 6765b8119cf..e7b7efc268c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ assets/* !assets/definitions/ !assets/definitions/*.xml # maybe add these in later -#!assets/gui/layouts/*.layout +!assets/gui/layouts/*.layout # Website directory diff --git a/assets/gui/layouts/Console.layout b/assets/gui/layouts/Console.layout new file mode 100644 index 00000000000..dc036634746 --- /dev/null +++ b/assets/gui/layouts/Console.layout @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/console.lua b/scripts/console.lua new file mode 100644 index 00000000000..5fc0e5ca7cc --- /dev/null +++ b/scripts/console.lua @@ -0,0 +1,358 @@ +-- In-game console, derived with heavy modification from Steve Donovan's ilua.lua +-- I do hope we're allowed to use it though. If not I'll just rewrite the copied sections. +-- Original header: +---------------------- +-- ilua.lua +-- A more friendly Lua interactive prompt +-- doesn't need '=' +-- will try to print out tables recursively, subject to the pretty_print_limit value. +-- Steve Donovan, 2007 +---------------------- + +class "ConsoleHud" +class "Interpreter" + +require "string" + +function ConsoleHud:__init(interpreter) + self.active = false + self.interpreter = interpreter + self.inputHistory = {} + self.inputHistoryIndex = 0 +end + +function ConsoleHud:update() + local gameState = Engine:currentGameState() + local root = gameState:rootGUIWindow() + local consoleWindow = root:getChild("ConsoleWindow") + local inputArea = consoleWindow:getChild("TextEntry") + if Engine.keyboard:wasKeyPressed(Keyboard.KC_GRAVE) then + self.active = not self.active + if self.active then + consoleWindow:show() + consoleWindow:enable() + inputArea:setFocus() + else + -- inputArea captures ` before deactivation, we don't want that. + text, _ = string.gsub(inputArea:getText(), "`", "") + inputArea:setText(text) + consoleWindow:disable() + consoleWindow:hide() + end + elseif self.active then + if Engine.keyboard:wasKeyPressed(Keyboard.KC_RETURN) then + -- push line to interpreter + local outputArea = consoleWindow:getChild("History") + local line = inputArea:getText() + self.interpreter:eval_lua(line) + inputArea:setText("") + outputArea:setText(self.interpreter.history) + self.inputHistoryIndex = #self.inputHistory + self.inputHistory[self.inputHistoryIndex + 1] = line + self.inputHistoryIndex = self.inputHistoryIndex + 1 + elseif Engine.keyboard:wasKeyPressed(Keyboard.KC_UP) and self.inputHistoryIndex > 0 then + self.inputHistoryIndex = self.inputHistoryIndex - 1 + inputArea:setText(self.inputHistory[self.inputHistoryIndex + 1]) + elseif Engine.keyboard:wasKeyPressed(Keyboard.KC_DOWN) and self.inputHistoryIndex < #self.inputHistory - 1 then + self.inputHistoryIndex = self.inputHistoryIndex + 1 + inputArea:setText(self.inputHistory[self.inputHistoryIndex + 1]) + end + end +end + +function Interpreter:__init() + self.pretty_print_limit = 20 + self.max_depth = 7 + self.table_clever = true + self.prompt = '> ' + self.verbose = false + self.strict = false + -- suppress strict warnings + _ = true + + -- imported global functions + self.sub = string.sub + self.match = string.match + self.find = string.find + self.push = table.insert + self.pop = table.remove + self.append = table.insert + self.concat = table.concat + self.floor = math.floor + self.write = io.write + self.read = io.read + + self.savef = nil + self.collisions = {} + self.G_LIB = {} + self.declared = {} + self.line_handler_fn = nil + self.global_handler_fn = nil + self.print_handlers = {} + + self.ilua = {} + self.num_prec = nil + self.num_all = nil + + self.jstack = {} + + self.history = "" + + -- functions available in scripts + function self.ilua.precision(len,prec,all) + if not len then num_prec = nil + else + num_prec = '%'..len..'.'..prec..'f' + end + num_all = all + end + + function self.ilua.table_options(t) + if t.limit then self.pretty_print_limit = t.limit end + if t.depth then self.max_depth = t.depth end + if t.clever ~= nil then self.table_clever = t.clever end + end + + -- inject @tbl into the global namespace + function self.ilua.import(tbl,dont_complain,lib) + lib = lib or '' + if type(tbl) == 'table' then + for k,v in pairs(tbl) do + local key = rawget(_G,k) + -- NB to keep track of collisions! + if key and k ~= '_M' and k ~= '_NAME' and k ~= '_PACKAGE' and k ~= '_VERSION' then + append(collisions,{k,lib,G_LIB[k]}) + end + _G[k] = v + G_LIB[k] = lib + end + end + if not dont_complain and #self.collisions > 0 then + for i, coll in ipairs(self.collisions) do + local name,lib,oldlib = coll[1],coll[2],coll[3] + write('warning: ',lib,'.',name,' overwrites ') + if oldlib then + self.write(oldlib,'.',name,'\n') + else + self.write('global ',name,'\n') + end + end + end + end + + function self.ilua.print_handler(name,handler) + self.print_handlers[name] = handler + end + + function self.ilua.line_handler(handler) + self.line_handler_fn = handler + end + + function self.ilua.global_handler(handler) + self.global_handler_fn = handler + end + + function self.ilua.print_variables() + for name,v in pairs(self.declared) do + print(name,type(_G[name])) + end + end + + -- any import complaints? + self.ilua.import() + + -- enable 'not declared' error + if self.strict then + self:set_strict() + end + +end + +function Interpreter:oprint(...) + if self.savef then + self.savef:write(table.concat({...},' '),'\n') + end + self.history = self.history .. table.concat({...}, ' ') .. "\n" +end + +function Interpreter:join(tbl,delim,limit,depth) + if not limit then limit = self.pretty_print_limit end + if not depth then depth = self.max_depth end + local n = #tbl + local res = '' + local k = 0 + -- very important to avoid disgracing ourselves with circular references... + if #self.jstack > depth then + return "..." + end + for i,t in ipairs(self.jstack) do + if tbl == t then + return "" + end + end + push(self.jstack,tbl) + -- this is a hack to work out if a table is 'list-like' or 'map-like' + -- you can switch it off with ilua.table_options {clever = false} + local is_list + if self.table_clever then + local index1 = n > 0 and tbl[1] + local index2 = n > 1 and tbl[2] + is_list = index1 and index2 + end + if is_list then + for i,v in ipairs(tbl) do + res = res..delim..self:val2str(v) + k = k + 1 + if k > limit then + res = res.." ... " + break + end + end + else + for key,v in pairs(tbl) do + if type(key) == 'number' then + key = '['..tostring(key)..']' + else + key = tostring(key) + end + res = res..delim..key..'='..self:val2str(v) + k = k + 1 + if k > limit then + res = res.." ... " + break + end + end + end + pop(self.jstack) + return sub(res,2) +end + +function Interpreter:val2str(val) + local tp = type(val) + if self.print_handlers[tp] then + local s = self.print_handlers[tp](val) + return s or '?' + end + if tp == 'function' then + return tostring(val) + elseif tp == 'table' then + if val.__tostring then + return tostring(val) + else + return '{'..join(val,',')..'}' + end + elseif tp == 'string' then + return "'"..val.."'" + elseif tp == 'number' then + -- we try only to apply floating-point precision for numbers deemed to be floating-point, + -- unless the 3rd arg to precision() is true. + if self.num_prec and (self.num_all or floor(val) ~= val) then + return self.num_prec:format(val) + else + return tostring(val) + end + else + return tostring(val) + end +end + +function Interpreter:_pretty_print(...) + for i,val in ipairs({...}) do + self:oprint(self:val2str(val)) + end + _G['_'] = ({...})[1] +end + +function Interpreter:compile(line) + if self.verbose then self:oprint(line) end + local f,err = load(line) + return err,f +end + +function Interpreter:evaluate(chunk) + local ok,res = pcall(chunk) + if not ok then + return res + end + return nil -- meaning, fine! +end + +function Interpreter:eval_lua(line) + if self.savef then + self.savef:write(prompt,line,'\n') + end + -- is the line handler interested? + if self.line_handler_fn then + line = self.line_handler_fn(line) + -- returning nil here means that the handler doesn't want + -- Lua to see the string + if not line then return end + end + -- is it an expression? + local err,chunk = self:compile('interpreter:_pretty_print('..line..')') + if err then + -- otherwise, a statement? + err,chunk = self:compile(line) + end + -- if compiled ok, then evaluate the chunk + if not err then + err = self:evaluate(chunk) + end + -- if there was any error, print it out + if err then + self:oprint(err) + end +end + +function Interpreter:quit(code,msg) + io.stderr:write(msg,'\n') + -- os.exit(code) +end + +-- +-- strict.lua +-- checks uses of undeclared global variables +-- All global variables must be 'declared' through a regular assignment +-- (even assigning nil will do) in a main chunk before being used +-- anywhere. +-- +function Interpreter:set_strict() + local mt = getmetatable(_G) + if mt == nil then + mt = {} + setmetatable(_G, mt) + end + + local function what () + local d = debug.getinfo(3, "S") + return d and d.what or "C" + end + + mt.__newindex = function (t, n, v) + self.declared[n] = true + rawset(t, n, v) + end + + mt.__index = function (t, n) + if not self.declared[n] and what() ~= "C" then + local lookup = self.global_handler_fn and self.global_handler_fn(n) + if not lookup then + error("variable '"..n.."' is not declared", 2) + else + return lookup + end + end + return rawget(t, n) + end + +end + +interpreter = Interpreter() + +function oprint(...) + interpreter:oprint(...) +end +print = oprint + +console = ConsoleHud(interpreter) +Engine:registerConsoleObject(console) diff --git a/scripts/manifest.txt b/scripts/manifest.txt index 30fbb0541f9..3475e3108af 100644 --- a/scripts/manifest.txt +++ b/scripts/manifest.txt @@ -7,3 +7,6 @@ main_menu microbe_stage microbe_editor //sandbox + +console.lua + diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 6e1c019d4fd..36322fc927c 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -41,6 +41,9 @@ // Microbe #include "microbe_stage/compound.h" +// Console +#include "gui/CEGUIWindow.h" + #include "util/contains.h" #include "util/pair_hash.h" @@ -107,6 +110,7 @@ struct Engine::Implementation : public Ogre::WindowEventListener { m_currentGameState = gameState; if (gameState) { gameState->activate(); + m_currentGameState->rootGUIWindow().addChild(*m_consoleGUIWindow); } } @@ -344,6 +348,8 @@ struct Engine::Implementation : public Ogre::WindowEventListener { CEGUI::ImageManager::getSingleton().loadImageset("DriveIcons.imageset"); CEGUI::ImageManager::getSingleton().loadImageset("GameMenu.imageset"); CEGUI::ImageManager::getSingleton().loadImageset("HUDDemo.imageset"); + + m_consoleGUIWindow = new CEGUIWindow("Console"); } void @@ -423,6 +429,8 @@ struct Engine::Implementation : public Ogre::WindowEventListener { GameState* m_currentGameState = nullptr; + CEGUIWindow* m_consoleGUIWindow = nullptr; + ComponentFactory m_componentFactory; Engine& m_engine; @@ -469,6 +477,8 @@ struct Engine::Implementation : public Ogre::WindowEventListener { std::string saveFile; } m_serialization; + + luabind::object m_console; }; @@ -523,6 +533,7 @@ Engine::luaBindings() { .def("timedSystemShutdown", &Engine::timedSystemShutdown) .def("isSystemTimedShutdown", &Engine::isSystemTimedShutdown) .def("thriveVersion", &Engine::thriveVersion) + .def("registerConsoleObject", &Engine::registerConsoleObject) .property("componentFactory", &Engine::componentFactory) .property("keyboard", &Engine::keyboard) .property("mouse", &Engine::mouse) @@ -615,7 +626,6 @@ Engine::init() { // Ogre::SceneManager has been instantiated m_impl->setupSoundManager(); m_impl->m_currentGameState = previousGameState; - } @@ -819,7 +829,6 @@ Engine::transferEntityGameState( return newEntity; } - void Engine::update( int milliseconds @@ -841,6 +850,9 @@ Engine::update( } assert(m_impl->m_currentGameState != nullptr); m_impl->m_currentGameState->update(milliseconds); + + luabind::call_member(m_impl->m_console, "update"); + // Update any timed shutdown systems auto itr = m_impl->m_prevShutdownSystems->begin(); while (itr != m_impl->m_prevShutdownSystems->end()) { @@ -880,3 +892,7 @@ Engine::thriveVersion() const { return m_impl->m_thriveVersion; } +void +Engine::registerConsoleObject(luabind::object consoleObject) { + m_impl->m_console = consoleObject; +} diff --git a/src/engine/engine.h b/src/engine/engine.h index b1571e84619..a895e1a0a35 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -7,6 +7,8 @@ #include #include +#include + class btDiscreteDynamicsWorld; class lua_State; @@ -74,6 +76,7 @@ class Engine { * - Engine::keyboard() (as property) * - Engine::mouse() (as property) * - Engine::thriveVersion() + * - Engine::registerConsoleObject() * * @return */ @@ -406,6 +409,12 @@ class Engine { Ogre::RenderWindow* renderWindow() const; + /** + * @brief Registers the console object + */ + void + registerConsoleObject(luabind::object consoleObject); + /** * @brief Gets the current version of thrive as a string. diff --git a/src/scripting/script_initializer.cpp b/src/scripting/script_initializer.cpp index e8d240f97d8..68c6f8d11c4 100644 --- a/src/scripting/script_initializer.cpp +++ b/src/scripting/script_initializer.cpp @@ -17,12 +17,6 @@ #include #include -static void -debug( - const std::string& msg -) { - std::cout << msg << std::endl; -} static int constructTraceback( @@ -58,7 +52,6 @@ thrive::initializeLua( luabind::open(L); luabind::bind_class_info(L); luabind::module(L) [ - luabind::def("debug", debug), EngineBindings::luaBindings(), GeneralBindings::luaBindings(), OgreBindings::luaBindings(),