Skip to content

Commit

Permalink
🎮 Multiplayer: Added PeerOptions + UI
Browse files Browse the repository at this point in the history
PeerOptions are flags used only locally on client to filter and control incoming traffic.
*    PEEROPT_MUTE_CHAT     //!< CHAT and PRIVCHAT messages will not be allowed through.
*    PEEROPT_MUTE_ACTORS   //!< Spawn actors muted and immediatelly mute existing actors.
*    PEEROPT_HIDE_ACTORS   //!< Spawn actors hidden and immediatelly hide existing actors.

At the moment, these flags are only kept as long as you're connected to one server. The game does not remember them across sessions yet.

This introduces new messages:
:envelope:    `MSG_SIM_MUTE_NET_ACTOR_REQUESTED`      - for consistency with existing `(UN)HIDE_NET_ACTOR_REQUESTED`
:envelope:    `MSG_SIM_UNMUTE_NET_ACTOR_REQUESTED`
:envelope:    `MSG_NET_ADD_PEEROPTIONS_REQUESTED`     - for easy setting/clearing of the flags from UI, also triggers necessary updates to existing actors.
:envelope:    `MSG_NET_REMOVE_PEEROPTIONS_REQUESTED`

The flags are set via extended MultiplayerClientList UI which now has a [<] button at every nickname, opening a popup menu.
  • Loading branch information
ohlidalp committed Jan 21, 2025
1 parent 17a31ca commit b62706b
Show file tree
Hide file tree
Showing 15 changed files with 348 additions and 28 deletions.
4 changes: 4 additions & 0 deletions source/main/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,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 @@ -599,6 +601,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
4 changes: 4 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 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
96 changes: 94 additions & 2 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,11 +104,27 @@ 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 (ImGui::Button(" < "))
{
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_WIDTH + 15, 0);
}
}
ImGui::SameLine();

// Icon sizes: flag(16x11), auth(16x16), up(16x16), down(16x16)
Ogre::TexturePtr flag_tex;
Ogre::TexturePtr auth_tex;
Expand Down Expand Up @@ -140,10 +177,11 @@ void MpClientList::Draw()
ColourValue col = App::GetNetwork()->GetPlayerColor(user.colournum);
ImGui::TextColored(ImVec4(col.r, col.g, col.b, col.a), "%s", user.username);
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
&& ImGui::GetIO().MousePos.x > (hover_tl.x + HOVER_TL_SHIFTX)
&& ImGui::GetIO().MousePos.y > hover_tl.y;

// Tooltip
Expand Down Expand Up @@ -226,6 +264,7 @@ void MpClientList::Draw()
ImGui::EndTooltip();
}
ImGui::PopID(); // user.uniqueid
vectorpos++;
}

if (App::GetNetwork()->GetNetQuality() != 0)
Expand All @@ -239,9 +278,62 @@ void MpClientList::Draw()

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

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

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 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);
const int flags = ImGuiWindowFlags_NoDecoration;
if (ImGui::Begin("PeerOptions", nullptr, flags))
{
ImGui::TextDisabled("%s", _LC("MultiplayerClientList", "Peer options"));
ImGui::Separator();
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"));
m_peeropts_menu_corner_br = ImGui::GetCursorScreenPos();

ImGui::End();
}

// Check hover and hide
const ImVec2 hoverbox_tl = m_peeropts_menu_corner_tl - ImVec2(PEEROPTS_HOVER_PAD, PEEROPTS_HOVER_PAD);
const ImVec2 hoverbox_br = m_peeropts_menu_corner_br + ImVec2(PEEROPTS_HOVER_PAD, PEEROPTS_HOVER_PAD);
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();
Expand Down
12 changes: 11 additions & 1 deletion source/main/gui/panels/GUI_MultiplayerClientList.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,17 @@ class MpClientList
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);
const int PEEROPTS_MENU_WIDTH = 125;
const int PEEROPTS_HOVER_PAD = 150;
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
129 changes: 128 additions & 1 deletion source/main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,86 @@ int main(int argc, char *argv[])
break;
}

