Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiplayer: Created anti-griefing "PeerOptions UI" (mute chat, mute/hide actors etc...) #3215

Merged
merged 6 commits into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions source/main/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,8 @@ const char* MsgTypeToString(MsgType type)
case MSG_NET_REFRESH_REPOLIST_FAILURE : return "MSG_NET_REFRESH_REPOLIST_FAILURE";
case MSG_NET_FETCH_AI_PRESETS_SUCCESS : return "MSG_NET_FETCH_AI_PRESETS_SUCCESS";
case MSG_NET_FETCH_AI_PRESETS_FAILURE : return "MSG_NET_FETCH_AI_PRESETS_FAILURE";
case MSG_NET_ADD_PEEROPTIONS_REQUESTED : return "MSG_NET_ADD_PEEROPTIONS_REQUESTED";
case MSG_NET_REMOVE_PEEROPTIONS_REQUESTED : return "MSG_NET_REMOVE_PEEROPTIONS_REQUESTED";

case MSG_SIM_PAUSE_REQUESTED : return "MSG_SIM_PAUSE_REQUESTED";
case MSG_SIM_UNPAUSE_REQUESTED : return "MSG_SIM_UNPAUSE_REQUESTED";
Expand All @@ -613,6 +615,8 @@ const char* MsgTypeToString(MsgType type)
case MSG_SIM_TELEPORT_PLAYER_REQUESTED : return "MSG_SIM_TELEPORT_PLAYER_REQUESTED";
case MSG_SIM_HIDE_NET_ACTOR_REQUESTED : return "MSG_SIM_HIDE_NET_ACTOR_REQUESTED";
case MSG_SIM_UNHIDE_NET_ACTOR_REQUESTED : return "MSG_SIM_UNHIDE_NET_ACTOR_REQUESTED";
case MSG_SIM_MUTE_NET_ACTOR_REQUESTED : return "MSG_SIM_MUTE_NET_ACTOR_REQUESTED";
case MSG_SIM_UNMUTE_NET_ACTOR_REQUESTED : return "MSG_SIM_UNMUTE_NET_ACTOR_REQUESTED";
case MSG_SIM_SCRIPT_EVENT_TRIGGERED : return "MSG_SIM_SCRIPT_EVENT_TRIGGERED";
case MSG_SIM_SCRIPT_CALLBACK_QUEUED : return "MSG_SIM_SCRIPT_CALLBACK_QUEUED";
case MSG_SIM_ACTOR_LINKING_REQUESTED : return "MSG_SIM_ACTOR_LINKING_REQUESTED";
Expand Down
5 changes: 5 additions & 0 deletions source/main/Application.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ enum MsgType
MSG_NET_REFRESH_REPOLIST_FAILURE, //!< Payload = RoR::CurlFailInfo* (owner)
MSG_NET_FETCH_AI_PRESETS_SUCCESS, //!< Description = JSON string
MSG_NET_FETCH_AI_PRESETS_FAILURE, //!< Description = message
MSG_NET_ADD_PEEROPTIONS_REQUESTED, //!< Payload = RoR::PeerOptionsRequest* (owner)
MSG_NET_REMOVE_PEEROPTIONS_REQUESTED, //!< Payload = RoR::PeerOptionsRequest* (owner)
// Simulation
MSG_SIM_PAUSE_REQUESTED,
MSG_SIM_UNPAUSE_REQUESTED,
Expand All @@ -123,6 +125,8 @@ enum MsgType
MSG_SIM_TELEPORT_PLAYER_REQUESTED, //!< Payload = Ogre::Vector3* (owner)
MSG_SIM_HIDE_NET_ACTOR_REQUESTED, //!< Payload = ActorPtr* (owner)
MSG_SIM_UNHIDE_NET_ACTOR_REQUESTED, //!< Payload = ActorPtr* (owner)
MSG_SIM_MUTE_NET_ACTOR_REQUESTED, //!< Payload = ActorPtr* (owner)
MSG_SIM_UNMUTE_NET_ACTOR_REQUESTED, //!< Payload = ActorPtr* (owner)
MSG_SIM_SCRIPT_EVENT_TRIGGERED, //!< Payload = RoR::ScriptEventArgs* (owner)
MSG_SIM_SCRIPT_CALLBACK_QUEUED, //!< Payload = RoR::ScriptCallbackArgs* (owner)
MSG_SIM_ACTOR_LINKING_REQUESTED, //!< Payload = RoR::ActorLinkingRequest* (owner)
Expand All @@ -140,6 +144,7 @@ enum MsgType
MSG_GUI_DOWNLOAD_PROGRESS,
MSG_GUI_DOWNLOAD_FINISHED,
MSG_GUI_REFRESH_TUNING_MENU_REQUESTED,
MSG_GUI_SHOW_CHATBOX_REQUESTED, //!< Description = message or server command to pre-fill in the chatbox (deleting whatever was there previously)
// Editing
MSG_EDI_MODIFY_GROUNDMODEL_REQUESTED, //!< Payload = RoR::ground_model_t* (weak)
MSG_EDI_ENTER_TERRN_EDITOR_REQUESTED,
Expand Down
9 changes: 9 additions & 0 deletions source/main/GameContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,15 @@ ActorPtr GameContext::SpawnActor(ActorSpawnRequest& rq)
{
fresh_actor->ar_net_source_id = rq.net_source_id;
fresh_actor->ar_net_stream_id = rq.net_stream_id;

if (BITMASK_IS_1(rq.asr_net_peeropts, RoRnet::PEEROPT_MUTE_ACTORS))
{
this->PushMessage(Message(MSG_SIM_MUTE_NET_ACTOR_REQUESTED, new ActorPtr(fresh_actor)));
}
if (BITMASK_IS_1(rq.asr_net_peeropts, RoRnet::PEEROPT_HIDE_ACTORS))
{
this->PushMessage(Message(MSG_SIM_HIDE_NET_ACTOR_REQUESTED, new ActorPtr(fresh_actor)));
}
}
else if (rq.asr_origin == ActorSpawnRequest::Origin::SAVEGAME)
{
Expand Down
7 changes: 7 additions & 0 deletions source/main/gameplay/ChatSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ void ReceiveStreamData(unsigned int type, int source, char* buffer)
if (type != MSG2_UTF8_CHAT && type != MSG2_UTF8_PRIVCHAT)
return;

if (source != -1) // user ID -1 is a server broadcast message, defined as `TO_ALL` in rorserver.
{
BitMask_t peeropts = BitMask_t(0);
if (!App::GetNetwork()->GetUserPeerOpts(source, peeropts) || BITMASK_IS_1(peeropts, RoRnet::PEEROPT_MUTE_CHAT))
return;
}

std::string text = SanitizeUtf8CString(buffer);
if (type == MSG2_UTF8_PRIVCHAT)
{
Expand Down
22 changes: 22 additions & 0 deletions source/main/gui/GUIUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,28 @@ bool RoR::ImButtonHoldToConfirm(const std::string& btn_idstr, const bool smallbu
return false;
}

bool RoR::ImMoveTextInputCursorToEnd(const char* label)
{
ImGuiWindow* window = ImGui::GetCurrentWindow();
const ImGuiID id = window->GetID(label);
ImGuiContext& g = *GImGui;

// NB: we are only allowed to access 'edit_state' if we are the active widget.
ImGuiInputTextState* state = NULL;
if (g.InputTextState.ID != id)
{
return false;
}

state = &g.InputTextState;
// based on `ImGuiInputTextState::CursorClamp()`
state->Stb.cursor = state->CurLenW;
state->Stb.select_start = 0;
state->Stb.select_end = 0;

return true;
}

bool RoR::GetScreenPosFromWorldPos(Ogre::Vector3 const& world_pos, ImVec2& out_screen)
{
ImVec2 screen_size = ImGui::GetIO().DisplaySize;
Expand Down
3 changes: 3 additions & 0 deletions source/main/gui/GUIUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,7 @@ ImVec2 ImCalcEventHighlightedSize(events input_event);
// Draws button which must be held for a period to report "clicked" - shows a tooltip with countdown progressbar.
bool ImButtonHoldToConfirm(const std::string& btn_idstr, const bool smallbutton, const float time_limit);

// Returns true if succeeded (needs the text box to have focus).
bool ImMoveTextInputCursorToEnd(const char* label);

} // namespace RoR
7 changes: 6 additions & 1 deletion source/main/gui/panels/GUI_GameChatBox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "ChatSystem.h"
#include "Console.h"
#include "GUIManager.h"
#include "GUIUtils.h"
#include "Language.h"
#include "InputEngine.h"

Expand Down Expand Up @@ -148,8 +149,12 @@ void GameChatBox::Draw()
ImGui::SetKeyboardFocusHere();
m_kb_focused = true;
}
if (m_scheduled_move_textcursor_to_end && ImMoveTextInputCursorToEnd("##chatbox"))
{
m_scheduled_move_textcursor_to_end = false;
}
const ImGuiInputTextFlags cmd_flags = ImGuiInputTextFlags_EnterReturnsTrue;
if (ImGui::InputText("", m_msg_buffer.GetBuffer(), m_msg_buffer.GetCapacity(), cmd_flags))
if (ImGui::InputText("##chatbox", m_msg_buffer.GetBuffer(), m_msg_buffer.GetCapacity(), cmd_flags))
{
if (App::mp_state->getEnum<MpState>() == MpState::CONNECTED)
{
Expand Down
2 changes: 2 additions & 0 deletions source/main/gui/panels/GUI_GameChatBox.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class GameChatBox

void Draw();
ConsoleView& GetConsoleView() { return m_console_view; }
void AssignBuffer(const std::string& buffer) { m_msg_buffer = buffer; m_scheduled_move_textcursor_to_end = true; }

private:
void SubmitMessage(); //!< Flush the user input box
Expand All @@ -57,6 +58,7 @@ class GameChatBox
ConsoleView m_console_view;
bool initialized = true;
bool init_scroll = false;
bool m_scheduled_move_textcursor_to_end = false;
};

} // namespace GUI
Expand Down
141 changes: 130 additions & 11 deletions source/main/gui/panels/GUI_MultiplayerClientList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,29 @@ using namespace Ogre;
void MpClientList::UpdateClients()
{
#if USE_SOCKETW
// vectorpos may change, so we must look up ID again.
int peeropts_menu_active_user_uid = -1;
if (m_peeropts_menu_active_user_vectorpos != -1)
{
peeropts_menu_active_user_uid = (int)m_users[m_peeropts_menu_active_user_vectorpos].uniqueid;
}
m_peeropts_menu_active_user_vectorpos = -1;

// Update the user data
m_users = App::GetNetwork()->GetUserInfos();
m_users.insert(m_users.begin(), App::GetNetwork()->GetLocalUserData());

m_users_peeropts = App::GetNetwork()->GetAllUsersPeerOpts();
m_users_peeropts.insert(m_users_peeropts.begin(), BitMask_t(0));

// Restore the vectorpos
for (int i = 0; i < (int)m_users.size(); i++)
{
if ((int)m_users[i].uniqueid == peeropts_menu_active_user_uid)
{
m_peeropts_menu_active_user_vectorpos = i;
}
}
#endif // USE_SOCKETW
}

Expand All @@ -65,7 +86,7 @@ void MpClientList::Draw()

ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar;
const float content_width = 200.f;
const float content_width = 225.f;
ImGui::SetNextWindowContentWidth(content_width);
ImGui::SetNextWindowPos(ImVec2(
ImGui::GetIO().DisplaySize.x - (content_width + (2*ImGui::GetStyle().WindowPadding.x) + theme.screen_edge_padding.x),
Expand All @@ -83,10 +104,32 @@ void MpClientList::Draw()
ImGui::Begin("Peers", nullptr, flags);

const RoRnet::UserInfo& local_user = m_users[0]; // See `UpdateClients()`
int vectorpos = 0;
for (RoRnet::UserInfo const& user: m_users)
{
ImGui::PushID(user.uniqueid);
const ImVec2 hover_tl = ImGui::GetCursorScreenPos();

// PeerOptions popup menu
if (user.uniqueid == local_user.uniqueid)
{
ImGui::Dummy(ImVec2(ImGui::CalcTextSize(" < ").x + ImGui::GetStyle().FramePadding.x*2 , ImGui::GetTextLineHeight()));
}
else if (ImGui::SmallButton(" < "))
{
if (m_peeropts_menu_active_user_vectorpos == vectorpos)
{
m_peeropts_menu_active_user_vectorpos = -1; // hide menu
}
else
{
m_peeropts_menu_active_user_vectorpos = vectorpos; // show menu
m_peeropts_menu_corner_tl = hover_tl - ImVec2(PEEROPTS_MENU_CONTENT_WIDTH + ImGui::GetStyle().WindowPadding.x*3 + PEEROPTS_MENU_MARGIN, 0);
}
}
ImGui::SameLine();

// Icon sizes: flag(16x11), auth(16x16), up(16x16), down(16x16)
bool hovered = false;
Ogre::TexturePtr flag_tex;
Ogre::TexturePtr auth_tex;
Ogre::TexturePtr down_tex;
Expand Down Expand Up @@ -114,30 +157,36 @@ void MpClientList::Draw()
}
}
// Always invoke to keep usernames aligned
hovered |= this->DrawIcon(down_tex, ImVec2(8.f, ImGui::GetTextLineHeight()));
hovered |= this->DrawIcon(up_tex, ImVec2(8.f, ImGui::GetTextLineHeight()));
this->DrawIcon(down_tex, ImVec2(8.f, ImGui::GetTextLineHeight()));
this->DrawIcon(up_tex, ImVec2(8.f, ImGui::GetTextLineHeight()));

// Auth icon
if (user.authstatus & RoRnet::AUTH_ADMIN ) { auth_tex = m_icon_flag_red; }
else if (user.authstatus & RoRnet::AUTH_MOD ) { auth_tex = m_icon_flag_blue; }
else if (user.authstatus & RoRnet::AUTH_RANKED) { auth_tex = m_icon_flag_green; }

hovered |= this->DrawIcon(auth_tex, ImVec2(14.f, ImGui::GetTextLineHeight()));
this->DrawIcon(auth_tex, ImVec2(14.f, ImGui::GetTextLineHeight()));

// Country flag
StringVector parts = StringUtil::split(user.language, "_");
if (parts.size() == 2)
{
StringUtil::toLowerCase(parts[1]);
flag_tex = FetchIcon((parts[1] + ".png").c_str());
hovered |= this->DrawIcon(flag_tex, ImVec2(16.f, ImGui::GetTextLineHeight()));
this->DrawIcon(flag_tex, ImVec2(16.f, ImGui::GetTextLineHeight()));
}

// Player name
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetStyle().ItemSpacing.x); // Some extra padding
ColourValue col = App::GetNetwork()->GetPlayerColor(user.colournum);
ImGui::TextColored(ImVec4(col.r, col.g, col.b, col.a), "%s", user.username);
hovered |= ImGui::IsItemHovered();
const ImVec2 hover_br = hover_tl + ImVec2(content_width, ImGui::GetTextLineHeight());
const float HOVER_TL_SHIFTX = 20.f; // leave the [<] button (PeerOptions submenu) out of the hover check.
const bool hovered
= hover_br.x > ImGui::GetIO().MousePos.x
&& hover_br.y > ImGui::GetIO().MousePos.y
&& ImGui::GetIO().MousePos.x > (hover_tl.x + HOVER_TL_SHIFTX)
&& ImGui::GetIO().MousePos.y > hover_tl.y;

// Tooltip
if (hovered)
Expand Down Expand Up @@ -218,6 +267,8 @@ void MpClientList::Draw()

ImGui::EndTooltip();
}
ImGui::PopID(); // user.uniqueid
vectorpos++;
}

