Add server side ncurses terminal
authorest31 <MTest31@outlook.com>
Sat, 25 Jul 2015 05:43:32 +0000 (07:43 +0200)
committerest31 <MTest31@outlook.com>
Fri, 6 Nov 2015 07:51:14 +0000 (08:51 +0100)
This adds a chat console the server owner can use for administration
or to talk with players.
It runs in its own thread, which makes the user interface immune to
the server's lag, behaving just like a client, except timeout.
As it uses the same console code as the f10 console, things like nick
completion or a scroll buffer basically come for free.
The terminal itself is written in a general way so that adding a
client version later on is just about implementing an interface.

Fatal errors are printed after the console exists and the ncurses
terminal buffer gets cleaned up with endwin(), so that the error still
remains visible.

The server owner can chose their username their entered text will
have in chat and where players can send PMs to.
Once the username is secured with a password to prevent anybody to
take over the server, the owner can execute admin tasks over the
console.

This change includes a contribution by @kahrl who has improved ncurses
library detection.

22 files changed:
builtin/init.lua
cmake/Modules/FindNcursesw.cmake [new file with mode: 0644]
doc/minetest.6
src/CMakeLists.txt
src/chat.h
src/chat_interface.h [new file with mode: 0644]
src/cmake_config.h.in
src/debug.cpp
src/log.cpp
src/log.h
src/main.cpp
src/network/serverpackethandler.cpp
src/player.h
src/script/lua_api/l_server.cpp
src/script/lua_api/l_server.h
src/script/lua_api/l_util.h
src/server.cpp
src/server.h
src/terminal_chat_console.cpp [new file with mode: 0644]
src/terminal_chat_console.h [new file with mode: 0644]
src/unittest/test_utilities.cpp
src/util/string.h

index 02fb9db93674a155fb22602b264869d4065bf648..b3004468e5efd5ce853d6f942181ba5ccd9d516c 100644 (file)
@@ -7,6 +7,15 @@
 
 -- Initialize some very basic things
 function core.debug(...) core.log(table.concat({...}, "\t")) end
+if core.print then
+       local core_print = core.print
+       -- Override native print and use
+       -- terminal if that's turned on
+       function print(...)
+               core_print(table.concat({...}, "\t"))
+       end
+       core.print = nil -- don't pollute our namespace
+end
 math.randomseed(os.time())
 os.setlocale("C", "numeric")
 minetest = core
diff --git a/cmake/Modules/FindNcursesw.cmake b/cmake/Modules/FindNcursesw.cmake
new file mode 100644 (file)
index 0000000..dcb7cdd
--- /dev/null
@@ -0,0 +1,189 @@
+#.rst:
+# FindNcursesw
+# ------------
+#
+# Find the ncursesw (wide ncurses) include file and library.
+#
+# Based on FindCurses.cmake which comes with CMake.
+#
+# Checks for ncursesw first. If not found, it then executes the
+# regular old FindCurses.cmake to look for for ncurses (or curses).
+#
+#
+# Result Variables
+# ^^^^^^^^^^^^^^^^
+#
+# This module defines the following variables:
+#
+# ``CURSES_FOUND``
+#   True if curses is found.
+# ``NCURSESW_FOUND``
+#   True if ncursesw is found.
+# ``CURSES_INCLUDE_DIRS``
+#   The include directories needed to use Curses.
+# ``CURSES_LIBRARIES``
+#   The libraries needed to use Curses.
+# ``CURSES_HAVE_CURSES_H``
+#   True if curses.h is available.
+# ``CURSES_HAVE_NCURSES_H``
+#   True if ncurses.h is available.
+# ``CURSES_HAVE_NCURSES_NCURSES_H``
+#   True if ``ncurses/ncurses.h`` is available.
+# ``CURSES_HAVE_NCURSES_CURSES_H``
+#   True if ``ncurses/curses.h`` is available.
+# ``CURSES_HAVE_NCURSESW_NCURSES_H``
+#   True if ``ncursesw/ncurses.h`` is available.
+# ``CURSES_HAVE_NCURSESW_CURSES_H``
+#   True if ``ncursesw/curses.h`` is available.
+#
+# Set ``CURSES_NEED_NCURSES`` to ``TRUE`` before the
+# ``find_package(Ncursesw)`` call if NCurses functionality is required.
+#
+#=============================================================================
+# Copyright 2001-2014 Kitware, Inc.
+# modifications: Copyright 2015 kahrl <kahrl@gmx.net>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+#   notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+#   notice, this list of conditions and the following disclaimer in the
+#   documentation and/or other materials provided with the distribution.
+#
+# * Neither the names of Kitware, Inc., the Insight Software Consortium,
+#   nor the names of their contributors may be used to endorse or promote
+#   products derived from this software without specific prior written
+#   permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# ------------------------------------------------------------------------------
+#
+# The above copyright and license notice applies to distributions of
+# CMake in source and binary form.  Some source files contain additional
+# notices of original copyright by their contributors; see each source
+# for details.  Third-party software packages supplied with CMake under
+# compatible licenses provide their own copyright notices documented in
+# corresponding subdirectories.
+#
+# ------------------------------------------------------------------------------
+#
+# CMake was initially developed by Kitware with the following sponsorship:
+#
+#  * National Library of Medicine at the National Institutes of Health
+#    as part of the Insight Segmentation and Registration Toolkit (ITK).
+#
+#  * US National Labs (Los Alamos, Livermore, Sandia) ASC Parallel
+#    Visualization Initiative.
+#
+#  * National Alliance for Medical Image Computing (NAMIC) is funded by the
+#    National Institutes of Health through the NIH Roadmap for Medical Research,
+#    Grant U54 EB005149.
+#
+#  * Kitware, Inc.
+#=============================================================================
+
+include(CheckLibraryExists)
+
+find_library(CURSES_NCURSESW_LIBRARY NAMES ncursesw
+  DOC "Path to libncursesw.so or .lib or .a")
+
+set(CURSES_USE_NCURSES FALSE)
+set(CURSES_USE_NCURSESW FALSE)
+
+if(CURSES_NCURSESW_LIBRARY)
+  set(CURSES_USE_NCURSES TRUE)
+  set(CURSES_USE_NCURSESW TRUE)
+endif()
+
+if(CURSES_USE_NCURSESW)
+  get_filename_component(_cursesLibDir "${CURSES_NCURSESW_LIBRARY}" PATH)
+  get_filename_component(_cursesParentDir "${_cursesLibDir}" PATH)
+
+  find_path(CURSES_INCLUDE_PATH
+    NAMES ncursesw/ncurses.h ncursesw/curses.h
+    HINTS "${_cursesParentDir}/include"
+    )
+
+  # Previous versions of FindCurses provided these values.
+  if(NOT DEFINED CURSES_LIBRARY)
+    set(CURSES_LIBRARY "${CURSES_NCURSESW_LIBRARY}")
+  endif()
+
+  CHECK_LIBRARY_EXISTS("${CURSES_NCURSESW_LIBRARY}"
+    cbreak "" CURSES_NCURSESW_HAS_CBREAK)
+  if(NOT CURSES_NCURSESW_HAS_CBREAK)
+    find_library(CURSES_EXTRA_LIBRARY tinfo HINTS "${_cursesLibDir}"
+      DOC "Path to libtinfo.so or .lib or .a")
+    find_library(CURSES_EXTRA_LIBRARY tinfo )
+  endif()
+
+  # Report whether each possible header name exists in the include directory.
+  if(NOT DEFINED CURSES_HAVE_NCURSESW_NCURSES_H)
+    if(EXISTS "${CURSES_INCLUDE_PATH}/ncursesw/ncurses.h")
+      set(CURSES_HAVE_NCURSESW_NCURSES_H "${CURSES_INCLUDE_PATH}/ncursesw/ncurses.h")
+    else()
+      set(CURSES_HAVE_NCURSESW_NCURSES_H "CURSES_HAVE_NCURSESW_NCURSES_H-NOTFOUND")
+    endif()
+  endif()
+  if(NOT DEFINED CURSES_HAVE_NCURSESW_CURSES_H)
+    if(EXISTS "${CURSES_INCLUDE_PATH}/ncursesw/curses.h")
+      set(CURSES_HAVE_NCURSESW_CURSES_H "${CURSES_INCLUDE_PATH}/ncursesw/curses.h")
+    else()
+      set(CURSES_HAVE_NCURSESW_CURSES_H "CURSES_HAVE_NCURSESW_CURSES_H-NOTFOUND")
+    endif()
+  endif()
+
+  find_library(CURSES_FORM_LIBRARY form HINTS "${_cursesLibDir}"
+    DOC "Path to libform.so or .lib or .a")
+  find_library(CURSES_FORM_LIBRARY form )
+
+  # Need to provide the *_LIBRARIES
+  set(CURSES_LIBRARIES ${CURSES_LIBRARY})
+
+  if(CURSES_EXTRA_LIBRARY)
+    set(CURSES_LIBRARIES ${CURSES_LIBRARIES} ${CURSES_EXTRA_LIBRARY})
+  endif()
+
+  if(CURSES_FORM_LIBRARY)
+    set(CURSES_LIBRARIES ${CURSES_LIBRARIES} ${CURSES_FORM_LIBRARY})
+  endif()
+
+  # Provide the *_INCLUDE_DIRS result.
+  set(CURSES_INCLUDE_DIRS ${CURSES_INCLUDE_PATH})
+  set(CURSES_INCLUDE_DIR ${CURSES_INCLUDE_PATH}) # compatibility
+
+  # handle the QUIETLY and REQUIRED arguments and set CURSES_FOUND to TRUE if
+  # all listed variables are TRUE
+  include(FindPackageHandleStandardArgs)
+  FIND_PACKAGE_HANDLE_STANDARD_ARGS(Ncursesw DEFAULT_MSG
+    CURSES_LIBRARY CURSES_INCLUDE_PATH)
+  set(CURSES_FOUND ${NCURSESW_FOUND})
+
+else()
+  find_package(Curses)
+  set(NCURSESW_FOUND FALSE)
+endif()
+
+mark_as_advanced(
+  CURSES_INCLUDE_PATH
+  CURSES_CURSES_LIBRARY
+  CURSES_NCURSES_LIBRARY
+  CURSES_NCURSESW_LIBRARY
+  CURSES_EXTRA_LIBRARY
+  CURSES_FORM_LIBRARY
+  )
index 036cea6c93aea61a793525ea27037be6677deee9..a135e541cfbc4bc7fcd4dccefdeb8140685c3d88 100644 (file)
@@ -89,6 +89,9 @@ Run speed tests
 .B \-\-migrate <value>
 Migrate from current map backend to another. Possible values are sqlite3,
 leveldb, redis, and dummy.
