diff --git a/src/core/netplay.cpp b/src/core/netplay.cpp index ce04535ee..fe1497d1a 100644 --- a/src/core/netplay.cpp +++ b/src/core/netplay.cpp @@ -543,62 +543,46 @@ void Netplay::NpFreeBuffCb(void* ctx, void* buffer, int frame) bool Netplay::NpOnEventCb(void* ctx, GGPOEvent* ev) { - char buff[128]; - std::string msg, filename; switch (ev->code) { case GGPOEventCode::GGPO_EVENTCODE_CONNECTED_TO_PEER: - sprintf(buff, "Netplay Connected To Player: %d", ev->u.connected.player); - msg = buff; + Host::OnNetplayMessage(fmt::format("Netplay Connected To Player: {}", ev->u.connected.player)); break; case GGPOEventCode::GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER: - sprintf(buff, "Netplay Synchronzing: %d/%d", ev->u.synchronizing.count, ev->u.synchronizing.total); - msg = buff; + Host::OnNetplayMessage(fmt::format("Netplay Synchronzing: {}/{}", ev->u.synchronizing.count, ev->u.synchronizing.total)); break; case GGPOEventCode::GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER: - sprintf(buff, "Netplay Synchronized With Player: %d", ev->u.synchronized.player); - msg = buff; + Host::OnNetplayMessage(fmt::format("Netplay Synchronized With Player: {}", ev->u.synchronized.player)); break; case GGPOEventCode::GGPO_EVENTCODE_DISCONNECTED_FROM_PEER: - sprintf(buff, "Netplay Player: %d Disconnected", ev->u.disconnected.player); - msg = buff; + Host::OnNetplayMessage(fmt::format("Netplay Player: %d Disconnected", ev->u.disconnected.player)); break; case GGPOEventCode::GGPO_EVENTCODE_RUNNING: - msg = "Netplay Is Running"; + Host::OnNetplayMessage("Netplay Is Running"); break; case GGPOEventCode::GGPO_EVENTCODE_CONNECTION_INTERRUPTED: - sprintf(buff, "Netplay Player: %d Connection Interupted, Timeout: %d", ev->u.connection_interrupted.player, - ev->u.connection_interrupted.disconnect_timeout); - msg = buff; + Host::OnNetplayMessage(fmt::format("Netplay Player: {} Connection Interupted, Timeout: {}", ev->u.connection_interrupted.player, + ev->u.connection_interrupted.disconnect_timeout)); break; case GGPOEventCode::GGPO_EVENTCODE_CONNECTION_RESUMED: - sprintf(buff, "Netplay Player: %d Connection Resumed", ev->u.connection_resumed.player); - msg = buff; + Host::OnNetplayMessage(fmt::format("Netplay Player: {} Connection Resumed", ev->u.connection_resumed.player)); break; case GGPOEventCode::GGPO_EVENTCODE_CHAT: - sprintf(buff, "%s", ev->u.chat.msg); - msg = buff; + Host::OnNetplayMessage(ev->u.chat.msg); break; case GGPOEventCode::GGPO_EVENTCODE_TIMESYNC: HandleTimeSyncEvent(ev->u.timesync.frames_ahead, ev->u.timesync.timeSyncPeriodInFrames); break; case GGPOEventCode::GGPO_EVENTCODE_DESYNC: - sprintf(buff, "Desync Detected: Current Frame: %d, Desync Frame: %d, Diff: %d, L:%u, R:%u", CurrentFrame(), + Host::OnNetplayMessage(fmt::format("Desync Detected: Current Frame: {}, Desync Frame: {}, Diff: {}, L:{}, R:{}", CurrentFrame(), ev->u.desync.nFrameOfDesync, CurrentFrame() - ev->u.desync.nFrameOfDesync, ev->u.desync.ourCheckSum, - ev->u.desync.remoteChecksum); - msg = buff; + ev->u.desync.remoteChecksum)); GenerateDesyncReport(ev->u.desync.nFrameOfDesync); - Host::AddKeyedOSDMessage("Netplay", msg, 5); - - return true; + break; default: - sprintf(buff, "Netplay Event Code: %d", ev->code); - msg = buff; - } - if (!msg.empty()) - { - Host::OnNetplayMessage(msg); - Log_InfoPrintf("%s", msg.c_str()); + Host::OnNetplayMessage(fmt::format("Netplay Event Code: {}", static_cast(ev->code))); + break; } + return true; } diff --git a/src/core/system.h b/src/core/system.h index aeae19561..d0c27602d 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -512,5 +512,5 @@ bool IsFullscreen(); /// Alters fullscreen state of hosting application. void SetFullscreen(bool enabled); // netplay -void OnNetplayMessage(std::string& message); +void OnNetplayMessage(std::string message); } // namespace Host diff --git a/src/duckstation-qt/netplaywidget.cpp b/src/duckstation-qt/netplaywidget.cpp index d598d89be..5e1153cf2 100644 --- a/src/duckstation-qt/netplaywidget.cpp +++ b/src/duckstation-qt/netplaywidget.cpp @@ -38,8 +38,6 @@ void NetplayWidget::FillGameList() void NetplayWidget::SetupConnections() { - // connect netplay window messages - connect(g_emu_thread, &EmuThread::onNetplayMessage, this, &NetplayWidget::OnMsgReceived); // connect sending messages when the chat button has been pressed connect(m_ui->btnSendMsg, &QPushButton::pressed, [this]() { // check if message aint empty and the complete message ( message + name + ":" + space) is below 120 characters diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 2520a6b53..80b368fd1 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -465,12 +465,6 @@ void EmuThread::startFullscreenUI() wakeThread(); } -void Host::OnNetplayMessage(std::string& message) -{ - QString msg(message.c_str()); - emit g_emu_thread->onNetplayMessage(msg); -} - void EmuThread::stopFullscreenUI() { if (!isOnThread()) @@ -1513,6 +1507,8 @@ void EmuThread::renderDisplay(bool skip_present) if (!skip_present) { FullscreenUI::Render(); + if (Netplay::IsActive()) + ImGuiManager::RenderNetplayOverlays(); ImGuiManager::RenderTextOverlays(); ImGuiManager::RenderOSDMessages(); } diff --git a/src/duckstation-qt/qthost.h b/src/duckstation-qt/qthost.h index 9ca0e1ba7..5f49c40b6 100644 --- a/src/duckstation-qt/qthost.h +++ b/src/duckstation-qt/qthost.h @@ -143,7 +143,6 @@ Q_SIGNALS: void achievementsRefreshed(quint32 id, const QString& game_info_string, quint32 total, quint32 points); void achievementsChallengeModeChanged(); void cheatEnabled(quint32 index, bool enabled); - void onNetplayMessage(const QString& message); public Q_SLOTS: void setDefaultSettings(bool system = true, bool controller = true); diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index 1496aad3f..f4d7907af 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -16,6 +16,7 @@ add_library(frontend-common imgui_fullscreen.h imgui_manager.cpp imgui_manager.h + imgui_netplay.cpp imgui_overlays.cpp imgui_overlays.h platform_misc.h diff --git a/src/frontend-common/common_host.cpp b/src/frontend-common/common_host.cpp index 1bcca55de..0b6e1d091 100644 --- a/src/frontend-common/common_host.cpp +++ b/src/frontend-common/common_host.cpp @@ -677,6 +677,11 @@ DEFINE_HOTKEY("OpenPauseMenu", TRANSLATABLE("Hotkeys", "General"), TRANSLATABLE( [](s32 pressed) { if (!pressed) FullscreenUI::OpenPauseMenu(); +}) +DEFINE_HOTKEY("OpenNetplayChat", TRANSLATABLE("Hotkeys", "General"), TRANSLATABLE("Hotkeys", "Open Netplay Chat"), + [](s32 pressed) { + if (!pressed) + ImGuiManager::OpenNetplayChat(); }) #endif diff --git a/src/frontend-common/frontend-common.vcxproj b/src/frontend-common/frontend-common.vcxproj index ea07d2c33..8b50bac2c 100644 --- a/src/frontend-common/frontend-common.vcxproj +++ b/src/frontend-common/frontend-common.vcxproj @@ -22,6 +22,7 @@ true + diff --git a/src/frontend-common/frontend-common.vcxproj.filters b/src/frontend-common/frontend-common.vcxproj.filters index a1d19f7ab..204808a39 100644 --- a/src/frontend-common/frontend-common.vcxproj.filters +++ b/src/frontend-common/frontend-common.vcxproj.filters @@ -30,6 +30,7 @@ + diff --git a/src/frontend-common/imgui_netplay.cpp b/src/frontend-common/imgui_netplay.cpp new file mode 100644 index 000000000..8560ee2ab --- /dev/null +++ b/src/frontend-common/imgui_netplay.cpp @@ -0,0 +1,218 @@ +// SPDX-FileCopyrightText: 2023 Connor McLaughlin +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#define IMGUI_DEFINE_MATH_OPERATORS + +#include "IconsFontAwesome5.h" +#include "common/align.h" +#include "common/assert.h" +#include "common/file_system.h" +#include "common/log.h" +#include "common/string_util.h" +#include "common/timer.h" +#include "common_host.h" +#include "core/controller.h" +#include "core/gpu.h" +#include "core/host.h" +#include "core/host_display.h" +#include "core/host_settings.h" +#include "core/netplay.h" +#include "core/settings.h" +#include "core/spu.h" +#include "core/system.h" +#include "fmt/chrono.h" +#include "fmt/format.h" +#include "fullscreen_ui.h" +#include "gsl/span" +#include "icon.h" +#include "imgui.h" +#include "imgui_fullscreen.h" +#include "imgui_internal.h" +#include "imgui_manager.h" +#include "imgui_overlays.h" +#include "imgui_stdlib.h" +#include "input_manager.h" +#include "util/audio_stream.h" +#include +#include +#include +#include +#include +#include + +#if defined(CPU_X64) +#include +#elif defined(CPU_AARCH64) +#ifdef _MSC_VER +#include +#else +#include +#endif +#endif + +Log_SetChannel(ImGuiManager); + +namespace ImGuiManager { +static void DrawNetplayMessages(); +static void DrawNetplayStats(); +static void DrawNetplayChatDialog(); +} // namespace ImGuiManager + +static std::deque> s_netplay_messages; +static constexpr u32 MAX_NETPLAY_MESSAGES = 15; +static constexpr float NETPLAY_MESSAGE_DURATION = 15.0f; +static constexpr float NETPLAY_MESSAGE_FADE_TIME = 2.0f; +static bool s_netplay_chat_dialog_open = false; +static bool s_netplay_chat_dialog_opening = false; +static std::string s_netplay_chat_message; + +void Host::OnNetplayMessage(std::string message) +{ + Log_InfoPrintf("Netplay: %s", message.c_str()); + + while (s_netplay_messages.size() >= MAX_NETPLAY_MESSAGES) + s_netplay_messages.pop_front(); + + s_netplay_messages.emplace_back(std::move(message), Common::Timer::GetCurrentValue() + + Common::Timer::ConvertSecondsToValue(NETPLAY_MESSAGE_DURATION)); +} + +void ImGuiManager::RenderNetplayOverlays() +{ + DrawNetplayMessages(); + DrawNetplayStats(); + DrawNetplayChatDialog(); +} + +void ImGuiManager::DrawNetplayMessages() +{ + if (s_netplay_messages.empty()) + return; + + const Common::Timer::Value ticks = Common::Timer::GetCurrentValue(); + const ImGuiIO& io = ImGui::GetIO(); + const float scale = ImGuiManager::GetGlobalScale(); + const float shadow_offset = 1.0f * scale; + const float margin = 10.0f * scale; + const float spacing = 5.0f * scale; + const float msg_spacing = 2.0f * scale; + ImFont* font = ImGuiManager::GetFixedFont(); + float position_y = io.DisplaySize.y - margin - ((spacing + font->FontSize) * 2.0f) - font->FontSize; + ImDrawList* dl = ImGui::GetBackgroundDrawList(); + + // drop expired messages.. because of the reverse iteration below, we can't do it in there :/ + for (auto iter = s_netplay_messages.begin(); iter != s_netplay_messages.end();) + { + if (ticks >= iter->second) + iter = s_netplay_messages.erase(iter); + else + ++iter; + } + + for (auto iter = s_netplay_messages.rbegin(); iter != s_netplay_messages.rend(); ++iter) + { + const float remainder = static_cast(Common::Timer::ConvertValueToSeconds(iter->second - ticks)); + const float opacity = std::min(remainder / NETPLAY_MESSAGE_FADE_TIME, 1.0f); + const u32 alpha = static_cast(opacity * 255.0f); + const u32 shadow_alpha = static_cast(opacity * 100.0f); + + // TODO: line wrapping.. + const char* text_start = iter->first.c_str(); + const char* text_end = text_start + iter->first.length(); + const ImVec2 text_size = font->CalcTextSizeA(font->FontSize, io.DisplaySize.x, 0.0f, text_start, text_end, nullptr); + + dl->AddText(font, font->FontSize, + ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x + shadow_offset, position_y + shadow_offset), + IM_COL32(0, 0, 0, shadow_alpha), text_start, text_end); + dl->AddText(font, font->FontSize, ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x, position_y), + IM_COL32(255, 255, 255, alpha), text_start, text_end); + + position_y -= text_size.y + msg_spacing; + } +} + +void ImGuiManager::DrawNetplayStats() +{ + // Not much yet.. eventually we'll render chat and such here too. + // We'll probably want to draw a graph too.. + + LargeString text; + text.AppendFmtString("Ping: {}", Netplay::GetPing()); + + const float scale = ImGuiManager::GetGlobalScale(); + const float shadow_offset = 1.0f * scale; + const float margin = 10.0f * scale; + const float spacing = 5.0f * scale; + ImFont* font = ImGuiManager::GetFixedFont(); + const float position_y = ImGui::GetIO().DisplaySize.y - margin - font->FontSize - spacing - font->FontSize; + + ImDrawList* dl = ImGui::GetBackgroundDrawList(); + ImVec2 text_size = font->CalcTextSizeA(font->FontSize, std::numeric_limits::max(), -1.0f, text, + text.GetCharArray() + text.GetLength(), nullptr); + dl->AddText(font, font->FontSize, + ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x + shadow_offset, position_y + shadow_offset), + IM_COL32(0, 0, 0, 100), text, text.GetCharArray() + text.GetLength()); + dl->AddText(font, font->FontSize, ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x, position_y), + IM_COL32(255, 255, 255, 255), text, text.GetCharArray() + text.GetLength()); +} + +void ImGuiManager::DrawNetplayChatDialog() +{ + // TODO: This needs to block controller input... + + if (s_netplay_chat_dialog_opening) + { + ImGui::OpenPopup("Netplay Chat"); + s_netplay_chat_dialog_open = true; + s_netplay_chat_dialog_opening = false; + } + else if (!s_netplay_chat_dialog_open) + { + return; + } + + const bool send_message = ImGui::IsKeyPressed(ImGuiKey_Enter); + const bool close_chat = + send_message || (s_netplay_chat_message.empty() && (ImGui::IsKeyPressed(ImGuiKey_Backspace)) || + ImGui::IsKeyPressed(ImGuiKey_Escape)); + if (send_message && !s_netplay_chat_message.empty()) + Netplay::SendMsg(s_netplay_chat_message.c_str()); + + const ImGuiIO& io = ImGui::GetIO(); + const ImGuiStyle& style = ImGui::GetStyle(); + const float scale = ImGuiManager::GetGlobalScale(); + const float width = 600.0f * scale; + const float height = 60.0f * scale; + + ImGui::SetNextWindowSize(ImVec2(width, height)); + ImGui::SetNextWindowPos(io.DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowFocus(); + + if (ImGui::BeginPopupModal("Netplay Chat", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize)) + { + ImGui::SetNextItemWidth(width - style.WindowPadding.x * 2.0f); + ImGui::SetKeyboardFocusHere(); + ImGui::InputText("##chatmsg", &s_netplay_chat_message); + + if (!s_netplay_chat_dialog_open) + ImGui::CloseCurrentPopup(); + + ImGui::EndPopup(); + } + + if (close_chat) + { + s_netplay_chat_message.clear(); + s_netplay_chat_dialog_open = false; + } + + s_netplay_chat_dialog_opening = false; +} + +void ImGuiManager::OpenNetplayChat() +{ + if (s_netplay_chat_dialog_open) + return; + + s_netplay_chat_dialog_opening = true; +} diff --git a/src/frontend-common/imgui_overlays.h b/src/frontend-common/imgui_overlays.h index c295202b5..6d1c9f395 100644 --- a/src/frontend-common/imgui_overlays.h +++ b/src/frontend-common/imgui_overlays.h @@ -8,6 +8,9 @@ namespace ImGuiManager { void RenderTextOverlays(); void RenderOverlayWindows(); + +void RenderNetplayOverlays(); +void OpenNetplayChat(); } namespace SaveStateSelectorUI {