if (App::GetNetwork()->GetNetQuality() != 0)
Expand All @@ -231,24 +282,92 @@ void MpClientList::Draw()

ImGui::End();
ImGui::PopStyleColor(1); // WindowBg

this->DrawPeerOptionsMenu();
#endif // USE_SOCKETW
}

bool MpClientList::DrawIcon(Ogre::TexturePtr tex, ImVec2 reference_box)
void MpClientList::DrawPeerOptCheckbox(const BitMask_t flag, const std::string& label)
{
int uid = (int)m_users[m_peeropts_menu_active_user_vectorpos].uniqueid;
bool flagval = m_users_peeropts[m_peeropts_menu_active_user_vectorpos] & flag;

if (ImGui::Checkbox(label.c_str(), &flagval))
{
MsgType peeropt_msg = flagval ? MSG_NET_ADD_PEEROPTIONS_REQUESTED : MSG_NET_REMOVE_PEEROPTIONS_REQUESTED;
App::GetGameContext()->PushMessage(Message(peeropt_msg, new PeerOptionsRequest{ uid, flag }));
App::GetGameContext()->ChainMessage(Message(MSG_GUI_MP_CLIENTS_REFRESH));
}
}

void RoR::GUI::MpClientList::DrawServerCommandBtn(const std::string& cmdfmt, const std::string& label)
{
std::string chatmsg = fmt::format(cmdfmt, m_users[m_peeropts_menu_active_user_vectorpos].uniqueid);
if (ImGui::Button(label.c_str()))
{
App::GetGameContext()->PushMessage(Message(MSG_GUI_SHOW_CHATBOX_REQUESTED, chatmsg));
}
}