+.TP
+.B \-\-terminal
+Display an interactive terminal over ncurses during execution.
 
 .SH ENVIRONMENT
 .TP
index 72b52436ce45f237f226474b3d9408c2e7c0d98c..55f5d4ad850777a2b1e122a7e04a2132171eed0a 100644 (file)
@@ -160,6 +160,20 @@ find_package(Lua REQUIRED)
 
 find_package(GMP REQUIRED)
 
+option(ENABLE_CURSES "Enable ncurses console" TRUE)
+set(USE_CURSES FALSE)
+
+if(ENABLE_CURSES)
+       find_package(Ncursesw)
+       if(CURSES_FOUND)
+               set(USE_CURSES TRUE)
+               message(STATUS "ncurses console enabled.")
+               include_directories(${CURSES_INCLUDE_DIRS})
+       else()
+               message(STATUS "ncurses not found!")
+       endif()
+endif(ENABLE_CURSES)
+
 option(ENABLE_LEVELDB "Enable LevelDB backend" TRUE)
 set(USE_LEVELDB FALSE)
 
@@ -322,6 +336,7 @@ set(common_SRCS
        areastore.cpp
        ban.cpp
        cavegen.cpp
+       chat.cpp
        clientiface.cpp
        collision.cpp
        content_abm.cpp
@@ -387,6 +402,7 @@ set(common_SRCS
        sound.cpp
        staticobject.cpp
        subgame.cpp
+       terminal_chat_console.cpp
        tool.cpp
        treegen.cpp
        version.cpp
@@ -431,7 +447,6 @@ set(client_SRCS
        ${sound_SRCS}
        ${client_network_SRCS}
        camera.cpp
-       chat.cpp
        client.cpp
        clientmap.cpp
        clientmedia.cpp
@@ -558,6 +573,9 @@ if(BUILD_CLIENT)
                        ${CGUITTFONT_LIBRARY}
                )
        endif()
+       if (USE_CURSES)
+               target_link_libraries(${PROJECT_NAME} ${CURSES_LIBRARIES})
+       endif()
        if (USE_LEVELDB)
                target_link_libraries(${PROJECT_NAME} ${LEVELDB_LIBRARY})
        endif()
@@ -585,6 +603,9 @@ if(BUILD_SERVER)
        )
        set_target_properties(${PROJECT_NAME}server PROPERTIES
                        COMPILE_DEFINITIONS "SERVER")
+       if (USE_CURSES)
+               target_link_libraries(${PROJECT_NAME}server ${CURSES_LIBRARIES})
+       endif()
        if (USE_LEVELDB)
                target_link_libraries(${PROJECT_NAME}server ${LEVELDB_LIBRARY})
        endif()
index 82ce80875a2bd7d38e635e13015b7085af070078..5d26baf7bbdb247f09197bf52b5d74d5eacb2785 100644 (file)
@@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include <vector>
 #include <list>
 
-// Chat console related classes, only used by the client
+// Chat console related classes
 
 struct ChatLine
 {
@@ -123,7 +123,7 @@ private:
        u32 m_scrollback;
        // Array of unformatted chat lines
        std::vector<ChatLine> m_unformatted;
-       
+
        // Number of character columns in console
        u32 m_cols;
        // Number of character rows in console
@@ -213,7 +213,7 @@ private:
        std::wstring m_line;
        // History buffer
        std::vector<std::wstring> m_history;
-       // History index (0 <= m_history_index <= m_history.size()) 
+       // History index (0 <= m_history_index <= m_history.size())
        u32 m_history_index;
        // Maximum number of history entries
        u32 m_history_limit;
diff --git a/src/chat_interface.h b/src/chat_interface.h
new file mode 100644 (file)
index 0000000..4784821
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+Minetest
+Copyright (C) 2015 est31 <MTest31@outlook.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#ifndef CHAT_INTERFACE_H
+#define CHAT_INTERFACE_H
+
+#include "util/container.h"
+#include <string>
+#include <queue>
+#include "irrlichttypes.h"
+
+enum ChatEventType {
+       CET_CHAT,
+       CET_NICK_ADD,
+       CET_NICK_REMOVE,
+       CET_TIME_INFO,
+};
+
+class ChatEvent {
+protected:
+       ChatEvent(ChatEventType a_type) { type = a_type; }
+public:
+       ChatEventType type;
+};
+
+struct ChatEventTimeInfo : public ChatEvent {
+       ChatEventTimeInfo(
+               u64 a_game_time,
+               u32 a_time) :
+       ChatEvent(CET_TIME_INFO),
+       game_time(a_game_time),
+       time(a_time)
+       {}
+
+       u64 game_time;
+       u32 time;
+};
+
+struct ChatEventNick : public ChatEvent {
+       ChatEventNick(ChatEventType a_type,
+               const std::string &a_nick) :
+       ChatEvent(a_type), // one of CET_NICK_ADD, CET_NICK_REMOVE
+       nick(a_nick)
+       {}
+
+       std::string nick;
+};
+
+struct ChatEventChat : public ChatEvent {
+       ChatEventChat(const std::string &a_nick,
+               const std::wstring &an_evt_msg) :
+       ChatEvent(CET_CHAT),
+       nick(a_nick),
+       evt_msg(an_evt_msg)
+       {}
+
+       std::string nick;
+       std::wstring evt_msg;
+};
+
+struct ChatInterface {
+       MutexedQueue<ChatEvent *> command_queue; // chat backend --> server
+       MutexedQueue<ChatEvent *> outgoing_queue; // server --> chat backend
+};
+
+#endif
index bda7a891abc5ba7b26a747b1701cb3fbb544a751..018532d131927f5fb94b67e4438c365f1a2acb9a 100644 (file)
 #cmakedefine01 USE_CURL
 #cmakedefine01 USE_SOUND
 #cmakedefine01 USE_FREETYPE
+#cmakedefine01 USE_CURSES
 #cmakedefine01 USE_LEVELDB
 #cmakedefine01 USE_LUAJIT
 #cmakedefine01 USE_SPATIAL
 #cmakedefine01 USE_SYSTEM_GMP
 #cmakedefine01 USE_REDIS
 #cmakedefine01 HAVE_ENDIAN_H
+#cmakedefine01 CURSES_HAVE_CURSES_H
+#cmakedefine01 CURSES_HAVE_NCURSES_H
+#cmakedefine01 CURSES_HAVE_NCURSES_NCURSES_H
+#cmakedefine01 CURSES_HAVE_NCURSES_CURSES_H
+#cmakedefine01 CURSES_HAVE_NCURSESW_NCURSES_H
+#cmakedefine01 CURSES_HAVE_NCURSESW_CURSES_H
 
 #endif
 
index 3761e416d5c0b2ef4bbf7ace693b2cb086121a75..8647160b1bed7c20cae3dae6e035bfc23bef521e 100644 (file)
@@ -37,6 +37,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
        #include "filesys.h"
 #endif
 
+#if USE_CURSES
+       #include "terminal_chat_console.h"
+#endif
+
 /*
        Assert
 */
@@ -44,6 +48,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 void sanity_check_fn(const char *assertion, const char *file,
                unsigned int line, const char *function)
 {
+#if USE_CURSES
+       g_term_console.stopAndWaitforThread();
+#endif
+
        errorstream << std::endl << "In thread " << std::hex
                << thr_get_current_thread_id() << ":" << std::endl;
        errorstream << file << ":" << line << ": " << function
@@ -57,6 +65,10 @@ void sanity_check_fn(const char *assertion, const char *file,
 void fatal_error_fn(const char *msg, const char *file,
                unsigned int line, const char *function)
 {
+#if USE_CURSES
+       g_term_console.stopAndWaitforThread();
+#endif
+
        errorstream << std::endl << "In thread " << std::hex
                << thr_get_current_thread_id() << ":" << std::endl;
        errorstream << file << ":" << line << ": " << function
index 5cba8f70056a53ccd90199e9df656ef7c1f407dc..600e715c12680653379b935551aa2e48384bbffe 100644 (file)
@@ -181,6 +181,14 @@ void Logger::addOutput(ILogOutput *out, LogLevel lev)
        m_outputs[lev].push_back(out);
 }
 
+void Logger::addOutputMasked(ILogOutput *out, LogLevelMask mask)
+{
+       for (size_t i = 0; i < LL_MAX; i++) {
+               if (mask & LOGLEVEL_TO_MASKLEVEL(i))
+                       m_outputs[i].push_back(out);
+       }
+}
+
 void Logger::addOutputMaxLevel(ILogOutput *out, LogLevel lev)
 {
        assert(lev < LL_MAX);
@@ -188,15 +196,19 @@ void Logger::addOutputMaxLevel(ILogOutput *out, LogLevel lev)
                m_outputs[i].push_back(out);
 }
 
-void Logger::removeOutput(ILogOutput *out)
+LogLevelMask Logger::removeOutput(ILogOutput *out)
 {
+       LogLevelMask ret_mask = 0;
        for (size_t i = 0; i < LL_MAX; i++) {
                std::vector<ILogOutput *>::iterator it;
 
                it = std::find(m_outputs[i].begin(), m_outputs[i].end(), out);
-               if (it != m_outputs[i].end())
+               if (it != m_outputs[i].end()) {
+                       ret_mask |= LOGLEVEL_TO_MASKLEVEL(i);
                        m_outputs[i].erase(it);
+               }
        }
+       return ret_mask;
 }
 
 void Logger::setLevelSilenced(LogLevel lev, bool silenced)
index f877f2f8a1c26f9767996c5c72d04a4e67327ff5..219255d9abfd6af92f042efa32ea0df3946d56e2 100644 (file)
--- a/src/log.h
+++ b/src/log.h
@@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include <string>
 #include <fstream>
 #include "threads.h"
+#include "irrlichttypes.h"
 
 class ILogOutput;
 
@@ -38,12 +39,16 @@ enum LogLevel {
        LL_MAX,
 };
 
+typedef u8 LogLevelMask;
+#define LOGLEVEL_TO_MASKLEVEL(x) (1 << x)
+
 class Logger {
 public:
        void addOutput(ILogOutput *out);
        void addOutput(ILogOutput *out, LogLevel lev);
+       void addOutputMasked(ILogOutput *out, LogLevelMask mask);
        void addOutputMaxLevel(ILogOutput *out, LogLevel lev);
-       void removeOutput(ILogOutput *out);
+       LogLevelMask removeOutput(ILogOutput *out);
        void setLevelSilenced(LogLevel lev, bool silenced);
 
        void registerThread(const std::string &name);
index 48b1af60313d306477169b42976fc9d8fabe2917..5046181b548ee25dc68b8154e2fe167676026188 100644 (file)
@@ -45,10 +45,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "httpfetch.h"
 #include "guiEngine.h"
 #include "map.h"
+#include "player.h"
 #include "mapsector.h"
 #include "fontengine.h"
 #include "gameparams.h"
 #include "database.h"
+#include "config.h"
+#if USE_CURSES
+       #include "terminal_chat_console.h"
+#endif
 #ifndef SERVER
 #include "client/clientlauncher.h"
 #endif
@@ -277,6 +282,8 @@ static void set_allowed_options(OptionList *allowed_options)
                        _("Set gameid (\"--gameid list\" prints available ones)"))));
        allowed_options->insert(std::make_pair("migrate", ValueSpec(VALUETYPE_STRING,
                        _("Migrate from current map backend to another (Only works when using minetestserver or with --server)"))));
+       allowed_options->insert(std::make_pair("terminal", ValueSpec(VALUETYPE_FLAG,
+                       _("Feature an interactive terminal (Only works when using minetestserver or with --server)"))));
 #ifndef SERVER
        allowed_options->insert(std::make_pair("videomodes", ValueSpec(VALUETYPE_FLAG,
                        _("Show available video modes"))));
@@ -816,21 +823,83 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
        if (cmd_args.exists("migrate"))
                return migrate_database(game_params, cmd_args);
 
-       try {
-               // Create server
-               Server server(game_params.world_path, game_params.game_spec, false,
-                       bind_addr.isIPv6());
-               server.start(bind_addr);
-
-               // Run server
+       if (cmd_args.exists("terminal")) {
+#if USE_CURSES
+               bool name_ok = true;
+               std::string admin_nick = g_settings->get("name");
+
+               name_ok = name_ok && !admin_nick.empty();
+               name_ok = name_ok && string_allowed(admin_nick, PLAYERNAME_ALLOWED_CHARS);
+
+               if (!name_ok) {
+                       if (admin_nick.empty()) {
+                               errorstream << "No name given for admin. "
+                                       << "Please check your minetest.conf that it "
+                                       << "contains a 'name = ' to your main admin account."
+                                       << std::endl;
+                       } else {
+                               errorstream << "Name for admin '"
+                                       << admin_nick << "' is not valid. "
+                                       << "Please check that it only contains allowed characters. "
+                                       << "Valid characters are: " << PLAYERNAME_ALLOWED_CHARS_USER_EXPL
+                                       << std::endl;
+                       }
+                       return false;
+               }
+               ChatInterface iface;
                bool &kill = *porting::signal_handler_killstatus();
-               dedicated_server_loop(server, kill);
-       } catch (const ModError &e) {
-               errorstream << "ModError: " << e.what() << std::endl;
-               return false;
-       } catch (const ServerError &e) {
-               errorstream << "ServerError: " << e.what() << std::endl;
-               return false;
+
+               try {
+                       // Create server
+                       Server server(game_params.world_path,
+                               game_params.game_spec, false, bind_addr.isIPv6(), &iface);
+
+                       g_term_console.setup(&iface, &kill, admin_nick);
+
+                       g_term_console.start();
+
+                       server.start(bind_addr);
+                       // Run server
+                       dedicated_server_loop(server, kill);
+               } catch (const ModError &e) {
+                       g_term_console.stopAndWaitforThread();
+                       errorstream << "ModError: " << e.what() << std::endl;
+                       return false;
+               } catch (const ServerError &e) {
+                       g_term_console.stopAndWaitforThread();
+                       errorstream << "ServerError: " << e.what() << std::endl;
+                       return false;
+               }
+
+               // Tell the console to stop, and wait for it to finish,
+               // only then leave context and free iface
+               g_term_console.stop();
+               g_term_console.wait();
+
+               g_term_console.clearKillStatus();
+       } else {
+#else
+               errorstream << "Cmd arg --terminal passed, but "
+                       << "compiled without ncurses. Ignoring." << std::endl;
+       } {
+#endif
+               try {
+                       // Create server
+                       Server server(game_params.world_path, game_params.game_spec, false,
+                               bind_addr.isIPv6());
+                       server.start(bind_addr);
+
+                       // Run server
+                       bool &kill = *porting::signal_handler_killstatus();
+                       dedicated_server_loop(server, kill);
+
+               } catch (const ModError &e) {
+                       errorstream << "ModError: " << e.what() << std::endl;
+                       return false;
+               } catch (const ServerError &e) {
+                       errorstream << "ServerError: " << e.what() << std::endl;
+                       return false;
+               }
        }
 
        return true;
index d9ff564da924f03d890d4295a641f0de5f1a4dae..3c446e31d2926c973bef211ceaf399b21ebb371b 100644 (file)
@@ -1059,69 +1059,14 @@ void Server::handleCommand_ChatMessage(NetworkPacket* pkt)
                return;
        }
 
-       // If something goes wrong, this player is to blame
-       RollbackScopeActor rollback_scope(m_rollback,
-                       std::string("player:")+player->getName());
-
        // Get player name of this client
-       std::wstring name = narrow_to_wide(player->getName());
-
-       // Run script hook
-       bool ate = m_script->on_chat_message(player->getName(),
-                       wide_to_narrow(message));
-       // If script ate the message, don't proceed
-       if (ate)
-               return;
-
-       // Line to send to players
-       std::wstring line;
-       // Whether to send to the player that sent the line
-       bool send_to_sender_only = false;
-
-       // Commands are implemented in Lua, so only catch invalid
-       // commands that were not "eaten" and send an error back
-       if (message[0] == L'/') {
-               message = message.substr(1);
-               send_to_sender_only = true;
-               if (message.length() == 0)
-                       line += L"-!- Empty command";
-               else
-                       line += L"-!- Invalid command: " + str_split(message, L' ')[0];
-       }
-       else {
-               if (checkPriv(player->getName(), "shout")) {
-                       line += L"<";
-                       line += name;
-                       line += L"> ";
-                       line += message;
-               } else {
-                       line += L"-!- You don't have permission to shout.";
-                       send_to_sender_only = true;
-               }
-       }
+       std::string name = player->getName();
+       std::wstring wname = narrow_to_wide(name);
 
-       if (line != L"")
-       {
-               /*
-                       Send the message to sender
-               */
-               if (send_to_sender_only) {
-                       SendChatMessage(pkt->getPeerId(), line);
-               }
-               /*
-                       Send the message to others
-               */
-               else {
-                       actionstream << "CHAT: " << wide_to_narrow(line)<<std::endl;
-
-                       std::vector<u16> clients = m_clients.getClientIDs();
-
-                       for (std::vector<u16>::iterator i = clients.begin();
-                               i != clients.end(); ++i) {
-                               if (*i != pkt->getPeerId())
-                                       SendChatMessage(*i, line);
-                       }
-               }
+       std::wstring answer_to_sender = handleChat(name, wname, message, pkt->getPeerId());
+       if (!answer_to_sender.empty()) {
+               // Send the answer to sender
+               SendChatMessage(pkt->getPeerId(), answer_to_sender);
        }
 }
 
index ec30e59d2982494f923282ded08649adba5e85d2..c112618761d50572c5cc35ca792d9077d0faa0ad 100644 (file)
@@ -29,6 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #define PLAYERNAME_SIZE 20
 
 #define PLAYERNAME_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
+#define PLAYERNAME_ALLOWED_CHARS_USER_EXPL "'a' to 'z', 'A' to 'Z', '0' to '9', '-', '_'"
 
 struct PlayerControl
 {
index 85314a3bc319653d82da9eb471fc245e97cd2979..59d3f5c70351274947e3fe7b9c159df5b519e05e 100644 (file)
@@ -45,6 +45,16 @@ int ModApiServer::l_get_server_status(lua_State *L)
        return 1;
 }
 
+// print(text)
+int ModApiServer::l_print(lua_State *L)
+{
+       NO_MAP_LOCK_REQUIRED;
+       std::string text;
+       text = luaL_checkstring(L, 1);
+       getServer(L)->printToConsoleOnly(text);
+       return 0;
+}
+
 // chat_send_all(text)
 int ModApiServer::l_chat_send_all(lua_State *L)
 {
@@ -505,6 +515,8 @@ void ModApiServer::Initialize(lua_State *L, int top)
        API_FCT(get_modpath);
        API_FCT(get_modnames);
 
+       API_FCT(print);
+
        API_FCT(chat_send_all);
        API_FCT(chat_send_player);
        API_FCT(show_formspec);
index df31f325f03281f323a03e29f1bb2f0532cb23b4..06a5ddc24d23a66e229864f0d1722c770d20b314 100644 (file)
@@ -46,6 +46,9 @@ private:
        // the returned list is sorted alphabetically for you
        static int l_get_modnames(lua_State *L);
 
+       // print(text)
+       static int l_print(lua_State *L);
+
        // chat_send_all(text)
        static int l_chat_send_all(lua_State *L);
 
index 68c24520c8edfd505cb93ce182b0f59c60be2ab8..6fac7e7eb67608014e044b66ea949b5bb8dcc79d 100644 (file)
@@ -38,7 +38,7 @@ private:
        // log([level,] text)
        // Writes a line to the logger.
        // The one-argument version logs to infostream.
-       // The two-argument version accept a log level: error, action, info, or verbose.
+       // The two-argument version accepts a log level.
        static int l_log(lua_State *L);
 
        // get us precision time
index 8c42ab5fd29d550ea04a35c72e7b21ac8c1979cb..6cb79c8754b77eab96613ad983aa2147fde8b358 100644 (file)
@@ -148,7 +148,8 @@ Server::Server(
                const std::string &path_world,
                const SubgameSpec &gamespec,
                bool simple_singleplayer_mode,
-               bool ipv6
+               bool ipv6,
+               ChatInterface *iface
        ):
        m_path_world(path_world),
        m_gamespec(gamespec),
@@ -175,6 +176,7 @@ Server::Server(
        m_clients(&m_con),
        m_shutdown_requested(false),
        m_shutdown_ask_reconnect(false),
+       m_admin_chat(iface),
        m_ignore_map_edit_events(false),
        m_ignore_map_edit_events_peer_id(0),
        m_next_sound_id(0)
@@ -575,6 +577,36 @@ void Server::AsyncRunStep(bool initial_step)
                        U32_MAX);
        }
 
+       /*
+               Listen to the admin chat, if available
+       */
+       if (m_admin_chat) {
+               if (!m_admin_chat->command_queue.empty()) {
+                       MutexAutoLock lock(m_env_mutex);
+                       while (!m_admin_chat->command_queue.empty()) {
+                               ChatEvent *evt = m_admin_chat->command_queue.pop_frontNoEx();
+                               if (evt->type == CET_NICK_ADD) {
+                                       // The terminal informed us of its nick choice
+                                       m_admin_nick = ((ChatEventNick *)evt)->nick;
+                                       if (!m_script->getAuth(m_admin_nick, NULL, NULL)) {
+                                               errorstream << "You haven't set up an account." << std::endl
+                                                       << "Please log in using the client as '"
+                                                       << m_admin_nick << "' with a secure password." << std::endl
+                                                       << "Until then, you can't execute admin tasks via the console," << std::endl
+                                                       << "and everybody can claim the user account instead of you," << std::endl
+                                                       << "giving them full control over this server." << std::endl;
+                                       }
+                               } else {
+                                       assert(evt->type == CET_CHAT);
+                                       handleAdminChat((ChatEventChat *)evt);
+                               }
+                               delete evt;
+                       }
+               }
+               m_admin_chat->outgoing_queue.push_back(
+                       new ChatEventTimeInfo(m_env->getGameTime(), m_env->getTimeOfDay()));
+       }
+
        /*
                Do background stuff
        */
@@ -1100,16 +1132,19 @@ PlayerSAO* Server::StageTwoClientInit(u16 peer_id)
 
                // Send information about joining in chat
                {
-                       std::wstring name = L"unknown";
+                       std::string name = "unknown";
                        Player *player = m_env->getPlayer(peer_id);
                        if(player != NULL)
-                               name = narrow_to_wide(player->getName());
+                               name = player->getName();
 
                        std::wstring message;
                        message += L"*** ";
-                       message += name;
+                       message += narrow_to_wide(name);
                        message += L" joined the game.";
                        SendChatMessage(PEER_ID_INEXISTENT,message);
+                       if (m_admin_chat)
+                               m_admin_chat->outgoing_queue.push_back(
+                                       new ChatEventNick(CET_NICK_ADD, name));
                }
        }
        Address addr = getPeerAddress(player->peer_id);
@@ -1432,6 +1467,16 @@ void Server::handlePeerChanges()
        }
 }
 
