diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e9073b7c7..1152425193 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,16 +98,22 @@ set(INPUT_HEADER components/engine/input/poller.hpp) source_group(input FILES ${INPUT} ${INPUT_HEADER}) -set(MISC - components/misc/stringops.cpp - components/misc/fileops.cpp) -set(MISC_HEADER - components/misc/fileops.hpp - components/misc/slice_array.hpp - components/misc/stringops.hpp) +set(COMMANDSERVER + components/commandserver/server.hpp + components/commandserver/server.cpp) +source_group(commandserver FILES ${COMMANDSERVER}) + +set(MISC + components/misc/stringops.cpp + components/misc/fileops.cpp) +set(MISC_HEADER + components/misc/fileops.hpp + components/misc/slice_array.hpp + components/misc/stringops.hpp + components/misc/tsdeque.hpp) source_group(misc FILES ${MISC} ${MISC_HEADER}) -set(COMPONENTS ${BSA} ${NIF} ${NIFOGRE} ${ESM_STORE} ${OGRE} ${INPUT} ${MISC}) +set(COMPONENTS ${BSA} ${NIF} ${NIFOGRE} ${ESM_STORE} ${OGRE} ${INPUT} ${COMMANDSERVER} ${MISC}) set(COMPONENTS_HEADER ${BSA_HEADER} ${NIF_HEADER} ${NIFOGRE_HEADER} ${ESM_STORE_HEADER} ${ESM_HEADER} ${OGRE_HEADER} ${INPUT_HEADER} ${MISC_HEADER}) @@ -137,7 +143,7 @@ include_directories("." ${CMAKE_HOME_DIRECTORY}/extern/caelum/include) link_directories(${Boost_LIBRARY_DIRS} ${OGRE_LIB_DIR}) -ADD_SUBDIRECTORY( extern/caelum ) +add_subdirectory( extern/caelum ) # Specify build paths @@ -189,6 +195,9 @@ if (APPLE) target_link_libraries(openmw ${CARBON_FRAMEWORK}) endif (APPLE) +# Other apps and tools +add_subdirectory( apps/clientconsole ) + # Apple bundling if (APPLE) set_source_files_properties( diff --git a/apps/clientconsole/CMakeLists.txt b/apps/clientconsole/CMakeLists.txt new file mode 100755 index 0000000000..e6885841f1 --- /dev/null +++ b/apps/clientconsole/CMakeLists.txt @@ -0,0 +1,2 @@ +project(clientconsole) +add_executable(clientconsole client.cpp) diff --git a/apps/clientconsole/client.cpp b/apps/clientconsole/client.cpp new file mode 100755 index 0000000000..dac95ce993 --- /dev/null +++ b/apps/clientconsole/client.cpp @@ -0,0 +1,98 @@ +#include +#include +#include + +using boost::asio::ip::tcp; + +#pragma warning( disable : 4966 ) + +class Client +{ +protected: + boost::asio::io_service mIOService; + tcp::socket* mpSocket; + +public: + + bool connect(const char* port) + { + tcp::resolver resolver(mIOService); + tcp::resolver::query query("localhost", port); + tcp::resolver::iterator endpoint_iterator = resolver.resolve(query); + tcp::resolver::iterator end; + + mpSocket = new tcp::socket(mIOService); + boost::system::error_code error = boost::asio::error::host_not_found; + while (error && endpoint_iterator != end) + { + mpSocket->close(); + mpSocket->connect(*endpoint_iterator++, error); + } + + return (error) ? false : true; + } + void disconnect() + { + mpSocket->close(); + mIOService.stop(); + } + + bool send (const char* msg) + { + struct Header + { + char magic[4]; + boost::uint32_t dataLength; + }; + const size_t slen = strlen(msg); + const size_t plen = sizeof(Header) + slen + 1; + + std::vector packet(plen); + Header* pHeader = reinterpret_cast(&packet[0]); + strncpy(pHeader->magic, "OMW0", 4); + pHeader->dataLength = slen + 1; // Include the null terminator + strncpy(&packet[8], msg, pHeader->dataLength); + + boost::system::error_code ec; + boost::asio::write(*mpSocket, boost::asio::buffer(packet), + boost::asio::transfer_all(), ec); + if (ec) + std::cout << "Error: " << ec.message() << std::endl; + + return !ec; + } +}; + + +int main(int argc, char* argv[]) +{ + std::cout << "OpenMW client console" << std::endl; + std::cout << "=====================" << std::endl; + std::cout << "Type 'quit' to exit." << std::endl; + std::cout << "Connecting..."; + + Client client; + if (client.connect("27917")) + { + std::cout << "success." << std::endl; + + bool bDone = false; + do + { + std::cout << "> "; + char buffer[1024]; + gets(buffer); + + if (std::string(buffer) != "quit") + bDone = !client.send(buffer); + else + bDone = true; + } while (!bDone); + + client.disconnect(); + } + else + std::cout << "failed." << std::endl; + + return 0; +} diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index f431e51bdc..601df6354c 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -14,10 +14,25 @@ #include "apps/openmw/mwrender/playerpos.hpp" #include "apps/openmw/mwrender/sky.hpp" +class ProcessCommandsHook : public Ogre::FrameListener +{ +public: + ProcessCommandsHook(OMW::Engine* pEngine) : mpEngine (pEngine) {} + virtual bool frameStarted(const Ogre::FrameEvent& evt) + { + mpEngine->processCommands(); + return true; + } +protected: + OMW::Engine* mpEngine; +}; + + OMW::Engine::Engine() : mEnableSky (false) , mpSkyManager (NULL) { + mspCommandServer.reset(new OMW::CommandServer::Server(&mCommands, kCommandServerPort)); } // Load all BSA files in data directory. @@ -83,6 +98,16 @@ void OMW::Engine::enableSky (bool bEnable) mEnableSky = bEnable; } +void OMW::Engine::processCommands() +{ + std::string msg; + while (mCommands.pop_front(msg)) + { + ///\todo Add actual processing of the received command strings + std::cout << "Command: '" << msg << "'" << std::endl; + } +} + // Initialise and enter main loop. void OMW::Engine::go() @@ -146,11 +171,17 @@ void OMW::Engine::go() // Sets up the input system MWInput::MWInputManager input(mOgre, player); + // Launch the console server + std::cout << "Starting command server on port " << kCommandServerPort << std::endl; + mspCommandServer->start(); + mOgre.getRoot()->addFrameListener( new ProcessCommandsHook(this) ); + std::cout << "\nStart! Press Q/ESC or close window to exit.\n"; // Start the main rendering loop mOgre.start(); + mspCommandServer->stop(); delete mpSkyManager; std::cout << "\nThat's all for now!\n"; diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index ae69f52b45..7874070192 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -6,6 +6,9 @@ #include #include "apps/openmw/mwrender/mwscene.hpp" +#include "components/misc/tsdeque.hpp" +#include "components/commandserver/server.hpp" + namespace MWRender { @@ -18,6 +21,8 @@ namespace OMW class Engine { + enum { kCommandServerPort = 27917 }; + boost::filesystem::path mDataDir; Render::OgreRenderer mOgre; std::string mCellName; @@ -26,6 +31,9 @@ namespace OMW bool mEnableSky; MWRender::SkyManager* mpSkyManager; + TsDeque mCommands; + std::auto_ptr mspCommandServer; + // not implemented Engine (const Engine&); Engine& operator= (const Engine&); @@ -55,6 +63,9 @@ namespace OMW /// Enables rendering of the sky (off by default). void enableSky (bool bEnable); + /// Process pending commands + void processCommands(); + /// Initialise and enter main loop. void go(); }; diff --git a/components/commandserver/server.cpp b/components/commandserver/server.cpp new file mode 100755 index 0000000000..94ce03f97a --- /dev/null +++ b/components/commandserver/server.cpp @@ -0,0 +1,177 @@ + +#include "server.hpp" + +using boost::asio::ip::tcp; + +// +// Namespace for containing implementation details that the +// rest of OpenMW doesn't need to worry about +// +namespace OMW { namespace CommandServer { namespace Detail { + + /// + /// Tracks an active connection to the CommandServer + /// + class Connection + { + public: + Connection (boost::asio::io_service& io_service, Server* pServer); + + void start(); + void stop(); + tcp::socket& socket(); + + protected: + void handle (); + + tcp::socket mSocket; + Server* mpServer; + boost::thread* mpThread; + }; + + Connection::Connection (boost::asio::io_service& io_service, Server* pServer) + : mSocket (io_service) + , mpServer (pServer) + { + } + + void Connection::start() + { + mpThread = new boost::thread(boost::bind(&Connection::handle, this)); + } + + /// + /// Stops and disconnects the connection + /// + void Connection::stop() + { + mSocket.close(); + mpThread->join(); + } + + tcp::socket& Connection::socket() + { + return mSocket; + } + + void Connection::handle () + { + bool bDone = false; + while (!bDone) + { + struct Header + { + char magic[4]; + size_t dataLength; + } header; + + // Read the header + boost::system::error_code error; + mSocket.read_some(boost::asio::buffer(&header, sizeof(Header)), error); + + if (error != boost::asio::error::eof) + { + if (strncmp(header.magic, "OMW0", 4) == 0) + { + std::vector msg; + msg.resize(header.dataLength); + + boost::system::error_code error; + mSocket.read_some(boost::asio::buffer(&msg[0], header.dataLength), error); + if (!error) + mpServer->postMessage( &msg[0] ); + else + bDone = true; + } + else + throw std::exception("Unexpected header!"); + } + else + bDone = true; + } + mpServer->removeConnection(this); + } + +}}} + +namespace OMW { namespace CommandServer { + + using namespace Detail; + + Server::Server (Deque* pDeque, const int port) + : mAcceptor (mIOService, tcp::endpoint(tcp::v4(), port)) + , mpCommands (pDeque) + , mbStopping (false) + { + } + + + void Server::start() + { + mIOService.run(); + mpThread = new boost::thread(boost::bind(&Server::threadMain, this)); + } + + void Server::stop() + { + // (1) Stop accepting new connections + // (2) Wait for the listener thread to finish + mAcceptor.close(); + mpThread->join(); + + // Now that no new connections are possible, close any existing + // open connections + { + boost::mutex::scoped_lock lock(mConnectionsMutex); + mbStopping = true; + for (ConnectionSet::iterator it = mConnections.begin(); + it != mConnections.end(); + ++it) + { + (*it)->stop(); + } + } + } + + void Server::removeConnection (Connection* ptr) + { + // If the server is shutting down (rather the client closing the + // connection), don't remove the connection from the list: that + // would corrupt the iterator the server is using to shutdown all + // clients. + if (!mbStopping) + { + boost::mutex::scoped_lock lock(mConnectionsMutex); + std::set::iterator it = mConnections.find(ptr); + if (it != mConnections.end()) + mConnections.erase(it); + } + delete ptr; + } + + void Server::postMessage (const char* s) + { + mpCommands->push_back(s); + } + + void Server::threadMain() + { + // Loop until accept() fails, which will cause the break statement to be hit + while (true) + { + std::auto_ptr spConnection(new Connection(mAcceptor.io_service(), this)); + boost::system::error_code ec; + mAcceptor.accept(spConnection->socket(), ec); + if (!ec) + { + boost::mutex::scoped_lock lock(mConnectionsMutex); + mConnections.insert(spConnection.get()); + spConnection->start(); + spConnection.release(); + } + else + break; + } + } + +}} diff --git a/components/commandserver/server.hpp b/components/commandserver/server.hpp new file mode 100755 index 0000000000..8a2a74d1d2 --- /dev/null +++ b/components/commandserver/server.hpp @@ -0,0 +1,62 @@ +#ifndef CONSOLESERVER_H +#define CONSOLESERVER_H + +#include +#include +#include +#include +#include +#include + +#include "components/misc/tsdeque.hpp" + +namespace OMW { namespace CommandServer +{ + // + // Forward Declarations + // + namespace Detail + { + class Connection; + } + + // + // Server that opens a port to listen for string commands which will be + // put into the deque provided in the Server constructor. + // + class Server + { + public: + typedef TsDeque Deque; + + Server (Deque* pDeque, const int port); + + void start(); + void stop(); + + protected: + friend class Detail::Connection; + typedef std::set ConnectionSet; + + void removeConnection (Detail::Connection* ptr); + void postMessage (const char* s); + + void threadMain(); + + // Objects used to set up the listening server + boost::asio::io_service mIOService; + boost::asio::ip::tcp::acceptor mAcceptor; + boost::thread* mpThread; + bool mbStopping; + + // Track active connections + ConnectionSet mConnections; + mutable boost::mutex mConnectionsMutex; + + // Pointer to output queue in which to put received strings + Deque* mpCommands; + }; + +}} + +#endif // CONSOLESERVER_H diff --git a/components/misc/tsdeque.hpp b/components/misc/tsdeque.hpp new file mode 100755 index 0000000000..db14428bcb --- /dev/null +++ b/components/misc/tsdeque.hpp @@ -0,0 +1,34 @@ +#ifndef TSDEQUE_H +#define TSDEQUE_H + +#include + +template +class TsDeque +{ +public: + void push_back (const T& t) + { + boost::mutex::scoped_lock lock(mMutex); + mDeque.push_back(t); + } + + bool pop_front (T& t) + { + boost::mutex::scoped_lock lock(mMutex); + if (!mDeque.empty()) + { + t = mDeque.front(); + mDeque.pop_front(); + return true; + } + else + return false; + } + +protected: + std::deque mDeque; + mutable boost::mutex mMutex; +}; + +#endif // TSDEQUE_H