void MpClientList::DrawPeerOptionsMenu()
{
if (m_peeropts_menu_active_user_vectorpos == -1)
return; // Menu not visible

// Sanity check
const bool vectorpos_sane = (m_peeropts_menu_active_user_vectorpos >= 0
&& m_peeropts_menu_active_user_vectorpos < (int)m_users_peeropts.size());
ROR_ASSERT(vectorpos_sane);
if (!vectorpos_sane)
return; // Minimize damage

// Draw UI
ImGui::SetNextWindowPos(m_peeropts_menu_corner_tl);
ImGui::SetNextWindowContentWidth(PEEROPTS_MENU_CONTENT_WIDTH);
const int flags = ImGuiWindowFlags_NoDecoration;
if (ImGui::Begin("PeerOptions", nullptr, flags))
{
ImGui::TextDisabled("%s", _LC("MultiplayerClientList", "Local actions"));
this->DrawPeerOptCheckbox(RoRnet::PEEROPT_MUTE_CHAT, _LC("MultiplayerClientList", "Mute chat"));
this->DrawPeerOptCheckbox(RoRnet::PEEROPT_MUTE_ACTORS, _LC("MultiplayerClientList", "Mute actors"));
this->DrawPeerOptCheckbox(RoRnet::PEEROPT_HIDE_ACTORS, _LC("MultiplayerClientList", "Hide actors"));
ImGui::Separator();
ImGui::TextDisabled("%s", _LC("MultiplayerClientList", "Server commands"));
this->DrawServerCommandBtn("!report {} Please enter reason: ", _LC("MultiplayerClientList", "Report"));
const int32_t authstatus = m_users[0].authstatus; // User[0] is the local user
if (BITMASK_IS_1(authstatus, RoRnet::AUTH_ADMIN) || BITMASK_IS_1(authstatus, RoRnet::AUTH_MOD))
{
this->DrawServerCommandBtn("!kick {}", _LC("MultiplayerClientList", "Kick"));
this->DrawServerCommandBtn("!ban {}", _LC("MultiplayerClientList", "Ban"));
}
m_peeropts_menu_corner_br = m_peeropts_menu_corner_tl + ImGui::GetWindowContentRegionMax();

ImGui::End();
}

// Check hover and hide
const ImVec2 hoverbox_tl = m_peeropts_menu_corner_tl - ImVec2(PEEROPTS_HOVER_MARGIN, PEEROPTS_HOVER_MARGIN);
const ImVec2 hoverbox_br = m_peeropts_menu_corner_br + ImVec2(PEEROPTS_HOVER_MARGIN, PEEROPTS_HOVER_MARGIN);
const ImVec2 mousepos = ImGui::GetIO().MousePos;
if (mousepos.x < hoverbox_tl.x || mousepos.x > hoverbox_br.x
|| mousepos.y < hoverbox_tl.y || mousepos.y > hoverbox_br.y)
{
m_peeropts_menu_active_user_vectorpos = -1;
}
}