+void Server::printToConsoleOnly(const std::string &text)
+{
+       if (m_admin_chat) {
+               m_admin_chat->outgoing_queue.push_back(
+                       new ChatEventChat("", utf8_to_wide(text)));
+       } else {
+               std::cout << text;
+       }
+}
+
 void Server::Send(NetworkPacket* pkt)
 {
        m_clients.send(pkt->getPeerId(),
@@ -2665,9 +2710,13 @@ void Server::DeleteClient(u16 peer_id, ClientDeletionReason reason)
                                        os << player->getName() << " ";
                                }
 
-                               actionstream << player->getName() << " "
+                               std::string name = player->getName();
+                               actionstream << name << " "
                                                << (reason == CDR_TIMEOUT ? "times out." : "leaves game.")
                                                << " List of players: " << os.str() << std::endl;
+                               if (m_admin_chat)
+                                       m_admin_chat->outgoing_queue.push_back(
+                                               new ChatEventNick(CET_NICK_REMOVE, name));
                        }
                }
                {
@@ -2700,6 +2749,77 @@ void Server::UpdateCrafting(Player* player)
        plist->changeItem(0, preview);
 }
 
+std::wstring Server::handleChat(const std::string &name, const std::wstring &wname,
+       const std::wstring &wmessage, u16 peer_id_to_avoid_sending)
+{
+       // If something goes wrong, this player is to blame
+       RollbackScopeActor rollback_scope(m_rollback,
+               std::string("player:") + name);
+
+       // Line to send
+       std::wstring line;
+       // Whether to send line to the player that sent the message, or to all players
+       bool broadcast_line = true;
+
+       // Run script hook
+       bool ate = m_script->on_chat_message(name,
+               wide_to_utf8(wmessage));
+       // If script ate the message, don't proceed
+       if (ate)
+               return L"";
+
+       // Commands are implemented in Lua, so only catch invalid
+       // commands that were not "eaten" and send an error back
+       if (wmessage[0] == L'/') {
+               std::wstring wcmd = wmessage.substr(1);
+               broadcast_line = false;
+               if (wcmd.length() == 0)
+                       line += L"-!- Empty command";
+               else
+                       line += L"-!- Invalid command: " + str_split(wcmd, L' ')[0];
+       } else {
+               line += L"<";
+               line += wname;
+               line += L"> ";
+               line += wmessage;
+       }
+
+       /*
+               Tell calling method to send the message to sender
+       */
+       if (!broadcast_line) {
+               return line;
+       } else {
+               /*
+                       Send the message to others
+               */
+               actionstream << "CHAT: " << wide_to_narrow(line) << std::endl;
+
+               std::vector<u16> clients = m_clients.getClientIDs();
+
+               for (u16 i = 0; i < clients.size(); i++) {
+                       u16 cid = clients[i];
+                       if (cid != peer_id_to_avoid_sending)
+                               SendChatMessage(cid, line);
+               }
+       }
+       return L"";
+}
+
+void Server::handleAdminChat(const ChatEventChat *evt)
+{
+       std::string name = evt->nick;
+       std::wstring wname = utf8_to_wide(name);
+       std::wstring wmessage = evt->evt_msg;
+
+       std::wstring answer = handleChat(name, wname, wmessage);
+
+       // If asked to send answer to sender
+       if (!answer.empty()) {
+               m_admin_chat->outgoing_queue.push_back(new ChatEventChat("", answer));
+       }
+}
+
 RemoteClient* Server::getClient(u16 peer_id, ClientState state_min)
 {
        RemoteClient *client = getClientNoEx(peer_id,state_min);
@@ -2831,9 +2951,14 @@ void Server::notifyPlayer(const char *name, const std::wstring &msg)
        if (!m_env)
                return;
 
+       if (m_admin_nick == name && !m_admin_nick.empty()) {
+               m_admin_chat->outgoing_queue.push_back(new ChatEventChat("", msg));
+       }
+
        Player *player = m_env->getPlayer(name);
-       if (!player)
+       if (!player) {
                return;
+       }
 
        if (player->peer_id == PEER_ID_INEXISTENT)
                return;
index bee978de2505b9ec95d772853226c08962643191..6d66c938602e20b476c57656cbd54a8fc9c46978 100644 (file)
@@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "util/numeric.h"
 #include "util/thread.h"
 #include "environment.h"
+#include "chat_interface.h"
 #include "clientiface.h"
 #include "network/networkpacket.h"
 #include <string>
@@ -171,7 +172,8 @@ public:
                const std::string &path_world,
                const SubgameSpec &gamespec,
                bool simple_singleplayer_mode,
-               bool ipv6
+               bool ipv6,
+               ChatInterface *iface = NULL
        );
        ~Server();
        void start(Address bind_addr);
