aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKahrl <kahrl@gmx.net>2011-12-03 09:01:14 +0100
committerPerttu Ahola <celeron55@gmail.com>2012-03-10 20:11:10 +0200
commit967f25461bbde28dbc0247fa1c491e9d9938a5b2 (patch)
treed1e26464862551db3f1d12627cdbe9e31fe211a9 /src
parent00536518142a586f3fc51a07f76e12085e3cbd30 (diff)
downloadminetest-967f25461bbde28dbc0247fa1c491e9d9938a5b2.tar.gz
minetest-967f25461bbde28dbc0247fa1c491e9d9938a5b2.tar.bz2
minetest-967f25461bbde28dbc0247fa1c491e9d9938a5b2.zip
Chat console, including a number of rebases and modifications.
Defaults modified from original: alpha=200, key=F10
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt2
-rw-r--r--src/chat.cpp768
-rw-r--r--src/chat.h272
-rw-r--r--src/client.cpp49
-rw-r--r--src/client.h27
-rw-r--r--src/defaultsettings.cpp4
-rw-r--r--src/game.cpp151
-rw-r--r--src/game.h5
-rw-r--r--src/guiChatConsole.cpp550
-rw-r--r--src/guiChatConsole.h125
-rw-r--r--src/guiKeyChangeMenu.cpp28
-rw-r--r--src/guiKeyChangeMenu.h3
-rw-r--r--src/main.cpp32
-rw-r--r--src/utility.h49
14 files changed, 1918 insertions, 147 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e11c0f345..e1cfcfa09 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -167,6 +167,7 @@ set(minetest_SRCS
camera.cpp
clouds.cpp
clientobject.cpp
+ chat.cpp
guiMainMenu.cpp
guiKeyChangeMenu.cpp
guiMessageMenu.cpp
@@ -175,6 +176,7 @@ set(minetest_SRCS
guiPauseMenu.cpp
guiPasswordChange.cpp
guiDeathScreen.cpp
+ guiChatConsole.cpp
client.cpp
tile.cpp
game.cpp
diff --git a/src/chat.cpp b/src/chat.cpp
new file mode 100644
index 000000000..2f5f8a448
--- /dev/null
+++ b/src/chat.cpp
@@ -0,0 +1,768 @@
+/*
+Minetest-c55
+Copyright (C) 2011 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 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 General Public License for more details.
+
+You should have received a copy of the GNU 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 "chat.h"
+#include "debug.h"
+#include "utility.h"
+#include <cassert>
+#include <cctype>
+#include <sstream>
+
+ChatBuffer::ChatBuffer(u32 scrollback):
+ m_scrollback(scrollback),
+ m_unformatted(),
+ m_cols(0),
+ m_rows(0),
+ m_scroll(0),
+ m_formatted(),
+ m_empty_formatted_line()
+{
+ if (m_scrollback == 0)
+ m_scrollback = 1;
+ m_empty_formatted_line.first = true;
+}
+
+ChatBuffer::~ChatBuffer()
+{
+}
+
+void ChatBuffer::addLine(std::wstring name, std::wstring text)
+{
+ ChatLine line(name, text);
+ m_unformatted.push_back(line);
+
+ if (m_rows > 0)
+ {
+ // m_formatted is valid and must be kept valid
+ bool scrolled_at_bottom = (m_scroll == getBottomScrollPos());
+ u32 num_added = formatChatLine(line, m_cols, m_formatted);
+ if (scrolled_at_bottom)
+ m_scroll += num_added;
+ }
+
+ // Limit number of lines by m_scrollback
+ if (m_unformatted.size() > m_scrollback)
+ {
+ deleteOldest(m_unformatted.size() - m_scrollback);
+ }
+}
+
+void ChatBuffer::clear()
+{
+ m_unformatted.clear();
+ m_formatted.clear();
+ m_scroll = 0;
+}
+
+u32 ChatBuffer::getLineCount() const
+{
+ return m_unformatted.size();
+}
+
+u32 ChatBuffer::getScrollback() const
+{
+ return m_scrollback;
+}
+
+const ChatLine& ChatBuffer::getLine(u32 index) const
+{
+ assert(index < getLineCount());
+ return m_unformatted[index];
+}
+
+void ChatBuffer::step(f32 dtime)
+{
+ for (u32 i = 0; i < m_unformatted.size(); ++i)
+ {
+ m_unformatted[i].age += dtime;
+ }
+}
+
+void ChatBuffer::deleteOldest(u32 count)
+{
+ u32 del_unformatted = 0;
+ u32 del_formatted = 0;
+
+ while (count > 0 && del_unformatted < m_unformatted.size())
+ {
+ ++del_unformatted;
+
+ // keep m_formatted in sync
+ if (del_formatted < m_formatted.size())
+ {
+ assert(m_formatted[del_formatted].first);
+ ++del_formatted;
+ while (del_formatted < m_formatted.size() &&
+ !m_formatted[del_formatted].first)
+ ++del_formatted;
+ }
+
+ --count;
+ }
+
+ m_unformatted.erase(0, del_unformatted);
+ m_formatted.erase(0, del_formatted);
+}
+
+void ChatBuffer::deleteByAge(f32 maxAge)
+{
+ u32 count = 0;
+ while (count < m_unformatted.size() && m_unformatted[count].age > maxAge)
+ ++count;
+ deleteOldest(count);
+}
+
+u32 ChatBuffer::getColumns() const
+{
+ return m_cols;
+}
+
+u32 ChatBuffer::getRows() const
+{
+ return m_rows;
+}
+
+void ChatBuffer::reformat(u32 cols, u32 rows)
+{
+ if (cols == 0 || rows == 0)
+ {
+ // Clear formatted buffer
+ m_cols = 0;
+ m_rows = 0;
+ m_scroll = 0;
+ m_formatted.clear();
+ }
+ else if (cols != m_cols || rows != m_rows)
+ {
+ // TODO: Avoid reformatting ALL lines (even inivisble ones)
+ // each time the console size changes.
+
+ // Find out the scroll position in *unformatted* lines
+ u32 restore_scroll_unformatted = 0;
+ u32 restore_scroll_formatted = 0;
+ bool at_bottom = (m_scroll == getBottomScrollPos());
+ if (!at_bottom)
+ {
+ for (s32 i = 0; i < m_scroll; ++i)
+ {
+ if (m_formatted[i].first)
+ ++restore_scroll_unformatted;
+ }
+ }
+
+ // If number of columns change, reformat everything
+ if (cols != m_cols)
+ {
+ m_formatted.clear();
+ for (u32 i = 0; i < m_unformatted.size(); ++i)
+ {
+ if (i == restore_scroll_unformatted)
+ restore_scroll_formatted = m_formatted.size();
+ formatChatLine(m_unformatted[i], cols, m_formatted);
+ }
+ }
+
+ // Update the console size
+ m_cols = cols;
+ m_rows = rows;
+
+ // Restore the scroll position
+ if (at_bottom)
+ {
+ scrollBottom();
+ }
+ else
+ {
+ scrollAbsolute(restore_scroll_formatted);
+ }
+ }
+}
+
+const ChatFormattedLine& ChatBuffer::getFormattedLine(u32 row) const
+{
+ s32 index = m_scroll + (s32) row;
+ if (index >= 0 && index < (s32) m_formatted.size())
+ return m_formatted[index];
+ else
+ return m_empty_formatted_line;
+}
+
+void ChatBuffer::scroll(s32 rows)
+{
+ scrollAbsolute(m_scroll + rows);
+}
+
+void ChatBuffer::scrollAbsolute(s32 scroll)
+{
+ s32 top = getTopScrollPos();
+ s32 bottom = getBottomScrollPos();
+
+ m_scroll = scroll;
+ if (m_scroll < top)
+ m_scroll = top;
+ if (m_scroll > bottom)
+ m_scroll = bottom;
+}
+
+void ChatBuffer::scrollBottom()
+{
+ m_scroll = getBottomScrollPos();
+}
+
+void ChatBuffer::scrollTop()
+{
+ m_scroll = getTopScrollPos();
+}
+
+u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
+ core::array<ChatFormattedLine>& destination) const
+{
+ u32 num_added = 0;
+ core::array<ChatFormattedFragment> next_frags;
+ ChatFormattedLine next_line;
+ ChatFormattedFragment temp_frag;
+ u32 out_column = 0;
+ u32 in_pos = 0;
+ u32 hanging_indentation = 0;
+
+ // Format the sender name and produce fragments
+ if (!line.name.empty())
+ {
+ temp_frag.text = L"<";
+ temp_frag.column = 0;
+ //temp_frag.bold = 0;
+ next_frags.push_back(temp_frag);
+ temp_frag.text = line.name;
+ temp_frag.column = 0;
+ //temp_frag.bold = 1;
+ next_frags.push_back(temp_frag);
+ temp_frag.text = L"> ";
+ temp_frag.column = 0;
+ //temp_frag.bold = 0;
+ next_frags.push_back(temp_frag);
+ }
+
+ // Choose an indentation level
+ if (line.name.empty())
+ {
+ // Server messages
+ hanging_indentation = 0;
+ }
+ else if (line.name.size() + 3 <= cols/2)
+ {
+ // Names shorter than about half the console width
+ hanging_indentation = line.name.size() + 3;
+ }
+ else
+ {
+ // Very long names
+ hanging_indentation = 2;
+ }
+
+ next_line.first = true;
+ bool text_processing = false;
+
+ // Produce fragments and layout them into lines
+ while (!next_frags.empty() || in_pos < line.text.size())
+ {
+ // Layout fragments into lines
+ while (!next_frags.empty())
+ {
+ ChatFormattedFragment& frag = next_frags[0];
+ if (frag.text.size() <= cols - out_column)
+ {
+ // Fragment fits into current line
+ frag.column = out_column;
+ next_line.fragments.push_back(frag);
+ out_column += frag.text.size();
+ next_frags.erase(0, 1);
+ }
+ else
+ {
+ // Fragment does not fit into current line
+ // So split it up
+ temp_frag.text = frag.text.substr(0, cols - out_column);
+ temp_frag.column = out_column;
+ //temp_frag.bold = frag.bold;
+ next_line.fragments.push_back(temp_frag);
+ frag.text = frag.text.substr(cols - out_column);
+ out_column = cols;
+ }
+ if (out_column == cols || text_processing)
+ {
+ // End the current line
+ destination.push_back(next_line);
+ num_added++;
+ next_line.fragments.clear();
+ next_line.first = false;
+
+ out_column = text_processing ? hanging_indentation : 0;
+ }
+ }
+
+ // Produce fragment
+ if (in_pos < line.text.size())
+ {
+ u32 remaining_in_input = line.text.size() - in_pos;
+ u32 remaining_in_output = cols - out_column;
+
+ // Determine a fragment length <= the minimum of
+ // remaining_in_{in,out}put. Try to end the fragment
+ // on a word boundary.
+ u32 frag_length = 1, space_pos = 0;
+ while (frag_length < remaining_in_input &&
+ frag_length < remaining_in_output)
+ {
+ if (isspace(line.text[in_pos + frag_length]))
+ space_pos = frag_length;
+ ++frag_length;
+ }
+ if (space_pos != 0 && frag_length < remaining_in_input)
+ frag_length = space_pos + 1;
+
+ temp_frag.text = line.text.substr(in_pos, frag_length);
+ temp_frag.column = 0;
+ //temp_frag.bold = 0;
+ next_frags.push_back(temp_frag);
+ in_pos += frag_length;
+ text_processing = true;
+ }
+ }
+
+ // End the last line
+ if (num_added == 0 || !next_line.fragments.empty())
+ {
+ destination.push_back(next_line);
+ num_added++;
+ }
+
+ return num_added;
+}
+
+s32 ChatBuffer::getTopScrollPos() const
+{
+ s32 formatted_count = (s32) m_formatted.size();
+ s32 rows = (s32) m_rows;
+ if (rows == 0)
+ return 0;
+ else if (formatted_count <= rows)
+ return formatted_count - rows;
+ else
+ return 0;
+}
+
+s32 ChatBuffer::getBottomScrollPos() const
+{
+ s32 formatted_count = (s32) m_formatted.size();
+ s32 rows = (s32) m_rows;
+ if (rows == 0)
+ return 0;
+ else
+ return formatted_count - rows;
+}
+
+
+
+ChatPrompt::ChatPrompt(std::wstring prompt, u32 history_limit):
+ m_prompt(prompt),
+ m_line(L""),
+ m_history(),
+ m_history_index(0),
+ m_history_limit(history_limit),
+ m_cols(0),
+ m_view(0),
+ m_cursor(0),
+ m_nick_completion_start(0),
+ m_nick_completion_end(0)
+{
+}
+
+ChatPrompt::~ChatPrompt()
+{
+}
+
+void ChatPrompt::input(wchar_t ch)
+{
+ m_line.insert(m_cursor, 1, ch);
+ m_cursor++;
+ clampView();
+ m_nick_completion_start = 0;
+ m_nick_completion_end = 0;
+}
+
+std::wstring ChatPrompt::submit()
+{
+ std::wstring line = m_line;
+ m_line.clear();
+ if (!line.empty())
+ m_history.push_back(line);
+ if (m_history.size() > m_history_limit)
+ m_history.erase(0);
+ m_history_index = m_history.size();
+ m_view = 0;
+ m_cursor = 0;
+ m_nick_completion_start = 0;
+ m_nick_completion_end = 0;
+ return line;
+}
+
+void ChatPrompt::clear()
+{
+ m_line.clear();
+ m_view = 0;
+ m_cursor = 0;
+ m_nick_completion_start = 0;
+ m_nick_completion_end = 0;
+}
+
+void ChatPrompt::replace(std::wstring line)
+{
+ m_line = line;
+ m_view = m_cursor = line.size();
+ clampView();
+ m_nick_completion_start = 0;
+ m_nick_completion_end = 0;
+}
+
+void ChatPrompt::historyPrev()
+{
+ if (m_history_index != 0)
+ {
+ --m_history_index;
+ replace(m_history[m_history_index]);
+ }
+}
+
+void ChatPrompt::historyNext()
+{
+ if (m_history_index + 1 >= m_history.size())
+ {
+ m_history_index = m_history.size();
+ replace(L"");
+ }
+ else
+ {
+ ++m_history_index;
+ replace(m_history[m_history_index]);
+ }
+}
+
+void ChatPrompt::nickCompletion(const core::list<std::wstring>& names, bool backwards)
+{
+ // Two cases:
+ // (a) m_nick_completion_start == m_nick_completion_end == 0
+ // Then no previous nick completion is active.
+ // Get the word around the cursor and replace with any nick
+ // that has that word as a prefix.
+ // (b) else, continue a previous nick completion.
+ // m_nick_completion_start..m_nick_completion_end are the
+ // interval where the originally used prefix was. Cycle
+ // through the list of completions of that prefix.
+ u32 prefix_start = m_nick_completion_start;
+ u32 prefix_end = m_nick_completion_end;
+ bool initial = (prefix_end == 0);
+ if (initial)
+ {
+ // no previous nick completion is active
+ prefix_start = prefix_end = m_cursor;
+ while (prefix_start > 0 && !isspace(m_line[prefix_start-1]))
+ --prefix_start;
+ while (prefix_end < m_line.size() && !isspace(m_line[prefix_end]))
+ ++prefix_end;
+ if (prefix_start == prefix_end)
+ return;
+ }
+ std::wstring prefix = m_line.substr(prefix_start, prefix_end - prefix_start);
+
+ // find all names that start with the selected prefix
+ core::array<std::wstring> completions;
+ for (core::list<std::wstring>::ConstIterator
+ i = names.begin();
+ i != names.end(); i++)
+ {
+ if (str_starts_with(*i, prefix, true))
+ {
+ std::wstring completion = *i;
+ if (prefix_start == 0)
+ completion += L":";
+ completions.push_back(completion);
+ }
+ }
+ if (completions.empty())
+ return;
+
+ // find a replacement string and the word that will be replaced
+ u32 word_end = prefix_end;
+ u32 replacement_index = 0;
+ if (!initial)
+ {
+ while (word_end < m_line.size() && !isspace(m_line[word_end]))
+ ++word_end;
+ std::wstring word = m_line.substr(prefix_start, word_end - prefix_start);
+
+ // cycle through completions
+ for (u32 i = 0; i < completions.size(); ++i)
+ {
+ if (str_equal(word, completions[i], true))
+ {
+ if (backwards)
+ replacement_index = i + completions.size() - 1;
+ else
+ replacement_index = i + 1;
+ replacement_index %= completions.size();
+ break;
+ }
+ }
+ }
+ std::wstring replacement = completions[replacement_index] + L" ";
+ if (word_end < m_line.size() && isspace(word_end))
+ ++word_end;
+
+ // replace existing word with replacement word,
+ // place the cursor at the end and record the completion prefix
+ m_line.replace(prefix_start, word_end - prefix_start, replacement);
+ m_cursor = prefix_start + replacement.size();
+ clampView();
+ m_nick_completion_start = prefix_start;
+ m_nick_completion_end = prefix_end;
+}
+
+void ChatPrompt::reformat(u32 cols)
+{
+ if (cols <= m_prompt.size())
+ {
+ m_cols = 0;
+ m_view = m_cursor;
+ }
+ else
+ {
+ s32 length = m_line.size();
+ bool was_at_end = (m_view + m_cols >= length + 1);
+ m_cols = cols - m_prompt.size();
+ if (was_at_end)
+ m_view = length;
+ clampView();
+ }
+}
+
+std::wstring ChatPrompt::getVisiblePortion() const
+{
+ return m_prompt + m_line.substr(m_view, m_cols);
+}
+
+s32 ChatPrompt::getVisibleCursorPosition() const
+{
+ return m_cursor - m_view + m_prompt.size();
+}
+
+void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope)
+{
+ s32 old_cursor = m_cursor;
+ s32 new_cursor = m_cursor;
+
+ s32 length = m_line.size();
+ s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1;
+
+ if (scope == CURSOROP_SCOPE_CHARACTER)
+ {
+ new_cursor += increment;
+ }
+ else if (scope == CURSOROP_SCOPE_WORD)
+ {
+ if (increment > 0)
+ {
+ // skip one word to the right
+ while (new_cursor < length && isspace(m_line[new_cursor]))
+ new_cursor++;
+ while (new_cursor < length && !isspace(m_line[new_cursor]))
+ new_cursor++;
+ while (new_cursor < length && isspace(m_line[new_cursor]))
+ new_cursor++;
+ }
+ else
+ {
+ // skip one word to the left
+ while (new_cursor >= 1 && isspace(m_line[new_cursor - 1]))
+ new_cursor--;
+ while (new_cursor >= 1 && !isspace(m_line[new_cursor - 1]))
+ new_cursor--;
+ }
+ }
+ else if (scope == CURSOROP_SCOPE_LINE)
+ {
+ new_cursor += increment * length;
+ }
+
+ new_cursor = MYMAX(MYMIN(new_cursor, length), 0);
+
+ if (op == CURSOROP_MOVE)
+ {
+ m_cursor = new_cursor;
+ }
+ else if (op == CURSOROP_DELETE)
+ {
+ if (new_cursor < old_cursor)
+ {
+ m_line.erase(new_cursor, old_cursor - new_cursor);
+ m_cursor = new_cursor;
+ }
+ else if (new_cursor > old_cursor)
+ {
+ m_line.erase(old_cursor, new_cursor - old_cursor);
+ m_cursor = old_cursor;
+ }
+ }
+
+ clampView();
+
+ m_nick_completion_start = 0;
+ m_nick_completion_end = 0;
+}
+
+void ChatPrompt::clampView()
+{
+ s32 length = m_line.size();
+ if (length + 1 <= m_cols)
+ {
+ m_view = 0;
+ }
+ else
+ {
+ m_view = MYMIN(m_view, length + 1 - m_cols);
+ m_view = MYMIN(m_view, m_cursor);
+ m_view = MYMAX(m_view, m_cursor - m_cols + 1);
+ m_view = MYMAX(m_view, 0);
+ }
+}
+
+
+
+ChatBackend::ChatBackend():
+ m_console_buffer(500),
+ m_recent_buffer(6),
+ m_prompt(L"]", 500)
+{
+}
+
+ChatBackend::~ChatBackend()
+{
+}
+
+void ChatBackend::addMessage(std::wstring name, std::wstring text)
+{
+ // Note: A message may consist of multiple lines, for example the MOTD.
+ WStrfnd fnd(text);
+ while (!fnd.atend())
+ {
+ std::wstring line = fnd.next(L"\n");
+ m_console_buffer.addLine(name, line);
+ m_recent_buffer.addLine(name, line);
+ }
+}
+
+void ChatBackend::addUnparsedMessage(std::wstring message)
+{
+ // TODO: Remove the need to parse chat messages client-side, by sending
+ // separate name and text fields in TOCLIENT_CHAT_MESSAGE.
+
+ if (message.size() >= 2 && message[0] == L'<')
+ {
+ std::size_t closing = message.find_first_of(L'>', 1);
+ if (closing != std::wstring::npos &&
+ closing + 2 <= message.size() &&
+ message[closing+1] == L' ')
+ {
+ std::wstring name = message.substr(1, closing - 1);
+ std::wstring text = message.substr(closing + 2);
+ addMessage(name, text);
+ return;
+ }
+ }
+
+ // Unable to parse, probably a server message.
+ addMessage(L"", message);
+}
+
+ChatBuffer& ChatBackend::getConsoleBuffer()
+{
+ return m_console_buffer;
+}
+
+ChatBuffer& ChatBackend::getRecentBuffer()
+{
+ return m_recent_buffer;
+}
+
+std::wstring ChatBackend::getRecentChat()
+{
+ std::wostringstream stream;
+ for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i)
+ {
+ const ChatLine& line = m_recent_buffer.getLine(i);
+ if (i != 0)
+ stream << L"\n";
+ if (!line.name.empty())
+ stream << L"<" << line.name << L"> ";
+ stream << line.text;
+ }
+ return stream.str();
+}
+
+ChatPrompt& ChatBackend::getPrompt()
+{
+ return m_prompt;
+}
+
+void ChatBackend::reformat(u32 cols, u32 rows)
+{
+ m_console_buffer.reformat(cols, rows);
+
+ // no need to reformat m_recent_buffer, its formatted lines
+ // are not used
+
+ m_prompt.reformat(cols);
+}
+
+void ChatBackend::clearRecentChat()
+{
+ m_recent_buffer.clear();
+}
+
+void ChatBackend::step(float dtime)
+{
+ m_recent_buffer.step(dtime);
+ m_recent_buffer.deleteByAge(60.0);
+
+ // no need to age messages in anything but m_recent_buffer
+}
+
+void ChatBackend::scroll(s32 rows)
+{
+ m_console_buffer.scroll(rows);
+}
+
+void ChatBackend::scrollPageDown()
+{
+ m_console_buffer.scroll(m_console_buffer.getRows());
+}
+
+void ChatBackend::scrollPageUp()
+{
+ m_console_buffer.scroll(-m_console_buffer.getRows());
+}
diff --git a/src/chat.h b/src/chat.h
new file mode 100644
index 000000000..0e636ea43
--- /dev/null
+++ b/src/chat.h
@@ -0,0 +1,272 @@
+/*
+Minetest-c55
+Copyright (C) 2011 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 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 General Public License for more details.
+
+You should have received a copy of the GNU 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_HEADER
+#define CHAT_HEADER
+
+#include "common_irrlicht.h"
+#include <string>
+
+// Chat console related classes, only used by the client
+
+struct ChatLine
+{
+ // age in seconds
+ f32 age;
+ // name of sending player, or empty if sent by server
+ std::wstring name;
+ // message text
+ std::wstring text;
+
+ ChatLine(std::wstring a_name, std::wstring a_text):
+ age(0.0),
+ name(a_name),
+ text(a_text)
+ {
+ }
+};
+
+struct ChatFormattedFragment
+{
+ // text string
+ std::wstring text;
+ // starting column
+ u32 column;
+ // formatting
+ //u8 bold:1;
+};
+
+struct ChatFormattedLine
+{
+ // Array of text fragments
+ core::array<ChatFormattedFragment> fragments;
+ // true if first line of one formatted ChatLine
+ bool first;
+};
+
+class ChatBuffer
+{
+public:
+ ChatBuffer(u32 scrollback);
+ ~ChatBuffer();
+
+ // Append chat line
+ // Removes oldest chat line if scrollback size is reached
+ void addLine(std::wstring name, std::wstring text);
+
+ // Remove all chat lines
+ void clear();
+
+ // Get number of lines currently in buffer.
+ u32 getLineCount() const;
+ // Get scrollback size, maximum number of lines in buffer.
+ u32 getScrollback() const;
+ // Get reference to i-th chat line.
+ const ChatLine& getLine(u32 index) const;
+
+ // Increase each chat line's age by dtime.
+ void step(f32 dtime);
+ // Delete oldest N chat lines.
+ void deleteOldest(u32 count);
+ // Delete lines older than maxAge.
+ void deleteByAge(f32 maxAge);
+
+ // Get number of columns, 0 if reformat has not been called yet.
+ u32 getColumns() const;
+ // Get number of rows, 0 if reformat has not been called yet.
+ u32 getRows() const;
+ // Update console size and reformat all formatted lines.
+ void reformat(u32 cols, u32 rows);
+ // Get formatted line for a given row (0 is top of screen).
+ // Only valid after reformat has been called at least once
+ const ChatFormattedLine& getFormattedLine(u32 row) const;
+ // Scrolling in formatted buffer (relative)
+ // positive rows == scroll up, negative rows == scroll down
+ void scroll(s32 rows);
+ // Scrolling in formatted buffer (absolute)
+ void scrollAbsolute(s32 scroll);
+ // Scroll to bottom of buffer (newest)
+ void scrollBottom();
+ // Scroll to top of buffer (oldest)
+ void scrollTop();
+
+ // Format a chat line for the given number of columns.
+ // Appends the formatted lines to the destination array and
+ // returns the number of formatted lines.
+ u32 formatChatLine(const ChatLine& line, u32 cols,
+ core::array<ChatFormattedLine>& destination) const;
+
+protected:
+ s32 getTopScrollPos() const;
+ s32 getBottomScrollPos() const;
+
+private:
+ // Scrollback size
+ u32 m_scrollback;
+ // Array of unformatted chat lines
+ core::array<ChatLine> m_unformatted;
+
+ // Number of character columns in console
+ u32 m_cols;
+ // Number of character rows in console
+ u32 m_rows;
+ // Scroll position (console's top line index into m_formatted)
+ s32 m_scroll;
+ // Array of formatted lines
+ core::array<ChatFormattedLine> m_formatted;
+ // Empty formatted line, for error returns
+ ChatFormattedLine m_empty_formatted_line;
+};
+
+class ChatPrompt
+{
+public:
+ ChatPrompt(std::wstring prompt, u32 history_limit);
+ ~ChatPrompt();
+
+ // Input character
+ void input(wchar_t ch);
+
+ // Submit, clear and return current line
+ std::wstring submit();
+
+ // Clear the current line
+ void clear();
+
+ // Replace the current line with the given text
+ void replace(std::wstring line);
+
+ // Select previous command from history
+ void historyPrev();
+ // Select next command from history
+ void historyNext();
+
+ // Nick completion
+ void nickCompletion(const core::list<std::wstring>& names, bool backwards);
+
+ // Update console size and reformat the visible portion of the prompt
+ void reformat(u32 cols);
+ // Get visible portion of the prompt.
+ std::wstring getVisiblePortion() const;
+ // Get cursor position (relative to visible portion). -1 if invalid
+ s32 getVisibleCursorPosition() const;
+
+ // Cursor operations
+ enum CursorOp {
+ CURSOROP_MOVE,
+ CURSOROP_DELETE
+ };
+
+ // Cursor operation direction
+ enum CursorOpDir {
+ CURSOROP_DIR_LEFT,
+ CURSOROP_DIR_RIGHT
+ };
+
+ // Cursor operation scope
+ enum CursorOpScope {
+ CURSOROP_SCOPE_CHARACTER,
+ CURSOROP_SCOPE_WORD,
+ CURSOROP_SCOPE_LINE
+ };
+
+ // Cursor operation
+ // op specifies whether it's a move or delete operation
+ // dir specifies whether the operation goes left or right
+ // scope specifies how far the operation will reach (char/word/line)
+ // Examples:
+ // cursorOperation(CURSOROP_MOVE, CURSOROP_DIR_RIGHT, CURSOROP_SCOPE_LINE)
+ // moves the cursor to the end of the line.
+ // cursorOperation(CURSOROP_DELETE, CURSOROP_DIR_LEFT, CURSOROP_SCOPE_WORD)
+ // deletes the word to the left of the cursor.
+ void cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope);
+
+protected:
+ // set m_view to ensure that 0 <= m_view <= m_cursor < m_view + m_cols
+ // if line can be fully shown, set m_view to zero
+ // else, also ensure m_view <= m_line.size() + 1 - m_cols
+ void clampView();
+
+private:
+ // Prompt prefix
+ std::wstring m_prompt;
+ // Currently edited line
+ std::wstring m_line;
+ // History buffer
+ core::array<std::wstring> m_history;
+ // History index (0 <= m_history_index <= m_history.size())
+ u32 m_history_index;
+ // Maximum number of history entries
+ u32 m_history_limit;
+
+ // Number of columns excluding columns reserved for the prompt
+ s32 m_cols;
+ // Start of visible portion (index into m_line)
+ s32 m_view;
+ // Cursor (index into m_line)
+ s32 m_cursor;
+
+ // Last nick completion start (index into m_line)
+ s32 m_nick_completion_start;
+ // Last nick completion start (index into m_line)
+ s32 m_nick_completion_end;
+};
+
+class ChatBackend
+{
+public:
+ ChatBackend();
+ ~ChatBackend();
+
+ // Add chat message
+ void addMessage(std::wstring name, std::wstring text);
+ // Parse and add unparsed chat message
+ void addUnparsedMessage(std::wstring line);
+
+ // Get the console buffer
+ ChatBuffer& getConsoleBuffer();
+ // Get the recent messages buffer
+ ChatBuffer& getRecentBuffer();
+ // Concatenate all recent messages
+ std::wstring getRecentChat();
+ // Get the console prompt
+ ChatPrompt& getPrompt();
+
+ // Reformat all buffers
+ void reformat(u32 cols, u32 rows);
+
+ // Clear all recent messages
+ void clearRecentChat();
+
+ // Age recent messages
+ void step(float dtime);
+
+ // Scrolling
+ void scroll(s32 rows);
+ void scrollPageDown();
+ void scrollPageUp();
+
+private:
+ ChatBuffer m_console_buffer;
+ ChatBuffer m_recent_buffer;
+ ChatPrompt m_prompt;
+};
+
+#endif
+
diff --git a/src/client.cpp b/src/client.cpp
index 72cd28b18..14f93a1a1 100644
--- a/src/client.cpp
+++ b/src/client.cpp
@@ -2036,7 +2036,21 @@ void Client::printDebugInfo(std::ostream &os)
//<<", m_opt_not_found_history.size()="<<m_opt_not_found_history.size()
<<std::endl;*/
}
-
+
+core::list<std::wstring> Client::getConnectedPlayerNames()
+{
+ core::list<Player*> players = m_env.getPlayers(true);
+ core::list<std::wstring> playerNames;
+ for(core::list<Player*>::Iterator
+ i = players.begin();
+ i != players.end(); i++)
+ {
+ Player *player = *i;
+ playerNames.push_back(narrow_to_wide(player->getName()));
+ }
+ return playerNames;
+}
+
u32 Client::getDayNightRatio()
{
//JMutexAutoLock envlock(m_env_mutex); //bulk comment-out
@@ -2084,6 +2098,39 @@ void Client::clearTempMod(v3s16 p)
}
}
+bool Client::getChatMessage(std::wstring &message)
+{
+ if(m_chat_queue.size() == 0)
+ return false;
+ message = m_chat_queue.pop_front();
+ return true;
+}
+
+void Client::typeChatMessage(const std::wstring &message)
+{
+ // Discard empty line
+ if(message == L"")
+ return;
+
+ // Send to others
+ sendChatMessage(message);
+
+ // Show locally
+ if (message[0] == L'/')
+ {
+ m_chat_queue.push_back(
+ (std::wstring)L"issued command: "+message);
+ }
+ else
+ {
+ LocalPlayer *player = m_env.getLocalPlayer();
+ assert(player != NULL);
+ std::wstring name = narrow_to_wide(player->getName());
+ m_chat_queue.push_back(
+ (std::wstring)L"<"+name+L"> "+message);
+ }
+}
+
void Client::addUpdateMeshTask(v3s16 p, bool ack_to_server)
{
/*infostream<<"Client::addUpdateMeshTask(): "
diff --git a/src/client.h b/src/client.h
index efdf315f7..4b16b717c 100644
--- a/src/client.h
+++ b/src/client.h
@@ -258,6 +258,8 @@ public:
// Prints a line or two of info
void printDebugInfo(std::ostream &os);
+ core::list<std::wstring> getConnectedPlayerNames();
+
u32 getDayNightRatio();
u16 getHP();
@@ -274,29 +276,8 @@ public:
}
}
- bool getChatMessage(std::wstring &message)
- {
- if(m_chat_queue.size() == 0)
- return false;
- message = m_chat_queue.pop_front();
- return true;
- }
-
- void addChatMessage(const std::wstring &message)
- {
- if (message[0] == L'/') {
- m_chat_queue.push_back(
- (std::wstring)L"issued command: "+message);
- return;
- }
-
- //JMutexAutoLock envlock(m_env_mutex); //bulk comment-out
- LocalPlayer *player = m_env.getLocalPlayer();
- assert(player != NULL);
- std::wstring name = narrow_to_wide(player->getName());
- m_chat_queue.push_back(
- (std::wstring)L"<"+name+L"> "+message);
- }
+ bool getChatMessage(std::wstring &message);
+ void typeChatMessage(const std::wstring& message);
u64 getMapSeed(){ return m_map_seed; }
diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp
index 6c611d672..23199eef4 100644
--- a/src/defaultsettings.cpp
+++ b/src/defaultsettings.cpp
@@ -40,6 +40,7 @@ void set_default_settings(Settings *settings)
settings->setDefault("keymap_special1", "KEY_KEY_E");
settings->setDefault("keymap_chat", "KEY_KEY_T");
settings->setDefault("keymap_cmd", "/");
+ settings->setDefault("keymap_console", "KEY_F10");
settings->setDefault("keymap_rangeselect", "KEY_KEY_R");
settings->setDefault("keymap_freemove", "KEY_KEY_K");
settings->setDefault("keymap_fastmove", "KEY_KEY_J");
@@ -91,7 +92,8 @@ void set_default_settings(Settings *settings)
settings->setDefault("view_bobbing_amount", "1.0");
settings->setDefault("enable_3d_clouds", "false");
settings->setDefault("opaque_water", "false");
-
+ settings->setDefault("console_color", "(0,0,0)");
+ settings->setDefault("console_alpha", "200");
// Server stuff
// "map-dir" doesn't exist by default.
settings->setDefault("motd", "");
diff --git a/src/game.cpp b/src/game.cpp
index 96f834341..616d05865 100644
--- a/src/game.cpp
+++ b/src/game.cpp
@@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "guiTextInputMenu.h"
#include "guiDeathScreen.h"
#include "tool.h"
+#include "guiChatConsole.h"
#include "config.h"
#include "clouds.h"
#include "camera.h"
@@ -62,22 +63,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#define FIELD_OF_VIEW_TEST 0
-// Chat data
-struct ChatLine
-{
- ChatLine():
- age(0.0)
- {
- }
- ChatLine(const std::wstring &a_text):
- age(0.0),
- text(a_text)
- {
- }
- float age;
- std::wstring text;
-};
-
/*
Text input system
*/
@@ -90,14 +75,7 @@ struct TextDestChat : public TextDest
}
void gotText(std::wstring text)
{
- // Discard empty line
- if(text == L"")
- return;
-
- // Send to others
- m_client->sendChatMessage(text);
- // Show locally
- m_client->addChatMessage(text);
+ m_client->typeChatMessage(text);
}
Client *m_client;
@@ -676,7 +654,8 @@ void the_game(
std::string address,
u16 port,
std::wstring &error_message,
- std::string configpath
+ std::string configpath,
+ ChatBackend &chat_backend
)
{
video::IVideoDriver* driver = device->getVideoDriver();
@@ -978,8 +957,10 @@ void the_game(
core::rect<s32>(0,0,0,0),
//false, false); // Disable word wrap as of now
false, true);
- //guitext_chat->setBackgroundColor(video::SColor(96,0,0,0));
- core::list<ChatLine> chat_lines;
+ // Remove stale "recent" chat messages from previous connections
+ chat_backend.clearRecentChat();
+ // Chat backend and console
+ GUIChatConsole *gui_chat_console = new GUIChatConsole(guienv, guienv->getRootGUIElement(), -1, &chat_backend, &client);
// Profiler text (size is updated when text is updated)
gui::IGUIStaticText *guitext_profiler = guienv->addStaticText(
@@ -1299,7 +1280,9 @@ void the_game(
*/
// Reset input if window not active or some menu is active
- if(device->isWindowActive() == false || noMenuActive() == false)
+ if(device->isWindowActive() == false
+ || noMenuActive() == false
+ || guienv->hasFocus(gui_chat_console))
{
input->clear();
}
@@ -1375,6 +1358,15 @@ void the_game(
&g_menumgr, dest,
L"/"))->drop();
}
+ else if(input->wasKeyDown(getKeySetting("keymap_console")))
+ {
+ if (!gui_chat_console->isOpenInhibited())
+ {
+ // Open up to over half of the screen
+ gui_chat_console->openConsole(0.6);
+ guienv->setFocus(gui_chat_console);
+ }
+ }
else if(input->wasKeyDown(getKeySetting("keymap_freemove")))
{
if(g_settings->getBool("free_move"))
@@ -1655,23 +1647,6 @@ void the_game(
/*
Player speed control
*/
-
- if(!noMenuActive() || !device->isWindowActive())
- {
- PlayerControl control(
- false,
- false,
- false,
- false,
- false,
- false,
- false,
- camera_pitch,
- camera_yaw
- );
- client.setPlayerControl(control);
- }
- else
{
/*bool a_up,
bool a_down,
@@ -1758,6 +1733,8 @@ void the_game(
&g_menumgr, respawner);
menu->drop();
+ chat_backend.addMessage(L"", L"You died.");
+
/* Handle visualization */
damage_flash_timer = 0;
@@ -2357,83 +2334,38 @@ void the_game(
// Get new messages from error log buffer
while(!chat_log_error_buf.empty())
{
- chat_lines.push_back(ChatLine(narrow_to_wide(
- chat_log_error_buf.get())));
+ chat_backend.addMessage(L"", narrow_to_wide(
+ chat_log_error_buf.get()));
}
// Get new messages from client
std::wstring message;
while(client.getChatMessage(message))
{
- chat_lines.push_back(ChatLine(message));
- /*if(chat_lines.size() > 6)
- {
- core::list<ChatLine>::Iterator
- i = chat_lines.begin();
- chat_lines.erase(i);
- }*/
+ chat_backend.addUnparsedMessage(message);
}
- // Append them to form the whole static text and throw
- // it to the gui element
- std::wstring whole;
- // This will correspond to the line number counted from
- // top to bottom, from size-1 to 0
- s16 line_number = chat_lines.size();
- // Count of messages to be removed from the top
- u16 to_be_removed_count = 0;
- for(core::list<ChatLine>::Iterator
- i = chat_lines.begin();
- i != chat_lines.end(); i++)
- {
- // After this, line number is valid for this loop
- line_number--;
- // Increment age
- (*i).age += dtime;
- /*
- This results in a maximum age of 60*6 to the
- lowermost line and a maximum of 6 lines
- */
- float allowed_age = (6-line_number) * 60.0;
-
- if((*i).age > allowed_age)
- {
- to_be_removed_count++;
- continue;
- }
- whole += (*i).text + L'\n';
- }
- for(u16 i=0; i<to_be_removed_count; i++)
- {
- core::list<ChatLine>::Iterator
- it = chat_lines.begin();
- chat_lines.erase(it);
- }
- guitext_chat->setText(whole.c_str());
-
- // Update gui element size and position
+ // Remove old messages
+ chat_backend.step(dtime);
- /*core::rect<s32> rect(
- 10,
- screensize.Y - guitext_chat_pad_bottom
- - text_height*chat_lines.size(),
- screensize.X - 10,
- screensize.Y - guitext_chat_pad_bottom
- );*/
+ // Display all messages in a static text element
+ u32 recent_chat_count = chat_backend.getRecentBuffer().getLineCount();
+ std::wstring recent_chat = chat_backend.getRecentChat();
+ guitext_chat->setText(recent_chat.c_str());
+ // Update gui element size and position
s32 chat_y = 5+(text_height+5);
if(show_debug)
chat_y += (text_height+5);
core::rect<s32> rect(
- 10,
- chat_y,
- screensize.X - 10,
- chat_y + guitext_chat->getTextHeight()
+ 10,
+ chat_y,
+ screensize.X - 10,
+ chat_y + guitext_chat->getTextHeight()
);
-
guitext_chat->setRelativePosition(rect);
- // Don't show chat if empty or profiler or debug is enabled
- guitext_chat->setVisible(chat_lines.size() != 0
- && show_chat && show_profiler == 0);
+ // Don't show chat if disabled or empty or profiler is enabled
+ guitext_chat->setVisible(show_chat && recent_chat_count != 0
+ && !show_profiler);
}
/*
@@ -2634,6 +2566,8 @@ void the_game(
*/
if(clouds)
clouds->drop();
+ if(gui_chat_console)
+ gui_chat_console->drop();
/*
Draw a "shutting down" screen, which will be shown while the map
@@ -2648,6 +2582,9 @@ void the_game(
gui_shuttingdowntext->remove();*/
}
+ chat_backend.addMessage(L"", L"# Disconnected.");
+ chat_backend.addMessage(L"", L"");
+
} // Client scope (must be destructed before destructing *def and tsrc
delete tsrc;
diff --git a/src/game.h b/src/game.h
index a9db6c3e1..01e955ecd 100644
--- a/src/game.h
+++ b/src/game.h
@@ -122,6 +122,8 @@ public:
virtual void clear() {};
};
+class ChatBackend; /* to avoid having to include chat.h */
+
void the_game(
bool &kill,
bool random_input,
@@ -134,7 +136,8 @@ void the_game(
std::string address,
u16 port,
std::wstring &error_message,
- std::string configpath
+ std::string configpath,
+ ChatBackend &chat_backend
);
#endif
diff --git a/src/guiChatConsole.cpp b/src/guiChatConsole.cpp
new file mode 100644
index 000000000..d11a50e20
--- /dev/null
+++ b/src/guiChatConsole.cpp
@@ -0,0 +1,550 @@
+/*
+Minetest-c55
+Copyright (C) 2011 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 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 General Public License for more details.
+
+You should have received a copy of the GNU 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 "guiChatConsole.h"
+#include "chat.h"
+#include "client.h"
+#include "debug.h"
+#include "gettime.h"
+#include "keycode.h"
+#include "settings.h"
+#include "main.h" // for g_settings
+#include "porting.h"
+#include "tile.h"
+#include "IGUIFont.h"
+#include <string>
+
+#include "gettext.h"
+
+inline u32 clamp_u8(s32 value)
+{
+ return (u32) MYMIN(MYMAX(value, 0), 255);
+}
+
+
+GUIChatConsole::GUIChatConsole(
+ gui::IGUIEnvironment* env,
+ gui::IGUIElement* parent,
+ s32 id,
+ ChatBackend* backend,
+ Client* client
+):
+ IGUIElement(gui::EGUIET_ELEMENT, env, parent, id,
+ core::rect<s32>(0,0,100,100)),
+ m_chat_backend(backend),
+ m_client(client),
+ m_screensize(v2u32(0,0)),
+ m_animate_time_old(0),
+ m_open(false),
+ m_height(0),
+ m_desired_height(0),
+ m_desired_height_fraction(0.0),
+ m_height_speed(5.0),
+ m_open_inhibited(0),
+ m_cursor_blink(0.0),
+ m_cursor_blink_speed(0.0),
+ m_cursor_height(0.0),
+ m_background(NULL),
+ m_background_color(255, 0, 0, 0),
+ m_font(NULL),
+ m_fontsize(0, 0)
+{
+ m_animate_time_old = getTimeMs();
+
+ // load background settings
+ bool console_color_set = !g_settings->get("console_color").empty();
+ s32 console_alpha = g_settings->getS32("console_alpha");
+
+ // load the background texture depending on settings
+ m_background_color.setAlpha(clamp_u8(console_alpha));
+ if (console_color_set)
+ {
+ v3f console_color = g_settings->getV3F("console_color");
+ m_background_color.setRed(clamp_u8(myround(console_color.X)));
+ m_background_color.setGreen(clamp_u8(myround(console_color.Y)));
+ m_background_color.setBlue(clamp_u8(myround(console_color.Z)));
+ }
+ else
+ {
+ m_background = env->getVideoDriver()->getTexture(getTexturePath("background_chat.jpg").c_str());
+ m_background_color.setRed(255);
+ m_background_color.setGreen(255);
+ m_background_color.setBlue(255);
+ }
+
+ // load the font
+ // FIXME should a custom texture_path be searched too?
+ std::string font_name = "fontdejavusansmono.png";
+ m_font = env->getFont(getTexturePath(font_name).c_str());
+ if (m_font == NULL)
+ {
+ dstream << "Unable to load font: " << font_name << std::endl;
+ }
+ else
+ {
+ core::dimension2d<u32> dim = m_font->getDimension(L"M");
+ m_fontsize = v2u32(dim.Width, dim.Height);
+ dstream << "Font size: " << m_fontsize.X << " " << m_fontsize.Y << std::endl;
+ }
+ m_fontsize.X = MYMAX(m_fontsize.X, 1);
+ m_fontsize.Y = MYMAX(m_fontsize.Y, 1);
+
+ // set default cursor options
+ setCursor(true, true, 2.0, 0.1);
+}
+
+GUIChatConsole::~GUIChatConsole()
+{
+}
+
+void GUIChatConsole::openConsole(f32 height)
+{
+ m_open = true;
+ m_desired_height_fraction = height;
+ m_desired_height = height * m_screensize.Y;
+ reformatConsole();
+}
+
+bool GUIChatConsole::isOpenInhibited() const
+{
+ return m_open_inhibited > 0;
+}
+
+void GUIChatConsole::closeConsole()
+{
+ m_open = false;
+}
+
+void GUIChatConsole::closeConsoleAtOnce()
+{
+ m_open = false;
+ m_height = 0;
+ recalculateConsolePosition();
+}
+
+f32 GUIChatConsole::getDesiredHeight() const
+{
+ return m_desired_height_fraction;
+}
+
+void GUIChatConsole::setCursor(
+ bool visible, bool blinking, f32 blink_speed, f32 relative_height)
+{
+ if (visible)
+ {
+ if (blinking)
+ {
+ // leave m_cursor_blink unchanged
+ m_cursor_blink_speed = blink_speed;
+ }
+ else
+ {
+ m_cursor_blink = 0x8000; // on
+ m_cursor_blink_speed = 0.0;
+ }
+ }
+ else
+ {
+ m_cursor_blink = 0; // off
+ m_cursor_blink_speed = 0.0;
+ }
+ m_cursor_height = relative_height;
+}
+
+void GUIChatConsole::draw()
+{
+ if(!IsVisible)
+ return;
+
+ video::IVideoDriver* driver = Environment->getVideoDriver();
+
+ // Check screen size
+ v2u32 screensize = driver->getScreenSize();
+ if (screensize != m_screensize)
+ {
+ // screen size has changed
+ // scale current console height to new window size
+ if (m_screensize.Y != 0)
+ m_height = m_height * screensize.Y / m_screensize.Y;
+ m_desired_height = m_desired_height_fraction * m_screensize.Y;
+ m_screensize = screensize;
+ reformatConsole();
+ }
+
+ // Animation
+ u32 now = getTimeMs();
+ animate(now - m_animate_time_old);
+ m_animate_time_old = now;
+
+ // Draw console elements if visible
+ if (m_height > 0)
+ {
+ drawBackground();
+ drawText();
+ drawPrompt();
+ }
+
+ gui::IGUIElement::draw();
+}
+
+void GUIChatConsole::reformatConsole()
+{
+ s32 cols = m_screensize.X / m_fontsize.X - 2; // make room for a margin (looks better)
+ s32 rows = m_desired_height / m_fontsize.Y - 1; // make room for the input prompt
+ if (cols <= 0 || rows <= 0)
+ cols = rows = 0;
+ m_chat_backend->reformat(cols, rows);
+}
+
+void GUIChatConsole::recalculateConsolePosition()
+{
+ core::rect<s32> rect(0, 0, m_screensize.X, m_height);
+ DesiredRect = rect;
+ recalculateAbsolutePosition(false);
+}
+
+void GUIChatConsole::animate(u32 msec)
+{
+ // animate the console height
+ s32 goal = m_open ? m_desired_height : 0;
+ if (m_height != goal)
+ {
+ s32 max_change = msec * m_screensize.Y * (m_height_speed / 1000.0);
+ if (max_change == 0)
+ max_change = 1;
+
+ if (m_height < goal)
+ {
+ // increase height
+ if (m_height + max_change < goal)
+ m_height += max_change;
+ else
+ m_height = goal;
+ }
+ else
+ {
+ // decrease height
+ if (m_height > goal + max_change)
+ m_height -= max_change;
+ else
+ m_height = goal;
+ }
+
+ recalculateConsolePosition();
+ }
+
+ // blink the cursor
+ if (m_cursor_blink_speed != 0.0)
+ {
+ u32 blink_increase = 0x10000 * msec * (m_cursor_blink_speed / 1000.0);
+ if (blink_increase == 0)
+ blink_increase = 1;
+ m_cursor_blink = ((m_cursor_blink + blink_increase) & 0xffff);
+ }
+
+ // decrease open inhibit counter
+ if (m_open_inhibited > msec)
+ m_open_inhibited -= msec;
+ else
+ m_open_inhibited = 0;
+}
+
+void GUIChatConsole::drawBackground()
+{
+ video::IVideoDriver* driver = Environment->getVideoDriver();
+ if (m_background != NULL)
+ {
+ core::rect<s32> sourcerect(0, -m_height, m_screensize.X, 0);
+ driver->draw2DImage(
+ m_background,
+ v2s32(0, 0),
+ sourcerect,
+ &AbsoluteClippingRect,
+ m_background_color,
+ false);
+ }
+ else
+ {
+ driver->draw2DRectangle(
+ m_background_color,
+ core::rect<s32>(0, 0, m_screensize.X, m_height),
+ &AbsoluteClippingRect);
+ }
+}
+
+void GUIChatConsole::drawText()
+{
+ if (m_font == NULL)
+ return;
+
+ ChatBuffer& buf = m_chat_backend->getConsoleBuffer();
+ for (u32 row = 0; row < buf.getRows(); ++row)
+ {
+ const ChatFormattedLine& line = buf.getFormattedLine(row);
+ if (line.fragments.empty())
+ continue;
+
+ s32 line_height = m_fontsize.Y;
+ s32 y = row * line_height + m_height - m_desired_height;
+ if (y + line_height < 0)
+ continue;
+
+ for (u32 i = 0; i < line.fragments.size(); ++i)
+ {
+ const ChatFormattedFragment& fragment = line.fragments[i];
+ s32 x = (fragment.column + 1) * m_fontsize.X;
+ core::rect<s32> destrect(
+ x, y, x + m_fontsize.X * fragment.text.size(), y + m_fontsize.Y);
+ m_font->draw(
+ fragment.text.c_str(),
+ destrect,
+ video::SColor(255, 255, 255, 255),
+ false,
+ false,
+ &AbsoluteClippingRect);
+ }
+ }
+}
+
+void GUIChatConsole::drawPrompt()
+{
+ if (m_font == NULL)
+ return;
+
+ u32 row = m_chat_backend->getConsoleBuffer().getRows();
+ s32 line_height = m_fontsize.Y;
+ s32 y = row * line_height + m_height - m_desired_height;
+
+ ChatPrompt& prompt = m_chat_backend->getPrompt();
+ std::wstring prompt_text = prompt.getVisiblePortion();
+
+ // FIXME Draw string at once, not character by character
+ // That will only work with the cursor once we have a monospace font
+ for (u32 i = 0; i < prompt_text.size(); ++i)
+ {
+ wchar_t ws[2] = {prompt_text[i], 0};
+ s32 x = (1 + i) * m_fontsize.X;
+ core::rect<s32> destrect(
+ x, y, x + m_fontsize.X, y + m_fontsize.Y);
+ m_font->draw(
+ ws,
+ destrect,
+ video::SColor(255, 255, 255, 255),
+ false,
+ false,
+ &AbsoluteClippingRect);
+ }
+
+ // Draw the cursor during on periods
+ if ((m_cursor_blink & 0x8000) != 0)
+ {
+ s32 cursor_pos = prompt.getVisibleCursorPosition();
+ if (cursor_pos >= 0)
+ {
+ video::IVideoDriver* driver = Environment->getVideoDriver();
+ s32 x = (1 + cursor_pos) * m_fontsize.X;
+ core::rect<s32> destrect(
+ x,
+ y + (1.0-m_cursor_height) * m_fontsize.Y,
+ x + m_fontsize.X,
+ y + m_fontsize.Y);
+ video::SColor cursor_color(255,255,255,255);
+ driver->draw2DRectangle(
+ cursor_color,
+ destrect,
+ &AbsoluteClippingRect);
+ }
+ }
+
+}
+
+bool GUIChatConsole::OnEvent(const SEvent& event)
+{
+ if(event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown)
+ {
+ // Key input
+ if(KeyPress(event.KeyInput) == getKeySetting("keymap_console"))
+ {
+ closeConsole();
+ Environment->removeFocus(this);
+
+ // inhibit open so the_game doesn't reopen immediately
+ m_open_inhibited = 50;
+ return true;
+ }
+ else if(event.KeyInput.Key == KEY_ESCAPE)
+ {
+ closeConsoleAtOnce();
+ Environment->removeFocus(this);
+ // the_game will open the pause menu
+ return true;
+ }
+ else if(event.KeyInput.Key == KEY_PRIOR)
+ {
+ m_chat_backend->scrollPageUp();
+ return true;
+ }
+ else if(event.KeyInput.Key == KEY_NEXT)
+ {
+ m_chat_backend->scrollPageDown();
+ return true;
+ }
+ else if(event.KeyInput.Key == KEY_RETURN)
+ {
+ std::wstring text = m_chat_backend->getPrompt().submit();
+ m_client->typeChatMessage(text);
+ return true;
+ }
+ else if(event.KeyInput.Key == KEY_UP)
+ {
+ // Up pressed
+ // Move back in history
+ m_chat_backend->getPrompt().historyPrev();
+ return true;
+ }
+ else if(event.KeyInput.Key == KEY_DOWN)
+ {
+ // Down pressed
+ // Move forward in history
+ m_chat_backend->getPrompt().historyNext();
+ return true;
+ }
+ else if(event.KeyInput.Key == KEY_LEFT)
+ {
+ // Left or Ctrl-Left pressed
+ // move character / word to the left
+ ChatPrompt::CursorOpScope scope =
+ event.KeyInput.Control ?
+ ChatPrompt::CURSOROP_SCOPE_WORD :
+ ChatPrompt::CURSOROP_SCOPE_CHARACTER;
+ m_chat_backend->getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_MOVE,
+ ChatPrompt::CURSOROP_DIR_LEFT,
+ scope);
+ return true;
+ }
+ else if(event.KeyInput.Key == KEY_RIGHT)
+ {
+ // Right or Ctrl-Right pressed
+ // move character / word to the right
+ ChatPrompt::CursorOpScope scope =
+ event.KeyInput.Control ?
+ ChatPrompt::CURSOROP_SCOPE_WORD :
+ ChatPrompt::CURSOROP_SCOPE_CHARACTER;
+ m_chat_backend->getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_MOVE,
+ ChatPrompt::CURSOROP_DIR_RIGHT,
+ scope);
+ return true;
+ }
+ else if(event.KeyInput.Key == 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);
+ return true;
+ }
+ else if(event.KeyInput.Key == 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);
+ return true;
+ }
+ else if(event.KeyInput.Key == KEY_BACK)
+ {
+ // Backspace or Ctrl-Backspace pressed
+ // delete character / word to the left
+ ChatPrompt::CursorOpScope scope =
+ event.KeyInput.Control ?
+ ChatPrompt::CURSOROP_SCOPE_WORD :
+ ChatPrompt::CURSOROP_SCOPE_CHARACTER;
+ m_chat_backend->getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_DELETE,
+ ChatPrompt::CURSOROP_DIR_LEFT,
+ scope);
+ return true;
+ }
+ else if(event.KeyInput.Key == KEY_DELETE)
+ {
+ // Delete or Ctrl-Delete pressed
+ // delete character / word to the right
+ ChatPrompt::CursorOpScope scope =
+ event.KeyInput.Control ?
+ ChatPrompt::CURSOROP_SCOPE_WORD :
+ ChatPrompt::CURSOROP_SCOPE_CHARACTER;
+ m_chat_backend->getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_DELETE,
+ ChatPrompt::CURSOROP_DIR_RIGHT,
+ scope);
+ return true;
+ }
+ else if(event.KeyInput.Key == KEY_KEY_U && event.KeyInput.Control)
+ {
+ // Ctrl-U pressed
+ // kill line to left end
+ m_chat_backend->getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_DELETE,
+ ChatPrompt::CURSOROP_DIR_LEFT,
+ ChatPrompt::CURSOROP_SCOPE_LINE);
+ return true;
+ }
+ else if(event.KeyInput.Key == KEY_KEY_K && event.KeyInput.Control)
+ {
+ // Ctrl-K pressed
+ // kill line to right end
+ m_chat_backend->getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_DELETE,
+ ChatPrompt::CURSOROP_DIR_RIGHT,
+ ChatPrompt::CURSOROP_SCOPE_LINE);
+ return true;
+ }
+ else if(event.KeyInput.Key == KEY_TAB)
+ {
+ // Tab or Shift-Tab pressed
+ // Nick completion
+ core::list<std::wstring> names = m_client->getConnectedPlayerNames();
+ bool backwards = event.KeyInput.Shift;
+ m_chat_backend->getPrompt().nickCompletion(names, backwards);
+ return true;
+ }
+ else if(event.KeyInput.Char != 0 && !event.KeyInput.Control)
+ {
+ m_chat_backend->getPrompt().input(event.KeyInput.Char);
+ return true;
+ }
+ }
+ else if(event.EventType == EET_MOUSE_INPUT_EVENT)
+ {
+ if(event.MouseInput.Event == EMIE_MOUSE_WHEEL)
+ {
+ s32 rows = myround(-3.0 * event.MouseInput.Wheel);
+ m_chat_backend->scroll(rows);
+ }
+ }
+
+ return Parent ? Parent->OnEvent(event) : false;
+}
+
diff --git a/src/guiChatConsole.h b/src/guiChatConsole.h
new file mode 100644
index 000000000..2b78b9e34
--- /dev/null
+++ b/src/guiChatConsole.h
@@ -0,0 +1,125 @@
+/*
+Minetest-c55
+Copyright (C) 2011 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 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 General Public License for more details.
+
+You should have received a copy of the GNU 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 GUICHATCONSOLE_HEADER
+#define GUICHATCONSOLE_HEADER
+
+#include "common_irrlicht.h"
+#include "chat.h"
+
+class Client;
+
+class GUIChatConsole : public gui::IGUIElement
+{
+public:
+ GUIChatConsole(gui::IGUIEnvironment* env,
+ gui::IGUIElement* parent,
+ s32 id,
+ ChatBackend* backend,
+ Client* client);
+ virtual ~GUIChatConsole();
+
+ // Open the console (height = desired fraction of screen size)
+ // This doesn't open immediately but initiates an animation.
+ // You should call isOpenInhibited() before this.
+ void openConsole(f32 height);
+ // Check if the console should not be opened at the moment
+ // This is to avoid reopening the console immediately after closing
+ bool isOpenInhibited() const;
+ // Close the console, equivalent to openConsole(0).
+ // This doesn't close immediately but initiates an animation.
+ void closeConsole();
+ // Close the console immediately, without animation.
+ void closeConsoleAtOnce();
+
+ // Return the desired height (fraction of screen size)
+ // Zero if the console is closed or getting closed
+ f32 getDesiredHeight() const;
+
+ // Change how the cursor looks
+ void setCursor(
+ bool visible,
+ bool blinking = false,
+ f32 blink_speed = 1.0,
+ f32 relative_height = 1.0);
+
+ // Irrlicht draw method
+ virtual void draw();
+
+ bool canTakeFocus(gui::IGUIElement* element) { return false; }
+
+ virtual bool OnEvent(const SEvent& event);
+
+private:
+ void reformatConsole();
+ void recalculateConsolePosition();
+
+ // These methods are called by draw
+ void animate(u32 msec);
+ void drawBackground();
+ void drawText();
+ void drawPrompt();
+
+private:
+ // pointer to the chat backend
+ ChatBackend* m_chat_backend;
+
+ // pointer to the client
+ Client* m_client;
+
+ // current screen size
+ v2u32 m_screensize;
+
+ // used to compute how much time passed since last animate()
+ u32 m_animate_time_old;
+
+ // should the console be opened or closed?
+ bool m_open;
+ // current console height [pixels]
+ s32 m_height;
+ // desired height [pixels]
+ f32 m_desired_height;
+ // desired height [screen height fraction]
+ f32 m_desired_height_fraction;
+ // console open/close animation speed [screen height fraction / second]
+ f32 m_height_speed;
+ // if nonzero, opening the console is inhibited [milliseconds]
+ u32 m_open_inhibited;
+
+ // cursor blink frame (16-bit value)
+ // cursor is off during [0,32767] and on during [32768,65535]
+ u32 m_cursor_blink;
+ // cursor blink speed [on/off toggles / second]
+ f32 m_cursor_blink_speed;
+ // cursor height [line height]
+ f32 m_cursor_height;
+
+ // background texture
+ video::ITexture* m_background;
+ // background color (including alpha)
+ video::SColor m_background_color;
+
+ // font
+ gui::IGUIFont* m_font;
+ v2u32 m_fontsize;
+};
+
+
+#endif
+
diff --git a/src/guiKeyChangeMenu.cpp b/src/guiKeyChangeMenu.cpp
index 01f583a01..4e04fccf0 100644
--- a/src/guiKeyChangeMenu.cpp
+++ b/src/guiKeyChangeMenu.cpp
@@ -261,7 +261,20 @@ void GUIKeyChangeMenu::regenerateGui(v2u32 screensize)
this->cmd = Environment->addButton(rect, this, GUI_ID_KEY_CMD_BUTTON,
wgettext(key_cmd.name()));
}
+ offset += v2s32(0, 25);
+ {
+ core::rect < s32 > rect(0, 0, 100, 20);
+ rect += topleft + v2s32(offset.X, offset.Y);
+ Environment->addStaticText(wgettext("Console"), rect, false, true, this, -1);
+ //t->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_UPPERLEFT);
+ }
+ {
+ core::rect < s32 > rect(0, 0, 100, 30);
+ rect += topleft + v2s32(offset.X + 105, offset.Y - 5);
+ this->console = Environment->addButton(rect, this, GUI_ID_KEY_CONSOLE_BUTTON,
+ wgettext(key_console.name()));
+ }
//next col
offset = v2s32(250, 40);
@@ -371,6 +384,7 @@ bool GUIKeyChangeMenu::acceptInput()
g_settings->set("keymap_inventory", key_inventory.sym());
g_settings->set("keymap_chat", key_chat.sym());
g_settings->set("keymap_cmd", key_cmd.sym());
+ g_settings->set("keymap_console", key_console.sym());
g_settings->set("keymap_rangeselect", key_range.sym());
g_settings->set("keymap_freemove", key_fly.sym());
g_settings->set("keymap_fastmove", key_fast.sym());
@@ -391,6 +405,7 @@ void GUIKeyChangeMenu::init_keys()
key_inventory = getKeySetting("keymap_inventory");
key_chat = getKeySetting("keymap_chat");
key_cmd = getKeySetting("keymap_cmd");
+ key_console = getKeySetting("keymap_console");
key_range = getKeySetting("keymap_rangeselect");
key_fly = getKeySetting("keymap_freemove");
key_fast = getKeySetting("keymap_fastmove");
@@ -437,6 +452,9 @@ bool GUIKeyChangeMenu::resetMenu()
case GUI_ID_KEY_CMD_BUTTON:
this->cmd->setText(wgettext(key_cmd.name()));
break;
+ case GUI_ID_KEY_CONSOLE_BUTTON:
+ this->console->setText(wgettext(key_console.name()));
+ break;
case GUI_ID_KEY_RANGE_BUTTON:
this->range->setText(wgettext(key_range.name()));
break;
@@ -516,6 +534,11 @@ bool GUIKeyChangeMenu::OnEvent(const SEvent& event)
this->cmd->setText(wgettext(kp.name()));
this->key_cmd = kp;
}
+ else if (activeKey == GUI_ID_KEY_CONSOLE_BUTTON)
+ {
+ this->console->setText(wgettext(kp.name()));
+ this->key_console = kp;
+ }
else if (activeKey == GUI_ID_KEY_RANGE_BUTTON)
{
this->range->setText(wgettext(kp.name()));
@@ -630,6 +653,11 @@ bool GUIKeyChangeMenu::OnEvent(const SEvent& event)
activeKey = event.GUIEvent.Caller->getID();
this->cmd->setText(wgettext("press Key"));
break;
+ case GUI_ID_KEY_CONSOLE_BUTTON:
+ resetMenu();
+ activeKey = event.GUIEvent.Caller->getID();
+ this->console->setText(wgettext("press Key"));
+ break;
case GUI_ID_KEY_SNEAK_BUTTON:
resetMenu();
activeKey = event.GUIEvent.Caller->getID();
diff --git a/src/guiKeyChangeMenu.h b/src/guiKeyChangeMenu.h
index a3d8b4743..9772bde3c 100644
--- a/src/guiKeyChangeMenu.h
+++ b/src/guiKeyChangeMenu.h
@@ -44,6 +44,7 @@ enum
GUI_ID_KEY_JUMP_BUTTON,
GUI_ID_KEY_CHAT_BUTTON,
GUI_ID_KEY_CMD_BUTTON,
+ GUI_ID_KEY_CONSOLE_BUTTON,
GUI_ID_KEY_SNEAK_BUTTON,
GUI_ID_KEY_DROP_BUTTON,
GUI_ID_KEY_INVENTORY_BUTTON,
@@ -91,6 +92,7 @@ private:
gui::IGUIButton *dump;
gui::IGUIButton *chat;
gui::IGUIButton *cmd;
+ gui::IGUIButton *console;
s32 activeKey;
KeyPress key_forward;
@@ -107,6 +109,7 @@ private:
KeyPress key_range;
KeyPress key_chat;
KeyPress key_cmd;
+ KeyPress key_console;
KeyPress key_dump;
};
diff --git a/src/main.cpp b/src/main.cpp
index 1b7331ee2..10e01be2a 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -403,6 +403,7 @@ Doing currently:
#include "game.h"
#include "keycode.h"
#include "tile.h"
+#include "chat.h"
#include "defaultsettings.h"
#include "gettext.h"
#include "settings.h"
@@ -940,20 +941,20 @@ void drawMenuBackground(video::IVideoDriver* driver)
driver->getTexture(getTexturePath("menubg.png").c_str());
if(bgtexture)
{
- s32 texturesize = 128;
- s32 tiled_y = screensize.Height / texturesize + 1;
- s32 tiled_x = screensize.Width / texturesize + 1;
+ s32 scaledsize = 128;
- for(s32 y=0; y<tiled_y; y++)
- for(s32 x=0; x<tiled_x; x++)
- {
- core::rect<s32> rect(0,0,texturesize,texturesize);
- rect += v2s32(x*texturesize, y*texturesize);
- driver->draw2DImage(bgtexture, rect,
- core::rect<s32>(core::position2d<s32>(0,0),
- core::dimension2di(bgtexture->getSize())),
- NULL, NULL, true);
- }
+ // The important difference between destsize and screensize is
+ // that destsize is rounded to whole scaled pixels.
+ // These formulas use component-wise multiplication and division of v2u32.
+ v2u32 texturesize = bgtexture->getSize();
+ v2u32 sourcesize = texturesize * screensize / scaledsize + v2u32(1,1);
+ v2u32 destsize = scaledsize * sourcesize / texturesize;
+
+ // Default texture wrapping mode in Irrlicht is ETC_REPEAT.
+ driver->draw2DImage(bgtexture,
+ core::rect<s32>(0, 0, destsize.X, destsize.Y),
+ core::rect<s32>(0, 0, sourcesize.X, sourcesize.Y),
+ NULL, NULL, true);
}
video::ITexture *logotexture =
@@ -1479,6 +1480,8 @@ int main(int argc, char *argv[])
GUI stuff
*/
+ ChatBackend chat_backend;
+
/*
If an error occurs, this is set to something and the
menu-game loop is restarted. It is then displayed before
@@ -1663,7 +1666,8 @@ int main(int argc, char *argv[])
address,
port,
error_message,
- configpath
+ configpath,
+ chat_backend
);
} //try
diff --git a/src/utility.h b/src/utility.h
index f4c7c3017..aa64c28bb 100644
--- a/src/utility.h
+++ b/src/utility.h
@@ -694,6 +694,46 @@ private:
u32 *m_result;
};
+// Tests if two strings are equal, optionally case insensitive
+inline bool str_equal(const std::wstring& s1, const std::wstring& s2,
+ bool case_insensitive = false)
+{
+ if(case_insensitive)
+ {
+ if(s1.size() != s2.size())
+ return false;
+ for(size_t i = 0; i < s1.size(); ++i)
+ if(tolower(s1[i]) != tolower(s2[i]))
+ return false;
+ return true;
+ }
+ else
+ {
+ return s1 == s2;
+ }
+}
+
+// Tests if the second string is a prefix of the first, optionally case insensitive
+inline bool str_starts_with(const std::wstring& str, const std::wstring& prefix,
+ bool case_insensitive = false)
+{
+ if(str.size() < prefix.size())
+ return false;
+ if(case_insensitive)
+ {
+ for(size_t i = 0; i < prefix.size(); ++i)
+ if(tolower(str[i]) != tolower(prefix[i]))
+ return false;
+ }
+ else
+ {
+ for(size_t i = 0; i < prefix.size(); ++i)
+ if(str[i] != prefix[i])
+ return false;
+ }
+ return true;
+}
+
// Calculates the borders of a "d-radius" cube
inline void getFacePositions(core::list<v3s16> &list, u16 d)
{
@@ -1566,6 +1606,15 @@ inline std::string wrap_rows(const std::string &from, u32 rowlen)
#define MYMAX(a,b) ((a)>(b)?(a):(b))
/*
+ Returns nearest 32-bit integer for given floating point number.
+ <cmath> and <math.h> in VC++ don't provide round().
+*/
+inline s32 myround(f32 f)
+{
+ return floor(f + 0.5);
+}
+
+/*
Returns integer position of node in given floating point position
*/
inline v3s16 floatToInt(v3f p, f32 d)