Add server side ncurses terminal

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.
This commit is contained in:
est31 2015-07-25 07:43:32 +02:00
parent 1384108f8c
commit 5e507c9829
22 changed files with 1213 additions and 90 deletions

@ -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

@ -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
)

@ -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

@ -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()

@ -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
{

82
src/chat_interface.h Normal file

@ -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

@ -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

@ -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

@ -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,16 +196,20 @@ 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)
{

@ -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);

@ -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,6 +823,66 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
if (cmd_args.exists("migrate"))
return migrate_database(game_params, cmd_args);
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();
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,
@ -825,6 +892,7 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
// 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;
@ -832,6 +900,7 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
errorstream << "ServerError: " << e.what() << std::endl;
return false;
}
}
return true;
}

@ -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());
std::string name = player->getName();
std::wstring wname = narrow_to_wide(name);
// 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;
}
}
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);
}
}

@ -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
{

@ -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);

@ -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);

@ -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

@ -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;

@ -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

@ -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

131
src/terminal_chat_console.h Normal file

@ -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

@ -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("")) == "");

@ -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;