@@ -369,6 +371,8 @@ public:
                        u8* ser_vers, u16* prot_vers, u8* major, u8* minor, u8* patch,
                        std::string* vers_string);
 
+       void printToConsoleOnly(const std::string &text);
+
        void SendPlayerHPOrDie(PlayerSAO *player);
        void SendPlayerBreath(u16 peer_id);
        void SendInventory(PlayerSAO* playerSAO);
@@ -472,6 +476,12 @@ private:
        void DeleteClient(u16 peer_id, ClientDeletionReason reason);
        void UpdateCrafting(Player *player);
 
+       // This returns the answer to the sender of wmessage, or "" if there is none
+       std::wstring handleChat(const std::string &name, const std::wstring &wname,
+               const std::wstring &wmessage,
+               u16 peer_id_to_avoid_sending = PEER_ID_INEXISTENT);
+       void handleAdminChat(const ChatEventChat *evt);
+
        v3f findSpawnPos();
 
        // When called, connection mutex should be locked
@@ -597,6 +607,9 @@ private:
        std::string m_shutdown_msg;
        bool m_shutdown_ask_reconnect;
 
+       ChatInterface *m_admin_chat;
+       std::string m_admin_nick;
+
        /*
                Map edit event queue. Automatically receives all map edits.
                The constructor of this class registers us to receive them through
diff --git a/src/terminal_chat_console.cpp b/src/terminal_chat_console.cpp
new file mode 100644 (file)
index 0000000..ac06285
--- /dev/null
@@ -0,0 +1,452 @@
+/*
+Minetest
+Copyright (C) 2015 est31 <MTest31@outlook.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "config.h"
+#if USE_CURSES
+#include "version.h"
+#include "terminal_chat_console.h"
+#include "porting.h"
+#include "settings.h"
+#include "util/numeric.h"
+#include "util/string.h"
+
+TerminalChatConsole g_term_console;
+
+// include this last to avoid any conflicts
+// (likes to set macros to common names, conflicting various stuff)
+#if CURSES_HAVE_NCURSESW_NCURSES_H
+#include <ncursesw/ncurses.h>
+#elif CURSES_HAVE_NCURSESW_CURSES_H
+#include <ncursesw/curses.h>
+#elif CURSES_HAVE_CURSES_H
+#include <curses.h>
+#elif CURSES_HAVE_NCURSES_H
+#include <ncurses.h>
+#elif CURSES_HAVE_NCURSES_NCURSES_H
+#include <ncurses/ncurses.h>
+#elif CURSES_HAVE_NCURSES_CURSES_H
+#include <ncurses/curses.h>
+#endif
+
+// Some functions to make drawing etc position independent
+static bool reformat_backend(ChatBackend *backend, int rows, int cols)
+{
+       if (rows < 2)
+               return false;
+       backend->reformat(cols, rows - 2);
+       return true;
+}
+
+static void move_for_backend(int row, int col)
+{
+       move(row + 1, col);
+}
+
+void TerminalChatConsole::initOfCurses()
+{
+       initscr();
+       cbreak(); //raw();
+       noecho();
+       keypad(stdscr, TRUE);
+       nodelay(stdscr, TRUE);
+       timeout(100);
+
+       // To make esc not delay up to one second. According to the internet,
+       // this is the value vim uses, too.
+       set_escdelay(25);
+
+       getmaxyx(stdscr, m_rows, m_cols);
+       m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols);
+}
+
+void TerminalChatConsole::deInitOfCurses()
+{
+       endwin();
+}
+
+void *TerminalChatConsole::run()
+{
+       BEGIN_DEBUG_EXCEPTION_HANDLER
+
+       std::cout << "========================" << std::endl;
+       std::cout << "Begin log output over terminal"
+               << " (no stdout/stderr backlog during that)" << std::endl;
+       // Make the loggers to stdout/stderr shut up.
+       // Go over our own loggers instead.
+       LogLevelMask err_mask = g_logger.removeOutput(&stderr_output);
+       LogLevelMask out_mask = g_logger.removeOutput(&stdout_output);
+
+       g_logger.addOutput(&m_log_output);
+
+       // Inform the server of our nick
+       m_chat_interface->command_queue.push_back(
+               new ChatEventNick(CET_NICK_ADD, m_nick));
+
+       {
+               // Ensures that curses is deinitialized even on an exception being thrown
+               CursesInitHelper helper(this);
+
+               while (!stopRequested()) {
+
+                       int ch = getch();
+                       if (stopRequested())
+                               break;
+
+                       step(ch);
+               }
+       }
+
+       if (m_kill_requested)
+               *m_kill_requested = true;
+
+       g_logger.removeOutput(&m_log_output);
+       g_logger.addOutputMasked(&stderr_output, err_mask);
+       g_logger.addOutputMasked(&stdout_output, out_mask);
+
+       std::cout << "End log output over terminal"
+               << " (no stdout/stderr backlog during that)" << std::endl;
+       std::cout << "========================" << std::endl;
+
+       END_DEBUG_EXCEPTION_HANDLER
+
+       return NULL;
+}
+
+void TerminalChatConsole::typeChatMessage(const std::wstring &msg)
+{
+       // Discard empty line
+       if (msg.empty())
+               return;
+
+       // Send to server
+       m_chat_interface->command_queue.push_back(
+               new ChatEventChat(m_nick, msg));
+
+       // Print if its a command (gets eaten by server otherwise)
+       if (msg[0] == L'/') {
+               m_chat_backend.addMessage(L"", (std::wstring)L"Issued command: " + msg);
+       }
+}
+
+void TerminalChatConsole::handleInput(int ch, bool &complete_redraw_needed)
+{
+       // Helpful if you want to collect key codes that aren't documented
+       /*if (ch != ERR) {
+               m_chat_backend.addMessage(L"",
+                       (std::wstring)L"Pressed key " + utf8_to_wide(
+                       std::string(keyname(ch)) + " (code " + itos(ch) + ")"));
+               complete_redraw_needed = true;
+       }//*/
+
+       // All the key codes below are compatible to xterm
+       // Only add new ones if you have tried them there,
+       // to ensure compatibility with not just xterm but the wide
+       // range of terminals that are compatible to xterm.
+
+       switch (ch) {
+               case ERR: // no input
+                       break;
+               case 27: // ESC
+                       // Toggle ESC mode
+                       m_esc_mode = !m_esc_mode;
+                       break;
+               case KEY_PPAGE:
+                       m_chat_backend.scrollPageUp();
+                       complete_redraw_needed = true;
+                       break;
+               case KEY_NPAGE:
+                       m_chat_backend.scrollPageDown();
+                       complete_redraw_needed = true;
+                       break;
+               case KEY_ENTER:
+               case '\r':
+               case '\n': {
+                       std::wstring text = m_chat_backend.getPrompt().submit();
+                       typeChatMessage(text);
+                       break;
+               }
+               case KEY_UP:
+                       m_chat_backend.getPrompt().historyPrev();
+                       break;
+               case KEY_DOWN:
+                       m_chat_backend.getPrompt().historyNext();
+                       break;
+               case KEY_LEFT:
+                       // Left pressed
+                       // move character to the left
+                       m_chat_backend.getPrompt().cursorOperation(
+                               ChatPrompt::CURSOROP_MOVE,
+                               ChatPrompt::CURSOROP_DIR_LEFT,
+                               ChatPrompt::CURSOROP_SCOPE_CHARACTER);
+                       break;
+               case 545:
+                       // Ctrl-Left pressed
+                       // move word to the left
+                       m_chat_backend.getPrompt().cursorOperation(
+                               ChatPrompt::CURSOROP_MOVE,
+                               ChatPrompt::CURSOROP_DIR_LEFT,
+                               ChatPrompt::CURSOROP_SCOPE_WORD);
+                       break;
+               case KEY_RIGHT:
+                       // Right pressed
+                       // move character to the right
+                       m_chat_backend.getPrompt().cursorOperation(
+                               ChatPrompt::CURSOROP_MOVE,
+                               ChatPrompt::CURSOROP_DIR_RIGHT,
+                               ChatPrompt::CURSOROP_SCOPE_CHARACTER);
+                       break;
+               case 560:
+                       // Ctrl-Right pressed
+                       // move word to the right
+                       m_chat_backend.getPrompt().cursorOperation(
+                               ChatPrompt::CURSOROP_MOVE,
+                               ChatPrompt::CURSOROP_DIR_RIGHT,
+                               ChatPrompt::CURSOROP_SCOPE_WORD);
+                       break;
+               case KEY_HOME:
+                       // Home pressed
+                       // move to beginning of line
+                       m_chat_backend.getPrompt().cursorOperation(
+                               ChatPrompt::CURSOROP_MOVE,
+                               ChatPrompt::CURSOROP_DIR_LEFT,
+                               ChatPrompt::CURSOROP_SCOPE_LINE);
+                       break;
+               case KEY_END:
+                       // End pressed
+                       // move to end of line
+                       m_chat_backend.getPrompt().cursorOperation(
+                               ChatPrompt::CURSOROP_MOVE,
+                               ChatPrompt::CURSOROP_DIR_RIGHT,
+                               ChatPrompt::CURSOROP_SCOPE_LINE);
+                       break;
+               case KEY_BACKSPACE:
+               case '\b':
+               case 127:
+                       // Backspace pressed
+                       // delete character to the left
+                       m_chat_backend.getPrompt().cursorOperation(
+                               ChatPrompt::CURSOROP_DELETE,
+                               ChatPrompt::CURSOROP_DIR_LEFT,
+                               ChatPrompt::CURSOROP_SCOPE_CHARACTER);
+                       break;
+               case KEY_DC:
+                       // Delete pressed
+                       // delete character to the right
+                       m_chat_backend.getPrompt().cursorOperation(
+                               ChatPrompt::CURSOROP_DELETE,
+                               ChatPrompt::CURSOROP_DIR_RIGHT,
+                               ChatPrompt::CURSOROP_SCOPE_CHARACTER);
+                       break;
+               case 519:
+                       // Ctrl-Delete pressed
+                       // delete word to the right
+                       m_chat_backend.getPrompt().cursorOperation(
+                               ChatPrompt::CURSOROP_DELETE,
+                               ChatPrompt::CURSOROP_DIR_RIGHT,
+                               ChatPrompt::CURSOROP_SCOPE_WORD);
+                       break;
+               case 21:
+                       // Ctrl-U pressed
+                       // kill line to left end
+                       m_chat_backend.getPrompt().cursorOperation(
+                               ChatPrompt::CURSOROP_DELETE,
+                               ChatPrompt::CURSOROP_DIR_LEFT,
+                               ChatPrompt::CURSOROP_SCOPE_LINE);
+                       break;
+               case 11:
+                       // Ctrl-K pressed
+                       // kill line to right end
+                       m_chat_backend.getPrompt().cursorOperation(
+                               ChatPrompt::CURSOROP_DELETE,
+                               ChatPrompt::CURSOROP_DIR_RIGHT,
+                               ChatPrompt::CURSOROP_SCOPE_LINE);
+                       break;
+               case KEY_TAB:
+                       // Tab pressed
+                       // Nick completion
+                       m_chat_backend.getPrompt().nickCompletion(m_nicks, false);
+                       break;
+               default:
+                       // Add character to the prompt,
+                       // assuming UTF-8.
+                       if (IS_UTF8_MULTB_START(ch)) {
+                               m_pending_utf8_bytes.append(1, (char)ch);
+                               m_utf8_bytes_to_wait += UTF8_MULTB_START_LEN(ch) - 1;
+                       } else if (m_utf8_bytes_to_wait != 0) {
+                               m_pending_utf8_bytes.append(1, (char)ch);
+                               m_utf8_bytes_to_wait--;
+                               if (m_utf8_bytes_to_wait == 0) {
+                                       std::wstring w = utf8_to_wide(m_pending_utf8_bytes);
+                                       m_pending_utf8_bytes = "";
+                                       // hopefully only one char in the wstring...
+                                       for (size_t i = 0; i < w.size(); i++) {
+                                               m_chat_backend.getPrompt().input(w.c_str()[i]);
+                                       }
+                               }
+                       } else if (IS_ASCII_PRINTABLE_CHAR(ch)) {
+                               m_chat_backend.getPrompt().input(ch);
+                       } else {
+                               // Silently ignore characters we don't handle
+
+                               //warningstream << "Pressed invalid character '"
+                               //      << keyname(ch) << "' (code " << itos(ch) << ")" << std::endl;
+                       }
+                       break;
+       }
+}
+
+void TerminalChatConsole::step(int ch)
+{
+       bool complete_redraw_needed = false;
+
+       // empty queues
+       while (!m_chat_interface->outgoing_queue.empty()) {
+               ChatEvent *evt = m_chat_interface->outgoing_queue.pop_frontNoEx();
+               switch (evt->type) {
+                       case CET_NICK_REMOVE:
+                               m_nicks.remove(((ChatEventNick *)evt)->nick);
+                               break;
+                       case CET_NICK_ADD:
+                               m_nicks.push_back(((ChatEventNick *)evt)->nick);
+                               break;
+                       case CET_CHAT:
+                               complete_redraw_needed = true;
+                               // This is only used for direct replies from commands
+                               // or for lua's print() functionality
+                               m_chat_backend.addMessage(L"", ((ChatEventChat *)evt)->evt_msg);
+                               break;
+                       case CET_TIME_INFO:
+                               ChatEventTimeInfo *tevt = (ChatEventTimeInfo *)evt;
+                               m_game_time = tevt->game_time;
+                               m_time_of_day = tevt->time;
+               };
+               delete evt;
+       }
+       while (!m_log_output.queue.empty()) {
+               complete_redraw_needed = true;
+               std::pair<LogLevel, std::string> p = m_log_output.queue.pop_frontNoEx();
+               if (p.first > m_log_level)
+                       continue;
+
+               m_chat_backend.addMessage(
+                       utf8_to_wide(Logger::getLevelLabel(p.first)),
+                       utf8_to_wide(p.second));
+       }
+
+       // handle input
+       if (!m_esc_mode) {
+               handleInput(ch, complete_redraw_needed);
+       } else {
+               switch (ch) {
+                       case ERR: // no input
+                               break;
+                       case 27: // ESC
+                               // Toggle ESC mode
+                               m_esc_mode = !m_esc_mode;
+                               break;
+                       case 'L':
+                               m_log_level--;
+                               m_log_level = MYMAX(m_log_level, LL_NONE + 1); // LL_NONE isn't accessible
+                               break;
+                       case 'l':
+                               m_log_level++;
+                               m_log_level = MYMIN(m_log_level, LL_MAX - 1);
+                               break;
+               }
+       }
+
+       // was there a resize?
+       int xn, yn;
+       getmaxyx(stdscr, yn, xn);
+       if (xn != m_cols || yn != m_rows) {
+               m_cols = xn;
+               m_rows = yn;
+               m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols);
+               complete_redraw_needed = true;
+       }
+
+       // draw title
+       move(0, 0);
+       clrtoeol();
+       addstr(PROJECT_NAME_C);
+       addstr(" ");
+       addstr(g_version_hash);
+
+       u32 minutes = m_time_of_day % 1000;
+       u32 hours = m_time_of_day / 1000;
+       minutes = (float)minutes / 1000 * 60;
+
+       if (m_game_time)
+               printw(" | Game %d Time of day %02d:%02d ",
+                       m_game_time, hours, minutes);
+
+       // draw text
+       if (complete_redraw_needed && m_can_draw_text)
+               draw_text();
+
+       // draw prompt
+       if (!m_esc_mode) {
+               // normal prompt
+               ChatPrompt& prompt = m_chat_backend.getPrompt();
+               std::string prompt_text = wide_to_utf8(prompt.getVisiblePortion());
+               move(m_rows - 1, 0);
+               clrtoeol();
+               addstr(prompt_text.c_str());
+               // Draw cursor
+               s32 cursor_pos = prompt.getVisibleCursorPosition();
+               if (cursor_pos >= 0) {
+                       move(m_rows - 1, cursor_pos);
+               }
+       } else {
+               // esc prompt
+               move(m_rows - 1, 0);
+               clrtoeol();
+               printw("[ESC] Toggle ESC mode |"
+                       " [CTRL+C] Shut down |"
+                       " (L) in-, (l) decrease loglevel %s",
+                       Logger::getLevelLabel((LogLevel) m_log_level).c_str());
+       }
+
+       refresh();
+}
+
+void TerminalChatConsole::draw_text()
+{
+       ChatBuffer& buf = m_chat_backend.getConsoleBuffer();
+       for (u32 row = 0; row < buf.getRows(); row++) {
+               move_for_backend(row, 0);
+               clrtoeol();
+               const ChatFormattedLine& line = buf.getFormattedLine(row);
+               if (line.fragments.empty())
+                       continue;
+               for (u32 i = 0; i < line.fragments.size(); ++i) {
+                       const ChatFormattedFragment& fragment = line.fragments[i];
+                       addstr(wide_to_utf8(fragment.text).c_str());
+               }
+       }
+}
+
+void TerminalChatConsole::stopAndWaitforThread()
+{
+       clearKillStatus();
+       stop();
+       wait();
+}
+
+#endif
diff --git a/src/terminal_chat_console.h b/src/terminal_chat_console.h
new file mode 100644 (file)
index 0000000..2111b7e
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+Minetest
+Copyright (C) 2015 est31 <MTest31@outlook.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#ifndef TERMINAL_CHAT_CONSOLE_H
+#define TERMINAL_CHAT_CONSOLE_H
+
+#include "chat.h"
+#include "threading/thread.h"
+#include "chat_interface.h"
+#include "log.h"
+
+#include <sstream>
+
+class TermLogOutput : public ILogOutput {
+public:
+
+       void logRaw(LogLevel lev, const std::string &line)
+       {
+               queue.push_back(std::make_pair(lev, line));
+       }
+
+       virtual void log(LogLevel lev, const std::string &combined,
+               const std::string &time, const std::string &thread_name,
+               const std::string &payload_text)
+       {
+               std::ostringstream os(std::ios_base::binary);
+               os << time << ": [" << thread_name << "] " << payload_text;
+
+               queue.push_back(std::make_pair(lev, os.str()));
+       }
+
+       MutexedQueue<std::pair<LogLevel, std::string> > queue;
+};
+
+class TerminalChatConsole : public Thread {
+public:
+
+       TerminalChatConsole() :
+               Thread("TerminalThread"),
+               m_log_level(LL_ACTION),
+               m_utf8_bytes_to_wait(0),
+               m_kill_requested(NULL),
+               m_esc_mode(false),
+               m_game_time(0),
+               m_time_of_day(0)
+       {}
+
+       void setup(
+               ChatInterface *iface,
+               bool *kill_requested,
+               const std::string &nick)
+       {
+               m_nick = nick;
+               m_kill_requested = kill_requested;
+               m_chat_interface = iface;
+       }
+
+       virtual void *run();
+
+       // Highly required!
+       void clearKillStatus() { m_kill_requested = NULL; }
+
+       void stopAndWaitforThread();
+
+private:
+       // these have stupid names so that nobody missclassifies them
+       // as curses functions. Oh, curses has stupid names too?
+       // Well, at least it was worth a try...
+       void initOfCurses();
+       void deInitOfCurses();
+
+       void draw_text();
+
+       void typeChatMessage(const std::wstring &m);
+
+       void handleInput(int ch, bool &complete_redraw_needed);
+
+       void step(int ch);
+
+       // Used to ensure the deinitialisation is always called.
+       struct CursesInitHelper {
+               TerminalChatConsole *cons;
+               CursesInitHelper(TerminalChatConsole * a_console)
+                       : cons(a_console)
+               { cons->initOfCurses(); }
+               ~CursesInitHelper() { cons->deInitOfCurses(); }
+       };
+
+       int m_log_level;
+       std::string m_nick;
+
+       u8 m_utf8_bytes_to_wait;
+       std::string m_pending_utf8_bytes;
+
+       std::list<std::string> m_nicks;
+
+       int m_cols;
+       int m_rows;
+       bool m_can_draw_text;
+
+       bool *m_kill_requested;
+       ChatBackend m_chat_backend;
+       ChatInterface *m_chat_interface;
+
+       TermLogOutput m_log_output;
+
+       bool m_esc_mode;
+
+       u64 m_game_time;
+       u32 m_time_of_day;
+};
+
+extern TerminalChatConsole g_term_console;
+
+#endif
index 3c000e76052627fe7488f0b665c9b25fe80ffb6f..1785997deb79fbd9390e8ae1026a28f24a5ac42d 100644 (file)
@@ -43,6 +43,7 @@ public:
        void testStrToIntConversion();
        void testStringReplace();
        void testStringAllowed();
