Browse Source

Add missing chat commands and keyboard shortcuts

kiwec 2 months ago
parent
commit
6226b0ad74

+ 23 - 0
src/App/Osu/BanchoUsers.cpp

@@ -13,6 +13,29 @@ UserInfo* find_user(UString username) {
     return NULL;
 }
 
+UserInfo* find_user_starting_with(UString prefix, UString last_match) {
+    bool matched = last_match.length() == 0;
+    for(auto pair : online_users) {
+        auto user = pair.second;
+        if(!matched) {
+            if(user->name == last_match) {
+                matched = true;
+            }
+            continue;
+        }
+
+        if(user->name.startsWithIgnoreCase(prefix)) {
+            return user;
+        }
+    }
+
+    if(last_match.length() == 0) {
+        return NULL;
+    } else {
+        return find_user_starting_with(prefix, "");
+    }
+}
+
 UserInfo* get_user_info(u32 user_id, bool fetch) {
     auto it = online_users.find(user_id);
     if(it != online_users.end()) {

+ 1 - 0
src/App/Osu/BanchoUsers.h

@@ -36,4 +36,5 @@ extern std::unordered_map<u32, UserInfo*> online_users;
 extern std::vector<u32> friends;
 
 UserInfo* find_user(UString username);
+UserInfo* find_user_starting_with(UString prefix, UString last_match);
 UserInfo* get_user_info(u32 user_id, bool fetch = false);

+ 2 - 0
src/App/Osu/Changelog.cpp

@@ -30,6 +30,8 @@ Changelog::Changelog() : ScreenBackable() {
     latest.title =
         UString::format("%.2f (%s, %s)", convar->getConVarByName("osu_version")->getFloat(), __DATE__, __TIME__);
     latest.changes.push_back("- Added setting to prevent servers from replacing the main menu logo");
+    latest.changes.push_back("- Chat: added missing chat commands");
+    latest.changes.push_back("- Chat: added missing keyboard shortcuts");
     latest.changes.push_back("- Chat: added support for user links");
     latest.changes.push_back("- Chat: improved map link support");
     latest.changes.push_back("- Fixed freeze when switching between songs in song browser");

+ 304 - 29
src/App/Osu/Chat.cpp

@@ -5,10 +5,12 @@
 #include "AnimationHandler.h"
 #include "Bancho.h"
 #include "BanchoNetworking.h"
+#include "BanchoUsers.h"
 #include "Beatmap.h"
 #include "CBaseUIButton.h"
 #include "CBaseUIContainer.h"
 #include "CBaseUILabel.h"
+#include "CBaseUITextbox.h"
 #include "ChatLink.h"
 #include "Engine.h"
 #include "Font.h"
@@ -23,6 +25,7 @@
 #include "ResourceManager.h"
 #include "RoomScreen.h"
 #include "Skin.h"
+#include "SongBrowser/ScoreButton.h"
 #include "SongBrowser/SongBrowser.h"
 #include "SoundEngine.h"
 #include "SpectatorScreen.h"
@@ -340,9 +343,215 @@ void Chat::mouse_update(bool *propagate_clicks) {
     m_input_box->focus(false);
 }
 
+void Chat::handle_command(UString msg) {
+    if(msg == UString("/clear")) {
+        m_selected_channel->messages.clear();
+        updateLayout(osu->getScreenSize());
+        return;
+    }
+
+    if(msg == UString("/close") || msg == UString("/p") || msg == UString("/part")) {
+        leave(m_selected_channel->name);
+        return;
+    }
+
+    if(msg == UString("/help") || msg == UString("/keys")) {
+        env->openURLInDefaultBrowser("https://osu.ppy.sh/wiki/en/Client/Interface/Chat_console");
+        return;
+    }
+
+    if(msg == UString("/np")) {
+        auto diff = osu->getSelectedBeatmap()->getSelectedDifficulty2();
+        if(diff == NULL) {
+            addSystemMessage("You are not listening to anything.");
+            return;
+        }
+
+        UString song_name = UString::format("%s - %s [%s]", diff->getArtist().c_str(), diff->getTitle().c_str(), diff->getDifficultyName().c_str());
+        UString song_link = UString::format("[https://osu.%s/beatmaps/%d %s]", bancho.endpoint.toUtf8(), diff->getID(), song_name.toUtf8());
+
+        UString np_msg;
+        if(osu->isInPlayMode()) {
+            np_msg = UString::format("\001ACTION is playing %s", song_link.toUtf8());
+
+            auto mod_string = ScoreButton::getModsStringForDisplay(osu->getScore()->getModsLegacy());
+            if(mod_string.length() > 0) {
+                np_msg.append("(+");
+                np_msg.append(mod_string);
+                np_msg.append(")");
+            }
+
+            np_msg.append("\001");
+        } else {
+            np_msg = UString::format("\001ACTION is listening to %s\001", song_link.toUtf8());
+        }
+
+        send_message(np_msg);
+        return;
+    }
+
+    if(msg.startsWith("/addfriend ")) {
+        auto friend_name = msg.substr(11);
+        auto user = find_user(friend_name);
+        if(!user) {
+            addSystemMessage(UString::format("User '%s' not found. Are they online?", friend_name.toUtf8()));
+            return;
+        }
+
+        if(user->is_friend()) {
+            addSystemMessage(UString::format("You are already friends with %s!", friend_name.toUtf8()));
+        } else {
+            Packet packet;
+            packet.id = FRIEND_ADD;
+            write<u32>(&packet, user->user_id);
+            send_packet(packet);
+
+            friends.push_back(user->user_id);
+
+            addSystemMessage(UString::format("You are now friends with %s.", friend_name.toUtf8()));
+        }
+
+        return;
+    }
+
+    if(msg.startsWith("/bb ")) {
+        addChannel("BanchoBot", true);
+        send_message(msg.substr(4));
+        return;
+    }
+
+    if(msg == UString("/away")) {
+        away_msg = "";
+        addSystemMessage("Away message removed.");
+        return;
+    }
+    if(msg.startsWith("/away ")) {
+        away_msg = msg.substr(6);
+        addSystemMessage(UString::format("Away message set to '%s'.", away_msg.toUtf8()));
+        return;
+    }
+
+    if(msg.startsWith("/delfriend ")) {
+        auto friend_name = msg.substr(11);
+        auto user = find_user(friend_name);
+        if(!user) {
+            addSystemMessage(UString::format("User '%s' not found. Are they online?", friend_name.toUtf8()));
+            return;
+        }
+
+        if(user->is_friend()) {
+            Packet packet;
+            packet.id = FRIEND_REMOVE;
+            write<u32>(&packet, user->user_id);
+            send_packet(packet);
+
+            auto it = std::find(friends.begin(), friends.end(), user->user_id);
+            if(it != friends.end()) {
+                friends.erase(it);
+            }
+
+            addSystemMessage(UString::format("You are no longer friends with %s.", friend_name.toUtf8()));
+        } else {
+            addSystemMessage(UString::format("You aren't friends with %s!", friend_name.toUtf8()));
+        }
+
+        return;
+    }
+
+    if(msg.startsWith("/me ")) {
+        auto new_text = msg.substr(3);
+        new_text.insert(0, "\001ACTION");
+        new_text.append("\001");
+        send_message(new_text);
+        return;
+    }
+
+    if(msg.startsWith("/chat ") || msg.startsWith("/msg ") || msg.startsWith("/query ")) {
+        auto username = msg.substr(msg.find(L" "));
+        addChannel(username, true);
+        return;
+    }
+
+    if(msg.startsWith("/invite ")) {
+        if(!bancho.is_in_a_multi_room()) {
+            addSystemMessage("You are not in a multiplayer room!");
+            return;
+        }
+
+        auto username = msg.substr(8);
+        auto invite_msg = UString::format("\001ACTION has invited you to join [osump://%d/%s %s]\001", bancho.room.id, bancho.room.password.toUtf8(), bancho.room.name.toUtf8());
+
+        Packet packet;
+        packet.id = SEND_PRIVATE_MESSAGE;
+        write_string(&packet, (char *)bancho.username.toUtf8());
+        write_string(&packet, (char *)invite_msg.toUtf8());
+        write_string(&packet, (char *)username.toUtf8());
+        write<u32>(&packet, bancho.user_id);
+        send_packet(packet);
+
+        addSystemMessage(UString::format("%s has been invited to the game.", username.toUtf8()));
+        return;
+    }
+
+    if(msg.startsWith("/j ") || msg.startsWith("/join ")) {
+        auto channel = msg.substr(msg.find(L" "));
+        join(channel);
+        return;
+    }
+
+    if(msg.startsWith("/p ") || msg.startsWith("/part ")) {
+        auto channel = msg.substr(msg.find(L" "));
+        leave(channel);
+        return;
+    }
+
+    addSystemMessage("This command is not supported.");
+}
+
 void Chat::onKeyDown(KeyboardEvent &key) {
     if(!m_bVisible) return;
 
+    if(engine->getKeyboard()->isAltDown()) {
+        i32 tab_select = -1;
+        if(key.getKeyCode() == KEY_1) tab_select = 0;
+        if(key.getKeyCode() == KEY_2) tab_select = 1;
+        if(key.getKeyCode() == KEY_3) tab_select = 2;
+        if(key.getKeyCode() == KEY_4) tab_select = 3;
+        if(key.getKeyCode() == KEY_5) tab_select = 4;
+        if(key.getKeyCode() == KEY_6) tab_select = 5;
+        if(key.getKeyCode() == KEY_7) tab_select = 6;
+        if(key.getKeyCode() == KEY_8) tab_select = 7;
+        if(key.getKeyCode() == KEY_9) tab_select = 8;
+        if(key.getKeyCode() == KEY_0) tab_select = 9;
+
+        if(tab_select != -1) {
+            if(tab_select >= m_channels.size()) {
+                key.consume();
+                return;
+            }
+
+            key.consume();
+            switchToChannel(m_channels[tab_select]);
+            return;
+        }
+    }
+
+    if(key.getKeyCode() == KEY_PAGEUP) {
+        if(m_selected_channel != NULL) {
+            key.consume();
+            m_selected_channel->ui->scrollY(getSize().y - input_box_height);
+            return;
+        }
+    }
+
+    if(key.getKeyCode() == KEY_PAGEDOWN) {
+        if(m_selected_channel != NULL) {
+            key.consume();
+            m_selected_channel->ui->scrollY(-(getSize().y - input_box_height));
+            return;
+        }
+    }
+
     // Escape: close chat
     if(key.getKeyCode() == KEY_ESCAPE) {
         if(isVisibilityForced()) return;
@@ -357,38 +566,17 @@ void Chat::onKeyDown(KeyboardEvent &key) {
     if(key.getKeyCode() == KEY_ENTER) {
         key.consume();
         if(m_selected_channel != NULL && m_input_box->getText().length() > 0) {
-            if(m_input_box->getText() == UString("/close")) {
-                leave(m_selected_channel->name);
-                return;
-            }
-
-            if(m_input_box->getText().startsWith("/me ")) {
-                auto new_text = m_input_box->getText().substr(3);
-                new_text.insert(0, "\001ACTION");
-                new_text.append("\001");
-                m_input_box->setText(new_text);
+            if(m_input_box->getText()[0] == L'/') {
+                handle_command(m_input_box->getText());
+            } else {
+                send_message(m_input_box->getText());
             }
 
-            Packet packet;
-            packet.id = m_selected_channel->name[0] == '#' ? SEND_PUBLIC_MESSAGE : SEND_PRIVATE_MESSAGE;
-            write_string(&packet, (char *)bancho.username.toUtf8());
-            write_string(&packet, (char *)m_input_box->getText().toUtf8());
-            write_string(&packet, (char *)m_selected_channel->name.toUtf8());
-            write<u32>(&packet, bancho.user_id);
-            send_packet(packet);
-
-            // Server doesn't echo the message back
-            addMessage(m_selected_channel->name, ChatMessage{
-                                                     .tms = time(NULL),
-                                                     .author_id = bancho.user_id,
-                                                     .author_name = bancho.username,
-                                                     .text = m_input_box->getText(),
-                                                 });
-
             engine->getSound()->play(osu->getSkin()->m_messageSent);
-
             m_input_box->clear();
         }
+        tab_completion_prefix = "";
+        tab_completion_match = "";
         return;
     }
 
@@ -429,8 +617,48 @@ void Chat::onKeyDown(KeyboardEvent &key) {
         return;
     }
 
+    // TAB: Complete nickname
+    // KEY_TAB doesn't work on Linux
+    if(key.getKeyCode() == 65056 || key.getKeyCode() == KEY_TAB) {
+        key.consume();
+
+        auto text = m_input_box->getText();
+        i32 username_start_idx = text.findLast(" ", 0, m_input_box->m_iCaretPosition) + 1;
+        i32 username_end_idx = m_input_box->m_iCaretPosition;
+        i32 username_len = username_end_idx - username_start_idx;
+
+        if(tab_completion_prefix.length() == 0) {
+            tab_completion_prefix = text.substr(username_start_idx, username_len);
+        } else {
+            username_start_idx = m_input_box->m_iCaretPosition - tab_completion_match.length();
+            username_len = username_end_idx - username_start_idx;
+        }
+
+        auto user = find_user_starting_with(tab_completion_prefix, tab_completion_match);
+        if(user) {
+            tab_completion_match = user->name;
+
+            // Remove current username, add new username
+            m_input_box->m_sText.erase(m_input_box->m_iCaretPosition - username_len, username_len);
+            m_input_box->m_iCaretPosition -= username_len;
+            m_input_box->m_sText.insert(m_input_box->m_iCaretPosition, tab_completion_match);
+            m_input_box->m_iCaretPosition += tab_completion_match.length();
+            m_input_box->setText(m_input_box->m_sText);
+            m_input_box->updateTextPos();
+            m_input_box->tickCaret();
+
+            Sound *sounds[] = {osu->getSkin()->m_typing1, osu->getSkin()->m_typing2, osu->getSkin()->m_typing3,
+                               osu->getSkin()->m_typing4};
+            engine->getSound()->play(sounds[rand() % 4]);
+        }
+
+        return;
+    }
+
     // Typing in chat: capture keypresses
     if(!engine->getKeyboard()->isAltDown()) {
+        tab_completion_prefix = "";
+        tab_completion_match = "";
         m_input_box->onKeyDown(key);
         key.consume();
         return;
@@ -520,7 +748,8 @@ void Chat::addChannel(UString channel_name, bool switch_to) {
 }
 
 void Chat::addMessage(UString channel_name, ChatMessage msg, bool mark_unread) {
-    if(msg.author_id > 0 && channel_name[0] != '#' && msg.author_name != bancho.username) {
+    bool is_pm = msg.author_id > 0 && channel_name[0] != '#' && msg.author_name != bancho.username;
+    if(is_pm) {
         // If it's a PM, the channel title should be the one who sent the message
         channel_name = msg.author_name;
     }
@@ -543,10 +772,38 @@ void Chat::addMessage(UString channel_name, ChatMessage msg, bool mark_unread) {
         if(chan->messages.size() > 100) {
             chan->messages.erase(chan->messages.begin());
         }
-        return;
+
+        break;
+    }
+
+    if(is_pm && away_msg.length() > 0) {
+        Packet packet;
+        packet.id = SEND_PRIVATE_MESSAGE;
+        write_string(&packet, (char *)bancho.username.toUtf8());
+        write_string(&packet, (char *)away_msg.toUtf8());
+        write_string(&packet, (char *)msg.author_name.toUtf8());
+        write<u32>(&packet, bancho.user_id);
+        send_packet(packet);
+
+        // Server doesn't echo the message back
+        addMessage(channel_name, ChatMessage{
+                                     .tms = time(NULL),
+                                     .author_id = bancho.user_id,
+                                     .author_name = bancho.username,
+                                     .text = away_msg,
+                                 });
     }
 }
 
+void Chat::addSystemMessage(UString msg) {
+    addMessage(m_selected_channel->name, ChatMessage{
+                                             .tms = time(NULL),
+                                             .author_id = 0,
+                                             .author_name = "",
+                                             .text = msg,
+                                         });
+}
+
 void Chat::removeChannel(UString channel_name) {
     ChatChannel *chan = NULL;
     for(auto c : m_channels) {
@@ -689,6 +946,24 @@ void Chat::leave(UString channel_name) {
     engine->getSound()->play(osu->getSkin()->m_closeChatTab);
 }
 
+void Chat::send_message(UString msg) {
+    Packet packet;
+    packet.id = m_selected_channel->name[0] == '#' ? SEND_PUBLIC_MESSAGE : SEND_PRIVATE_MESSAGE;
+    write_string(&packet, (char *)bancho.username.toUtf8());
+    write_string(&packet, (char *)msg.toUtf8());
+    write_string(&packet, (char *)m_selected_channel->name.toUtf8());
+    write<u32>(&packet, bancho.user_id);
+    send_packet(packet);
+
+    // Server doesn't echo the message back
+    addMessage(m_selected_channel->name, ChatMessage{
+                                             .tms = time(NULL),
+                                             .author_id = bancho.user_id,
+                                             .author_name = bancho.username,
+                                             .text = msg,
+                                         });
+}
+
 void Chat::onDisconnect() {
     for(auto chan : m_channels) {
         delete chan;

+ 7 - 0
src/App/Osu/Chat.h

@@ -49,12 +49,15 @@ class Chat : public OsuScreen {
     void switchToChannel(ChatChannel *chan);
     void addChannel(UString channel_name, bool switch_to = false);
     void addMessage(UString channel_name, ChatMessage msg, bool mark_unread = true);
+    void addSystemMessage(UString msg);
     void removeChannel(UString channel_name);
     void updateLayout(Vector2 newResolution);
     void updateButtonLayout(Vector2 screen);
 
     void join(UString channel_name);
     void leave(UString channel_name);
+    void handle_command(UString msg);
+    void send_message(UString msg);
     void onDisconnect();
 
     virtual CBaseUIContainer *setVisible(bool visible);
@@ -79,4 +82,8 @@ class Chat : public OsuScreen {
 
     const float input_box_height = 30.f;
     const float button_height = 26.f;
+
+    UString away_msg;
+    UString tab_completion_prefix;
+    UString tab_completion_match;
 };

+ 0 - 47
src/GUI/CBaseUITextbox.cpp

@@ -268,51 +268,6 @@ void CBaseUITextbox::mouse_update(bool *propagate_clicks) {
     // handle context menu
     if(!mright && m_bContextMouse && isMouseInside()) {
         m_bContextMouse = false;
-        /*
-        engine->getMouse()->setCursorType(CURSORTYPE::CURSOR_NORMAL);
-        cmenu->begin();
-        {
-                cmenu->addItem("Clear", 5);
-                cmenu->addSeparator();
-                cmenu->addItem("Paste", 4);
-
-                if (hasSelectedText())
-                {
-                        cmenu->addItem("Copy", 3);
-                        cmenu->addItem("Cut", 2);
-                        cmenu->addSeparator();
-                        cmenu->addItem("Delete", 1);
-                }
-        }
-        const int item = cmenu->end();
-
-        switch (item)
-        {
-        case 5: // clear
-                clear();
-                break;
-
-        case 4: // paste
-                handleDeleteSelectedText();
-                insertTextFromClipboard();
-                break;
-
-        case 3: // copy
-                //envDebugLog("selected copy text: %s\n",getSelectedText().toUtf8());
-                env->setClipBoardText(getSelectedText());
-                break;
-
-        case 2: // cut
-                env->setClipBoardText(getSelectedText());
-                handleDeleteSelectedText();
-                //envDebugLog("selected cut text: %s\n",getSelectedText().toUtf8());
-                break;
-
-        case 1: // delete
-                handleDeleteSelectedText();
-                break;
-        }
-        */
     }
 }
 
@@ -658,9 +613,7 @@ void CBaseUITextbox::updateTextPos() {
     if(m_iTextJustification == 0) {
         if((m_iTextAddX + m_fTextScrollAddX) > ui_textbox_text_offset_x.getInt()) {
             if(hasSelectedText() && m_iCaretPosition == 0) {
-                // TODO: animations? don't like it as it is
                 m_fTextScrollAddX = ui_textbox_text_offset_x.getInt() - m_iTextAddX;
-                /// animation->moveSmoothEnd(&m_fTextScrollAddX, ui_textbox_text_offset_x.getInt() - m_iTextAddX, 1);
             } else
                 m_fTextScrollAddX = ui_textbox_text_offset_x.getInt() - m_iTextAddX;
         }

+ 5 - 5
src/GUI/CBaseUITextbox.h

@@ -82,6 +82,11 @@ class CBaseUITextbox : public CBaseUIElement {
 
     bool is_password = false;
 
+    UString m_sText;
+    int m_iCaretPosition;
+    void tickCaret();
+    void updateTextPos();
+
    protected:
     virtual void drawText(Graphics *g);
 
@@ -92,19 +97,15 @@ class CBaseUITextbox : public CBaseUIElement {
     virtual void onMouseUpOutside();
     virtual void onResized();
 
-    void tickCaret();
     void handleCaretKeyboardMove();
     void handleCaretKeyboardDelete();
     void updateCaretX();
 
     void handleDeleteSelectedText();
     void insertTextFromClipboard();
-    void updateTextPos();
     void deselectText();
     UString getSelectedText();
 
-    UString m_sText;
-
     McFont *m_font;
 
     Color m_textColor;
@@ -124,7 +125,6 @@ class CBaseUITextbox : public CBaseUIElement {
     int m_iTextAddX;
     int m_iTextAddY;
     float m_fTextScrollAddX;
-    int m_iCaretPosition;
     int m_iCaretX;
     int m_iCaretWidth;
     int m_iTextJustification;

+ 8 - 0
src/Util/UString.cpp

@@ -668,6 +668,14 @@ bool UString::endsWith(const UString &ustr) const {
     return true;
 }
 
+bool UString::startsWithIgnoreCase(const UString &ustr) const {
+    if(mLength < ustr.mLength) return false;
+    for(int i = 0; i < ustr.mLength; i++) {
+        if(std::towlower(mUnicode[i]) != std::towlower(ustr.mUnicode[i])) return false;
+    }
+    return true;
+}
+
 bool UString::equalsIgnoreCase(const UString &ustr) const {
     if(mLength != ustr.mLength) return false;
 

+ 1 - 0
src/Util/UString.h

@@ -90,6 +90,7 @@ class UString {
     bool operator<(const UString &ustr) const;
 
     bool startsWith(const UString &ustr) const;
+    bool startsWithIgnoreCase(const UString &ustr) const;
     bool endsWith(const UString &ustr) const;
     bool equalsIgnoreCase(const UString &ustr) const;
     bool lessThanIgnoreCase(const UString &ustr) const;