7 changes: 7 additions & 0 deletions src/cmake_config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -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

12 changes: 12 additions & 0 deletions src/debug.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,21 @@ 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
*/

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
Expand All @@ -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
Expand Down
16 changes: 14 additions & 2 deletions src/log.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,22 +181,34 @@ 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);
for (size_t i = 0; i <= lev; i++)
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)
Expand Down
7 changes: 6 additions & 1 deletion src/log.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down
97 changes: 83 additions & 14 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"))));
Expand Down Expand Up @@ -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;
Expand Down
67 changes: 6 additions & 61 deletions src/network/serverpackethandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/player.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
12 changes: 12 additions & 0 deletions src/script/lua_api/l_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/script/lua_api/l_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ class ModApiServer : public ModApiBase {
// 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);

Expand Down
2 changes: 1 addition & 1 deletion src/script/lua_api/l_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class ModApiUtil : public ModApiBase {
// 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
Expand Down
137 changes: 131 additions & 6 deletions src/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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)
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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));
}
}
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
15 changes: 14 additions & 1 deletion src/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -171,7 +172,8 @@ class Server : public con::PeerHandler, public MapEventReceiver,
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);
Expand Down Expand Up @@ -369,6 +371,8 @@ class Server : public con::PeerHandler, public MapEventReceiver,
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);
Expand Down Expand Up @@ -472,6 +476,12 @@ class Server : public con::PeerHandler, public MapEventReceiver,
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
Expand Down Expand Up @@ -597,6 +607,9 @@ class Server : public con::PeerHandler, public MapEventReceiver,
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
Expand Down
452 changes: 452 additions & 0 deletions src/terminal_chat_console.cpp

Large diffs are not rendered by default.

131 changes: 131 additions & 0 deletions src/terminal_chat_console.h
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions src/unittest/test_utilities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class TestUtilities : public TestBase {
void testStrToIntConversion();
void testStringReplace();
void testStringAllowed();
void testAsciiPrintableHelper();
void testUTF8();
void testWrapRows();
void testIsNumber();
Expand All @@ -68,6 +69,7 @@ void TestUtilities::runTests(IGameDef *gamedef)
TEST(testStrToIntConversion);
TEST(testStringReplace);
TEST(testStringAllowed);
TEST(testAsciiPrintableHelper);
TEST(testUTF8);
TEST(testWrapRows);
TEST(testIsNumber);
Expand Down Expand Up @@ -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("")) == "");
Expand Down
20 changes: 19 additions & 1 deletion src/util/string.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down