+       void testAsciiPrintableHelper();
        void testUTF8();
        void testWrapRows();
        void testIsNumber();
@@ -68,6 +69,7 @@ void TestUtilities::runTests(IGameDef *gamedef)
        TEST(testStrToIntConversion);
        TEST(testStringReplace);
        TEST(testStringAllowed);
+       TEST(testAsciiPrintableHelper);
        TEST(testUTF8);
        TEST(testWrapRows);
        TEST(testIsNumber);
@@ -232,6 +234,18 @@ void TestUtilities::testStringAllowed()
        UASSERT(string_allowed_blacklist("hello123", "123") == false);
 }
 
+void TestUtilities::testAsciiPrintableHelper()
+{
+       UASSERT(IS_ASCII_PRINTABLE_CHAR('e') == true);
+       UASSERT(IS_ASCII_PRINTABLE_CHAR('\0') == false);
+
+       // Ensures that there is no cutting off going on...
+       // If there were, 331 would be cut to 75 in this example
+       // and 73 is a valid ASCII char.
+       int ch = 331;
+       UASSERT(IS_ASCII_PRINTABLE_CHAR(ch) == false);
+}
+
 void TestUtilities::testUTF8()
 {
        UASSERT(wide_to_utf8(utf8_to_wide("")) == "");
index 793baad0ea729f90c3731ea741bca26f2ca3fccf..c8f60b8023119ed9081c757513b593f9d2dab39d 100644 (file)
@@ -32,8 +32,26 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #define STRINGIFY(x) #x
 #define TOSTRING(x) STRINGIFY(x)
 
+// Checks whether a value is an ASCII printable character
+#define IS_ASCII_PRINTABLE_CHAR(x)   \
+       (((unsigned int)(x) >= 0x20) &&  \
+       ( (unsigned int)(x) <= 0x7e))
+
 // Checks whether a byte is an inner byte for an utf-8 multibyte sequence
-#define IS_UTF8_MULTB_INNER(x) (((unsigned char)x >= 0x80) && ((unsigned char)x < 0xc0))
+#define IS_UTF8_MULTB_INNER(x)       \
+       (((unsigned char)(x) >= 0x80) && \
+       ( (unsigned char)(x) <= 0xbf))
+
+// Checks whether a byte is a start byte for an utf-8 multibyte sequence
+#define IS_UTF8_MULTB_START(x)       \
+       (((unsigned char)(x) >= 0xc2) && \
+       ( (unsigned char)(x) <= 0xf4))
+
+// Given a start byte x for an utf-8 multibyte sequence
+// it gives the length of the whole sequence in bytes.
+#define UTF8_MULTB_START_LEN(x)            \
+       (((unsigned char)(x) < 0xe0) ? 2 :     \
+       (((unsigned char)(x) < 0xf0) ? 3 : 4))
 
 typedef std::map<std::string, std::string> StringMap;