summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--builtin/init.lua9
-rw-r--r--cmake/Modules/FindNcursesw.cmake189
-rw-r--r--doc/minetest.63
-rw-r--r--src/CMakeLists.txt23
-rw-r--r--src/chat.h6
-rw-r--r--src/chat_interface.h82
-rw-r--r--src/cmake_config.h.in7
-rw-r--r--src/debug.cpp12
-rw-r--r--src/log.cpp16
-rw-r--r--src/log.h7
-rw-r--r--src/main.cpp97
-rw-r--r--src/network/serverpackethandler.cpp67
-rw-r--r--src/player.h1
-rw-r--r--src/script/lua_api/l_server.cpp12
-rw-r--r--src/script/lua_api/l_server.h3
-rw-r--r--src/script/lua_api/l_util.h2
-rw-r--r--src/server.cpp137
-rw-r--r--src/server.h15
-rw-r--r--src/terminal_chat_console.cpp452
-rw-r--r--src/terminal_chat_console.h131
-rw-r--r--src/unittest/test_utilities.cpp14
-rw-r--r--src/util/string.h20
22 files changed, 1214 insertions, 91 deletions
diff --git a/builtin/init.lua b/builtin/init.lua
index 02fb9db93..b3004468e 100644
--- a/builtin/init.lua
+++ b/builtin/init.lua
@@ -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
index 000000000..dcb7cdda8
--- /dev/null
+++ b/cmake/Modules/FindNcursesw.cmake
@@ -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
+ )
diff --git a/doc/minetest.6 b/doc/minetest.6
index 036cea6c9..a135e541c 100644
--- a/doc/minetest.6
+++ b/doc/minetest.6
@@ -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
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 72b52436c..55f5d4ad8 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -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()
diff --git a/src/chat.h b/src/chat.h
index 82ce80875..5d26baf7b 100644
--- a/src/chat.h
+++ b/src/chat.h
@@ -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
index 000000000..4784821fc
--- /dev/null
+++ b/src/chat_interface.h
@@ -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
diff --git a/src/cmake_config.h.in b/src/cmake_config.h.in
index bda7a891a..018532d13 100644
--- a/src/cmake_config.h.in
+++ b/src/cmake_config.h.in
@@ -19,12 +19,19 @@
#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
diff --git a/src/debug.cpp b/src/debug.cpp
index 3761e416d..8647160b1 100644
--- a/src/debug.cpp
+++ b/src/debug.cpp
@@ -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
diff --git a/src/log.cpp b/src/log.cpp
index 5cba8f700..600e715c1 100644
--- a/src/log.cpp
+++ b/src/log.cpp
@@ -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)
diff --git a/src/log.h b/src/log.h
index f877f2f8a..219255d9a 100644
--- 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);
diff --git a/src/main.cpp b/src/main.cpp
index 48b1af603..5046181b5 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -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;
diff --git a/src/network/serverpackethandler.cpp b/src/network/serverpackethandler.cpp
index d9ff564da..3c446e31d 100644
--- a/src/network/serverpackethandler.cpp
+++ b/src/network/serverpackethandler.cpp
@@ -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);
}
}
diff --git a/src/player.h b/src/player.h
index ec30e59d2..c11261876 100644
--- a/src/player.h
+++ b/src/player.h
@@ -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
{
diff --git a/src/script/lua_api/l_server.cpp b/src/script/lua_api/l_server.cpp
index 85314a3bc..59d3f5c70 100644
--- a/src/script/lua_api/l_server.cpp
+++ b/src/script/lua_api/l_server.cpp
@@ -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);
diff --git a/src/script/lua_api/l_server.h b/src/script/lua_api/l_server.h
index df31f325f..06a5ddc24 100644
--- a/src/script/lua_api/l_server.h
+++ b/src/script/lua_api/l_server.h
@@ -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);
diff --git a/src/script/lua_api/l_util.h b/src/script/lua_api/l_util.h
index 68c24520c..6fac7e7eb 100644
--- a/src/script/lua_api/l_util.h
+++ b/src/script/lua_api/l_util.h
@@ -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
diff --git a/src/server.cpp b/src/server.cpp
index 8c42ab5fd..6cb79c875 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -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)
@@ -576,6 +578,36 @@ void Server::AsyncRunStep(bool initial_step)
}
/*
+ 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;
diff --git a/src/server.h b/src/server.h
index bee978de2..6d66c9386 100644
--- a/src/server.h
+++ b/src/server.h
@@ -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
index 000000000..ac06285eb
--- /dev/null
+++ b/src/terminal_chat_console.cpp
@@ -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
index 000000000..2111b7ecb
--- /dev/null
+++ b/src/terminal_chat_console.h
@@ -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
diff --git a/src/unittest/test_utilities.cpp b/src/unittest/test_utilities.cpp
index 3c000e760..1785997de 100644
--- a/src/unittest/test_utilities.cpp
+++ b/src/unittest/test_utilities.cpp
@@ -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("")) == "");
diff --git a/src/util/string.h b/src/util/string.h
index 793baad0e..c8f60b802 100644
--- a/src/util/string.h
+++ b/src/util/string.h
@@ -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;