LuaConsole         interactive access to Lua

General

Some of this code is loosely based on the previous article Creating a Scalable Console, but not a simple cut and paste. Other ideas are taken from the stand alone Lua interpreter. There is quite a bit of code here, but taken as small parts it should be quite digestible, and also you may find the various parts useful on their own.

So what is it?


A drop down console, that gives you interactive access to Lua, with scrolling, multi-line statements (i.e. you don't have to write Lua functions all on one line) and a command line history. Also it displays any Ogre log output. It should be quite easy to drop into most Ogre projects, that use OIS buffered input. If you are not using OIS you will have more work to do. It shouldn't be too hard to replace the LuaInterpreter class with for example, an AngelScript one, or GameMonkey, or bash. Ok, bash might be a little difficult...

What it isn't?


A binding of Lua to any game features. That's up to you.


Info Note:
This is a "first draft" and pretty much just a code dump. Once things that people need clarifying are found, we can add them, and eventually take this line out.

help For questions and feedback, use this forum topic

The Lua Interpreter Class

LuaInterpreter.h

#ifndef LUAINTERPRETER_H
#define LUAINTERPRETER_H

#include <lua.hpp>
#include <string>

#define LI_PROMPT  ">"
#define LI_PROMPT2 ">>"
#define LI_MESSAGE "Nigels wizzbang Lua Interpreting Class. Version 0.1. 2009\n"

class LuaInterpreter
{
    public:
        enum State
        {
            LI_READY = 0,
            LI_NEED_MORE_INPUT,
            LI_ERROR
        };

        LuaInterpreter(lua_State *L);
        virtual ~LuaInterpreter();

        // Retrieves the current output from the interpreter.
        std::string getOutput();
        void clearOutput() { mOutput.clear(); }

        std::string getPrompt() { return mPrompt; }

        // Insert (another) line of text into the interpreter.
        // If fInsertInOutput is true, the line will also go into the
        // output.
        State insertLine( std::string& line, bool fInsertInOutput = false );

        // Callback for lua to provide output.
        static int insertOutputFromLua( lua_State *L );

        // Retrieve the current state of affairs.
        State getState() { return mState; }

    protected:
        lua_State *mL;
        std::string mCurrentStatement;
        std::string mOutput;
        std::string mPrompt;
        State mState;
        bool mFirstLine;

    private:
};

#endif // LUAINTERPRETER_H

LuaInterpreter.cpp

#include "LuaInterpreter.h"

using std::string;

// The address of this int in memory is used as a garenteed unique id
// in the lua registry
static const char LuaRegistryGUID = 0;

LuaInterpreter::LuaInterpreter(lua_State *L) : mL(L), mState(LI_READY), mFirstLine(true)
{
    mOutput.clear();
    mCurrentStatement.clear();
    lua_pushlightuserdata( mL, (void *)&LuaRegistryGUID );
    lua_pushlightuserdata( mL, this );
    lua_settable( mL, LUA_REGISTRYINDEX );

    lua_register( mL, "interpreterOutput", &LuaInterpreter::insertOutputFromLua );

#ifdef LI_MESSAGE
    mOutput = LI_MESSAGE;
#endif
    mPrompt = LI_PROMPT;
}

LuaInterpreter::~LuaInterpreter()
{
    lua_register( mL, "interpreterOutput", NULL );
    lua_pushlightuserdata( mL, (void *)&LuaRegistryGUID );
    lua_pushnil( mL );
    lua_settable( mL, LUA_REGISTRYINDEX );
}

// Retrieves the current output from the interpreter.
string LuaInterpreter::getOutput()
{
    return mOutput;
}

// Insert (another) line of text into the interpreter.
LuaInterpreter::State LuaInterpreter::insertLine( string& line, bool fInsertInOutput )
{
    if( fInsertInOutput == true )
    {
        mOutput += line;
        mOutput += '\n';
    }

    if( mFirstLine && line.substr(0,1) == "=" )
    {
        line = "return " + line.substr(1, line.length()-1 );
    }

    mCurrentStatement += " ";
    mCurrentStatement += line;
    mFirstLine = false;

    mState = LI_READY;

    if( luaL_loadstring( mL, mCurrentStatement.c_str() ) )
    {
        string error( lua_tostring( mL, -1 ) );
        lua_pop( mL, 1 );

        // If the error is not a syntax error cuased by not enough of the
        // statement been yet entered...
        if( error.substr( error.length()-6, 5 ) != "<eof>" )
        {
            mOutput += error;
            mOutput += "\n";
            mOutput += LI_PROMPT;
            mCurrentStatement.clear();
            mState = LI_ERROR;
        }
        // Otherwise...
        else
        {
            // Secondary prompt
            mPrompt = LI_PROMPT2;

            mState = LI_NEED_MORE_INPUT;
        }
        return mState;
    }
    else
    {
        // The statment compiled correctly, now run it.

        if( lua_pcall( mL, 0, LUA_MULTRET, 0 ) )
        {
            // The error message (if any) will be added to the output as part
            // of the stack reporting.
            lua_gc( mL, LUA_GCCOLLECT, 0 );     // Do a full garbage collection on errors.
            mState = LI_ERROR;
        }
    }

    mCurrentStatement.clear();
    mFirstLine = true;

    // Report stack contents
    if ( lua_gettop(mL) > 0)
    {
      lua_getglobal(mL, "print");
      lua_insert(mL, 1);
      lua_pcall(mL, lua_gettop(mL)-1, 0, 0);
    }


    mPrompt = LI_PROMPT;

    // Clear stack
    lua_settop( mL, 0 );

    return mState;
}

// Callback for lua to provide output.
int LuaInterpreter::insertOutputFromLua( lua_State *L )
{
    // Retreive the current interpreter for current lua state.
    LuaInterpreter *interpreter;

    lua_pushlightuserdata( L, (void *)&LuaRegistryGUID );
    lua_gettable( L, LUA_REGISTRYINDEX );
    interpreter = static_cast<LuaInterpreter *>(lua_touserdata( L, -1 ));

    if( interpreter )
        interpreter->mOutput += lua_tostring( L, -2 );

    lua_settop( L, 0 );
    return 0;
}


Now you can try this out in a simple non-gui program. See the case block for the RETURN key in the console class below, and add some cout's and a cin with a loop. That's how it was tested before writing the rest of the code.

The line editor Class

Rather than repeat the code here, see the Simple keyboard string editing article for the string editing class used.

The Console Class

Some of this code you may recognise from Creating a Scalable Console

LuaConsole.h

#ifndef LUACONSOLE_H
#define LUACONSOLE_H

//#include <Ogre.h>
#include <OgreRoot.h>
#include <OgreFrameListener.h>
#include <OgreOverlayContainer.h>
#include <OgreOverlayElement.h>
#include <OgreOverlayManager.h>
#include <OIS.h>
#include <list>
#include <string>
#include "EditString.h"
#include "LuaInterpreter.h"

class LuaConsole: public Ogre::Singleton<LuaConsole>, Ogre::FrameListener, Ogre::LogListener
{
public:
    LuaConsole();
    virtual ~LuaConsole();

    void    init(lua_State *L );
    void    shutdown();
    void    setVisible(bool fVisible);
    bool    isVisible(){ return visible; }
    void    print( std::string text );
    bool    injectKeyPress( const OIS::KeyEvent &evt );

    // Frame listener
    bool    frameStarted(const Ogre::FrameEvent &evt);
    bool    frameEnded(const Ogre::FrameEvent &evt);

    // Log Listener
    void    messageLogged( const Ogre::String& message, Ogre::LogMessageLevel lml, bool maskDebug, const Ogre::String &logName );

protected:
    bool                    visible;
    bool                    textChanged;
    float                   height;
    int                     startLine;
    bool                    cursorBlink;
    float                   cursorBlinkTime;
    bool                    fInitialised;

    Ogre::Overlay           *overlay;
    Ogre::OverlayContainer  *panel;
    Ogre::OverlayElement    *textbox;

    EditString              editLine;
    LuaInterpreter          *interpreter;

    std::list<std::string>  lines;
    std::list<std::string>  history;

    std::list<std::string>::iterator    historyLine;

    void    addToHistory( const std::string& cmd );
};

#endif // LUACONSOLE_H

LuaConsole.cpp

#include "LuaConsole.h"

#define CONSOLE_LINE_LENGTH 85
#define CONSOLE_LINE_COUNT 15
#define CONSOLE_MAX_LINES 32000
#define CONSOLE_MAX_HISTORY 64
#define CONSOLE_TAB_STOP 8

using namespace Ogre;
using namespace std;

template<> LuaConsole *Singleton<LuaConsole>::ms_Singleton=0;

LuaConsole::LuaConsole() : fInitialised(false)
{
}

LuaConsole::~LuaConsole()
{
    if( fInitialised )
        shutdown();
}

void LuaConsole::init(lua_State *L)
{
    if( fInitialised )
        shutdown();

    OverlayManager &overlayManager = OverlayManager::getSingleton();

    Root *root = Root::getSingletonPtr();

    scene=root->getSceneManagerIterator().getNext();
    root->addFrameListener(this);

    height = 1;
    startLine = 0;
    cursorBlinkTime = 0;
    cursorBlink = false;
    visible = false;

    interpreter = new LuaInterpreter( L );
    print( interpreter->getOutput() );
    interpreter->clearOutput();

    textbox = overlayManager.createOverlayElement("TextArea","ConsoleText");
    textbox->setMetricsMode(GMM_RELATIVE);
    textbox->setPosition(0,0);
    textbox->setParameter("font_name","Console");
    textbox->setParameter("colour_top","0 0 0");
    textbox->setParameter("colour_bottom","0 0 0");
    textbox->setParameter("char_height","0.03");

    panel = static_cast<OverlayContainer*>(overlayManager.createOverlayElement("Panel", "ConsolePanel"));
    panel->setMetricsMode(Ogre::GMM_RELATIVE);
    panel->setPosition(0, 0);
    panel->setDimensions(1, 0);
    panel->setMaterialName("console/background");

    panel->addChild(textbox);

    overlay = overlayManager.create("Console");
    overlay->add2D(panel);
    overlay->show();
    LogManager::getSingleton().getDefaultLog()->addListener(this);

    fInitialised = true;
}

void LuaConsole::shutdown()
{
    if( fInitialised )
    {
        delete interpreter;

        OverlayManager::getSingleton().destroyOverlayElement( textbox );
        OverlayManager::getSingleton().destroyOverlayElement( panel );
        OverlayManager::getSingleton().destroy( overlay );

        Root::getSingleton().removeFrameListener( this );
        LogManager::getSingleton().getDefaultLog()->removeListener(this);
    }
    fInitialised = false;
}

void LuaConsole::setVisible(bool fVisible)
{
    visible = fVisible;
}

void LuaConsole::messageLogged( const Ogre::String& message, Ogre::LogMessageLevel lml, bool maskDebug, const Ogre::String &logName )
{
    print( message );
}

bool LuaConsole::frameStarted(const Ogre::FrameEvent &evt)
{
    if(visible)
    {
        cursorBlinkTime += evt.timeSinceLastFrame;

        if( cursorBlinkTime > 0.5f )
        {
            cursorBlinkTime -= 0.5f;
            cursorBlink = ! cursorBlink;
            textChanged = true;
        }
    }
 
    if(visible && height < 1)
    {
        height += evt.timeSinceLastFrame * 10;
        textbox->show();

        if(height >= 1)
        {
            height = 1;
        }
    }
    else if( !visible && height > 0)
    {
        height -= evt.timeSinceLastFrame * 10;
        if(height <= 0)
        {
            height = 0;
            textbox->hide();
        }
    }

    textbox->setPosition(0, (height - 1) * 0.5);
    panel->setDimensions( 1, height * 0.5 );

    if(textChanged)
    {
        String text;
        list<string>::iterator i,start,end;

        //make sure is in range
        //NOTE: the code elsewhere relies on startLine's signedness.
        //I.e. the ability to go below zero and not wrap around to a high number.
        if(startLine < 0 )
            startLine = 0;
        if((unsigned)startLine > lines.size())
            startLine = lines.size();

        start=lines.begin();

        for(int c = 0; c < startLine; c++)
            start++;

        end = start;

        for(int c = 0; c < CONSOLE_LINE_COUNT; c++)
        {
            if(end == lines.end())
                break;
            end++;
        }

        for(i = start; i != end; i++)
            text += (*i) + "\n";

        //add the edit line with cursor
        string editLineText( editLine.getText() + " " );
        if( cursorBlink )
            editLineText[editLine.getPosition()] = '_';

        text += interpreter->getPrompt() + editLineText;

        textbox->setCaption(text);

        textChanged = false;
    }

    return true;
}

bool LuaConsole::frameEnded(const Ogre::FrameEvent &evt)
{
    return true;
}

void LuaConsole::print( std::string text )
{
    string line;
    string::iterator pos;
    int column;

    pos = text.begin();
    column = 1;

    while( pos != text.end() )
    {
        if( *pos == '\n' || column > CONSOLE_LINE_LENGTH )
        {
            lines.push_back( line );
            line.clear();
            if( *pos != '\n' )
              --pos;  // We want to keep this character for the next line.
  
            column = 0;
        }
        else if (*pos =='\t')
        {
           // Push at least 1 space
           line.push_back (' ');
           column++;
  
           // fill until next multiple of CONSOLE_TAB_STOP
           while ((column % CONSOLE_TAB_STOP )!=0)
           {
              line.push_back (' ');
              column++;
           }
        }
        else
        {
            line.push_back( *pos );
            column++;
        }
        
        pos++;
    }
    if( line.length() )
    {
        if( lines.size() > CONSOLE_MAX_LINES-1 )
            lines.pop_front();

        lines.push_back( line );
    }

    // Make sure last text printed is in view.
    if( lines.size() > CONSOLE_LINE_COUNT )
        startLine = lines.size() - CONSOLE_LINE_COUNT;

    textChanged = true;

    return;
}

void LuaConsole::addToHistory( const string& cmd )
{
    history.remove( cmd );
    history.push_back( cmd );
    if( history.size() > CONSOLE_MAX_HISTORY )
        history.pop_front();
    historyLine = history.end();
}

bool LuaConsole::injectKeyPress( const OIS::KeyEvent &evt )
{
    switch( evt.key )
    {
        case OIS::KC_RETURN:
            print( interpreter->getPrompt() + editLine.getText() );
            interpreter->insertLine( editLine.getText() );
            addToHistory( editLine.getText() );
            print( interpreter->getOutput() );
            interpreter->clearOutput();
            editLine.clear();
            break;

        case OIS::KC_PGUP:
            startLine -= CONSOLE_LINE_COUNT;
            textChanged = true;
            break;

        case OIS::KC_PGDOWN:
            startLine += CONSOLE_LINE_COUNT;
            textChanged = true;
            break;

        case OIS::KC_UP:
            if( !history.empty() )
            {
              if( historyLine == history.begin() )
                  historyLine = history.end();
              historyLine--;
              editLine.setText( *historyLine );
              textChanged = true;
            }
            break;

        case OIS::KC_DOWN:
            if( !history.empty() )
            {
              if( historyLine != history.end() )
                  historyLine++;
              if( historyLine == history.end() )
                  historyLine = history.begin();
              editLine.setText( *historyLine );
              textChanged = true;
            }
            break;

        default:
            textChanged = editLine.injectKeyPress( evt );
            break;
    }

    return true;
}


There are almost no comments in that code. Perhaps it needs more, lets see how we go.

Support files

Here are various bits and pieces that you need, as well as a test program.

consolePrint.lua

function myprint(...)
	local str
	str = ''

	for i = 1, arg.n do
		if str ~= '' then str = str .. '\t' end
		str = str .. tostring( arg[i] )
	end

	str = str .. '\n'

	interpreterOutput( str )
end

oldprint = print
print = myprint

This redirects the output of any scripts that use print to the console output. You can use oldprint to still print to stdout if you need to. Remember interpreterOutput is the static method the interpreter class registers, and note it does not add a newline to the end of the string you provide. While this works, I found better results with binding the 'print' method of the console class to Lua and calling that. This way, any intermediate output from a script is displayed almost immediately, rather than once the script is finished.

Material Script - console.material

No rocket science here.

material console/background
{
	technique
	{
		pass
		{
			scene_blend modulate
			texture_unit
                        {
                                texture console_texture.png
                        }
		}

	}

}

Font definition - console.fontdef

Again, nothing special.

Console
{
	type 		truetype
	source 		console.ttf
	size 		32
	resolution 	100
}

test.cpp


This started with the boiler plate code that Code::Blocks gives you in its Ogre wizard. The main points are starting up and shutting down Lua, and the frame listener.

The frame listener tells the Example framework, to start OIS in buffered keyboard mode, and is itself a OIS::KeyListener. On each keypress, it either does its own processing or, if the console is visible, sends the key event to the console class instance. The one exception is the '~' key, which is allways processed by the listener, and toggles the console.

#include <Ogre.h>
#include <ExampleApplication.h>
#include "LuaConsole.h"

class MyFrameListener : public ExampleFrameListener, OIS::KeyListener
{
    bool mContinue;

public:
    MyFrameListener( RenderWindow *window, Camera *camera ) : ExampleFrameListener( window, camera, true ), mContinue(true)
    {
        mKeyboard->setEventCallback(this);
    }
    ~MyFrameListener()
    {
        mKeyboard->setEventCallback(0);
    }
    bool frameStarted(const FrameEvent &evt)
    {
        return mContinue;
    }
    bool keyPressed( const OIS::KeyEvent &arg )
    {
        if( arg.key == OIS::KC_GRAVE )
        {
            LuaConsole::getSingleton().setVisible( ! LuaConsole::getSingleton().isVisible() );
            return true;
        }

        if( LuaConsole::getSingleton().isVisible() )
        {
            LuaConsole::getSingleton().injectKeyPress( arg );
        }
        else
        {
            // Normal non-console key handling.
            switch(arg.key)
            {
                case OIS::KC_ESCAPE:
                case OIS::KC_Q:
                    mContinue = false;
                    return false;

                default:
                    break;
            }
        }
        return true;
    }
	bool keyReleased( const OIS::KeyEvent &arg )
	{
	    return true;
	}
};

class SampleApp : public ExampleApplication
{
public:
    // Basic constructor
    SampleApp()
    {
        L = lua_open();
        luaL_openlibs( L );
        luaL_dofile( L, "consolePrint.lua" );
        console = new LuaConsole();
    }
    ~SampleApp()
    {
        console->shutdown();
        delete console;
        lua_close( L );
    }

protected:
    lua_State *L;
    LuaConsole *console;

    // Just override the mandatory create scene method
    void createScene(void)
    {
        // Create the SkyBox
        mSceneMgr->setSkyBox(true, "Examples/MorningSkyBox");

        console->init( mRoot, L );
    }

    void createFrameListener(void)
    {
        mFrameListener= new MyFrameListener(mWindow, mCamera);
        mFrameListener->showDebugOverlay(true);
        mRoot->addFrameListener(mFrameListener);
    }
};


// ----------------------------------------------------------------------------
// Main function, just boots the application object
// ----------------------------------------------------------------------------
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include "windows.h"
INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )
#else
int main(int argc, char **argv)
#endif
{
    // Create application object
    SampleApp app;

    try
    {
        app.go();
    }
    catch( Exception& e )
    {
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
        MessageBox( NULL, e.getFullDescription().c_str(), "An exception has occured!", MB_OK | MB_IConerror | MB_TASKMODAL);
#else

        std::cerr << "An exception has occured: " << e.getFullDescription();
#endif
    }

    return 0;
}