void MpClientList::DrawIcon(Ogre::TexturePtr tex, ImVec2 reference_box)
{
ImVec2 orig_pos = ImGui::GetCursorPos();
bool hovered = false;
if (tex)
{
// TODO: moving the cursor somehow deforms the image
// ImGui::SetCursorPosX(orig_pos.x + (reference_box.x - tex->getWidth()) / 2.f);
// ImGui::SetCursorPosY(orig_pos.y + (reference_box.y - tex->getHeight()) / 2.f);
ImGui::Image(reinterpret_cast<ImTextureID>(tex->getHandle()), ImVec2(16, 16));
hovered = ImGui::IsItemHovered();
}
ImGui::SetCursorPosX(orig_pos.x + reference_box.x + ImGui::GetStyle().ItemSpacing.x);
ImGui::SetCursorPosY(orig_pos.y);
return hovered;
}

void MpClientList::CacheIcons()
Expand Down
16 changes: 14 additions & 2 deletions source/main/gui/panels/GUI_MultiplayerClientList.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,22 @@ class MpClientList
void UpdateClients();

private:
bool DrawIcon(Ogre::TexturePtr tex, ImVec2 reference_box); // Returns true if hovered
void DrawIcon(Ogre::TexturePtr tex, ImVec2 reference_box);
void CacheIcons();

std::vector<RoRnet::UserInfo> m_users; // only updated on demand to reduce mutex locking and vector allocating overhead.
std::vector<RoRnet::UserInfo> m_users; // only updated on demand to reduce mutex locking and vector allocating overhead; see `MSG_GUI_MP_CLIENTS_REFRESH`.
std::vector <BitMask_t> m_users_peeropts; // updated along with `m_users`, see `MSG_GUI_MP_CLIENTS_REFRESH`.

// Peer options menu - opened by [<] button, closes when mouse cursor leaves.
void DrawPeerOptionsMenu();
void DrawPeerOptCheckbox(const BitMask_t flag, const std::string& label);
void DrawServerCommandBtn(const std::string& cmdfmt, const std::string& label);
const int PEEROPTS_MENU_CONTENT_WIDTH = 150;
const int PEEROPTS_MENU_MARGIN = 10;
const int PEEROPTS_HOVER_MARGIN = 100;
int m_peeropts_menu_active_user_vectorpos = -1;
ImVec2 m_peeropts_menu_corner_tl = ImVec2(0, 0);
ImVec2 m_peeropts_menu_corner_br = ImVec2(0, 0);

// Icon cache
bool m_icons_cached = false;
Expand Down
Loading
Loading