case MSG_NET_ADD_PEEROPTIONS_REQUESTED:
{
PeerOptionsRequest* request = static_cast<PeerOptionsRequest*>(m.payload);
try
{
// Record the options for future incoming traffic.
App::GetNetwork()->AddPeerOptions(request);

// MUTE existing actors if needed
if (BITMASK_IS_1(request->por_peeropts, RoRnet::PEEROPT_MUTE_ACTORS))
{
for (ActorPtr& actor : App::GetGameContext()->GetActorManager()->GetActors())
{
if (actor->ar_net_source_id == request->por_uid)
{
App::GetGameContext()->PushMessage(Message(MSG_SIM_MUTE_NET_ACTOR_REQUESTED, new ActorPtr(actor)));
}
}
}

// HIDE existing actors if needed
if (BITMASK_IS_1(request->por_peeropts, RoRnet::PEEROPT_HIDE_ACTORS))
{
for (ActorPtr& actor : App::GetGameContext()->GetActorManager()->GetActors())
{
if (actor->ar_net_source_id == request->por_uid)
{
App::GetGameContext()->PushMessage(Message(MSG_SIM_HIDE_NET_ACTOR_REQUESTED, new ActorPtr(actor)));
}
}
}
}
catch (...)
{
HandleMsgQueueException(m.type);
}
delete request;
break;
}

case MSG_NET_REMOVE_PEEROPTIONS_REQUESTED:
{
PeerOptionsRequest* request = static_cast<PeerOptionsRequest*>(m.payload);
try
{
// Record the options for future incoming traffic.
App::GetNetwork()->RemovePeerOptions(request);

// un-MUTE existing actors if needed
if (BITMASK_IS_1(request->por_peeropts, RoRnet::PEEROPT_MUTE_ACTORS))
{
for (ActorPtr& actor : App::GetGameContext()->GetActorManager()->GetActors())
{
if (actor->ar_net_source_id == request->por_uid)
{
App::GetGameContext()->PushMessage(Message(MSG_SIM_UNMUTE_NET_ACTOR_REQUESTED, new ActorPtr(actor)));
}
}
}

// un-HIDE existing actors if needed
if (BITMASK_IS_1(request->por_peeropts, RoRnet::PEEROPT_HIDE_ACTORS))
{
for (ActorPtr& actor : App::GetGameContext()->GetActorManager()->GetActors())
{
if (actor->ar_net_source_id == request->por_uid)
{
App::GetGameContext()->PushMessage(Message(MSG_SIM_UNHIDE_NET_ACTOR_REQUESTED, new ActorPtr(actor)));
}
}
}
}
catch (...)
{
HandleMsgQueueException(m.type);
}
delete request;
break;
}

// -- Gameplay events --

case MSG_SIM_PAUSE_REQUESTED:
Expand All @@ -821,7 +901,10 @@ int main(int argc, char *argv[])
{
for (ActorPtr& actor: App::GetGameContext()->GetActorManager()->GetActors())
{
actor->unmuteAllSounds();
if (!actor->ar_muted_by_peeropt)
{
actor->unmuteAllSounds();
}
}
App::sim_state->setVal((int)SimState::RUNNING);
}
Expand Down Expand Up @@ -1132,6 +1215,50 @@ int main(int argc, char *argv[])
break;
}

case MSG_SIM_MUTE_NET_ACTOR_REQUESTED:
{
ActorPtr* actor_ptr = static_cast<ActorPtr*>(m.payload);
try
{
ROR_ASSERT(actor_ptr);
if ((App::mp_state->getEnum<MpState>() == MpState::CONNECTED) &&
((*actor_ptr)->ar_state == ActorState::NETWORKED_OK))
{
ActorPtr actor = *actor_ptr;
actor->ar_muted_by_peeropt = true;
actor->muteAllSounds();
}
}
catch (...)
{
HandleMsgQueueException(m.type);
}
delete actor_ptr;
break;
}

case MSG_SIM_UNMUTE_NET_ACTOR_REQUESTED:
{
ActorPtr* actor_ptr = static_cast<ActorPtr*>(m.payload);
try
{
ROR_ASSERT(actor_ptr);
if ((App::mp_state->getEnum<MpState>() == MpState::CONNECTED) &&
((*actor_ptr)->ar_state == ActorState::NETWORKED_OK))
{
ActorPtr actor = *actor_ptr;
actor->ar_muted_by_peeropt = false;
actor->unmuteAllSounds();
}
}
catch (...)
{
HandleMsgQueueException(m.type);
}
delete actor_ptr;
break;
}

case MSG_SIM_SCRIPT_EVENT_TRIGGERED:
{
ScriptEventArgs* args = static_cast<ScriptEventArgs*>(m.payload);
Expand Down
Loading

0 comments on commit b62706b

Please sign in to comment.