diff --git a/README.txt b/README.txt index c10fa7537..9002ab643 100644 --- a/README.txt +++ b/README.txt @@ -277,3 +277,21 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Fonts +--------------- + +DejaVu Sans Mono: + + Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. + Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) + + Bitstream Vera Fonts Copyright: + + Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is + a trademark of Bitstream, Inc. + + Arev Fonts Copyright: + + Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + + diff --git a/minetest.conf.example b/minetest.conf.example index fbc7249d2..8deee7bd0 100644 --- a/minetest.conf.example +++ b/minetest.conf.example @@ -42,6 +42,8 @@ # Go down ladder / go down in fly mode / go fast in fast mode #keymap_special1 = KEY_KEY_E #keymap_chat = KEY_KEY_T +#keymap_cmd = / +#keyman_console = KEY_F10 #keymap_rangeselect = KEY_KEY_R #keymap_freemove = KEY_KEY_K #keymap_fastmove = KEY_KEY_J @@ -104,6 +106,10 @@ #screenshot_path = . # Amount of view bobbing (0 = no view bobbing, 1.0 = normal, 2.0 = double) #view_bobbing_amount = 1.0 +# In-game chat console background color (R,G,B) +#console_color = (0,0,0) +# In-game chat console background alpha (opaqueness, between 0 and 255) +#console_alpha = 200 # # Server stuff diff --git a/share/client/textures/fontdejavusansmono.png b/share/client/textures/fontdejavusansmono.png new file mode 100644 index 000000000..416a74f8a Binary files /dev/null and b/share/client/textures/fontdejavusansmono.png differ 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 + +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 +#include +#include + +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& destination) const +{ + u32 num_added = 0; + core::array 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& 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 completions; + for (core::list::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 + +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 + +// 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 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& destination) const; + +protected: + s32 getTopScrollPos() const; + s32 getBottomScrollPos() const; + +private: + // Scrollback size + u32 m_scrollback; + // Array of unformatted chat lines + core::array 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 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& 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 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()="< Client::getConnectedPlayerNames() +{ + core::list players = m_env.getPlayers(true); + core::list playerNames; + for(core::list::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 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(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 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::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::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; + // Remove old messages + chat_backend.step(dtime); - if((*i).age > allowed_age) - { - to_be_removed_count++; - continue; - } - whole += (*i).text + L'\n'; - } - for(u16 i=0; i::Iterator - it = chat_lines.begin(); - chat_lines.erase(it); - } - guitext_chat->setText(whole.c_str()); + // 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 - - /*core::rect rect( - 10, - screensize.Y - guitext_chat_pad_bottom - - text_height*chat_lines.size(), - screensize.X - 10, - screensize.Y - guitext_chat_pad_bottom - );*/ - s32 chat_y = 5+(text_height+5); if(show_debug) chat_y += (text_height+5); core::rect 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 + +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 + +#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(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 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 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 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(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 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 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 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 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 + +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 rect(0,0,texturesize,texturesize); - rect += v2s32(x*texturesize, y*texturesize); - driver->draw2DImage(bgtexture, rect, - core::rect(core::position2d(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(0, 0, destsize.X, destsize.Y), + core::rect(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 &list, u16 d) { @@ -1565,6 +1605,15 @@ inline std::string wrap_rows(const std::string &from, u32 rowlen) #define MYMIN(a,b) ((a)<(b)?(a):(b)) #define MYMAX(a,b) ((a)>(b)?(a):(b)) +/* + Returns nearest 32-bit integer for given floating point number. + and 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 */