Import fork code

Netplay Refactor: Qt Redo Netplay Window

Added ggpo-x dependency.

Qt/Netplay:
Redone DirectIP Functionality
Integrate Netplay into the Pad Runahead system

Qt/Netplay: bring back in all the Netplay features besides the chat window thats still wip.

Qt/Netplay Rewire Netplay chat window

Qt/Netplay: Add Temporary alert stating this feature is not ready yet

Settings: Apply the right settings when netplay is active

Qt/Netplay: Fixed crashing causes my missing references in the gamelist also faster loading of the game at start of the session.

Netplay: fixed stuttery feeling at high ping due to wrong order of operations including input in time calculations.

Qt/Netplay Cleanup and fix stuttering at higher ping.

Qt/Netplay Cleanup and fix stuttering at higher ping.
This commit is contained in:
HeatXD 2023-03-19 04:30:23 +01:00 committed by Stenzek
parent 1c3742dc8e
commit 938981f899
14 changed files with 1670 additions and 15 deletions

265
src/core/netplay.cpp Normal file
View File

@ -0,0 +1,265 @@
#include "netplay.h"
#include "pad.h"
#include "spu.h"
#include "system.h"
#include <bitset>
// Netplay Impl
Netplay::Session::Session() = default;
Netplay::Session::~Session()
{
Close();
}
int32_t Netplay::Session::Start(int32_t lhandle, uint16_t lport, std::string& raddr, uint16_t rport, int32_t ldelay,
uint32_t pred)
{
s_net_session.m_max_pred = pred;
/*
TODO: since saving every frame during rollback loses us time to do actual gamestate iterations it might be better to
hijack the update / save / load cycle to only save every confirmed frame only saving when actually needed.
*/
GGPOSessionCallbacks cb{};
cb.advance_frame = NpAdvFrameCb;
cb.save_game_state = NpSaveFrameCb;
cb.load_game_state = NpLoadFrameCb;
cb.begin_game = NpBeginGameCb;
cb.free_buffer = NpFreeBuffCb;
cb.on_event = NpOnEventCb;
GGPOErrorCode result;
result = ggpo_start_session(&s_net_session.p_ggpo, &cb, "Duckstation-Netplay", 2, sizeof(Netplay::Input), lport,
s_net_session.m_max_pred);
ggpo_set_disconnect_timeout(s_net_session.p_ggpo, 3000);
ggpo_set_disconnect_notify_start(s_net_session.p_ggpo, 1000);
for (int i = 1; i <= 2; i++)
{
GGPOPlayer player = {};
GGPOPlayerHandle handle = 0;
player.size = sizeof(GGPOPlayer);
player.player_num = i;
if (lhandle == i)
{
player.type = GGPOPlayerType::GGPO_PLAYERTYPE_LOCAL;
result = ggpo_add_player(s_net_session.p_ggpo, &player, &handle);
s_net_session.m_local_handle = handle;
}
else
{
player.type = GGPOPlayerType::GGPO_PLAYERTYPE_REMOTE;
#ifdef _WIN32
strcpy_s(player.u.remote.ip_address, raddr.c_str());
#else
strcpy(player.u.remote.ip_address, raddr.c_str());
#endif
player.u.remote.port = rport;
result = ggpo_add_player(s_net_session.p_ggpo, &player, &handle);
}
}
ggpo_set_frame_delay(s_net_session.p_ggpo, s_net_session.m_local_handle, ldelay);
return result;
}
void Netplay::Session::Close()
{
ggpo_close_session(s_net_session.p_ggpo);
s_net_session.p_ggpo = nullptr;
s_net_session.m_local_handle = GGPO_INVALID_HANDLE;
s_net_session.m_max_pred = 0;
}
bool Netplay::Session::IsActive()
{
return s_net_session.p_ggpo != nullptr;
}
void Netplay::Session::RunIdle()
{
ggpo_idle(s_net_session.p_ggpo);
}
void Netplay::Session::AdvanceFrame(uint16_t checksum)
{
ggpo_advance_frame(s_net_session.p_ggpo, checksum);
}
void Netplay::Session::RunFrame(int32_t& waitTime)
{
// run game
auto result = GGPO_OK;
int disconnectFlags = 0;
Netplay::Input inputs[2] = {};
// add local input
if (GetLocalHandle() != GGPO_INVALID_HANDLE)
{
auto inp = ReadLocalInput();
result = AddLocalInput(inp);
}
// advance game
if (GGPO_SUCCEEDED(result))
{
result = SyncInput(inputs, &disconnectFlags);
if (GGPO_SUCCEEDED(result))
{
// enable again when rolling back done
SPU::SetAudioOutputMuted(false);
System::NetplayAdvanceFrame (inputs, disconnectFlags);
}
else
RunIdle();
}
else
RunIdle();
waitTime = GetTimer()->UsToWaitThisLoop();
}
int32_t Netplay::Session::CurrentFrame()
{
int32_t frame;
ggpo_get_current_frame(s_net_session.p_ggpo, frame);
return frame;
}
void Netplay::Session::CollectInput(uint32_t slot, uint32_t bind, float value)
{
s_net_session.m_net_input[slot][bind] = value;
}
Netplay::Input Netplay::Session::ReadLocalInput()
{
// get controller data of the first controller (0 internally)
Netplay::Input inp{0};
for (uint32_t i = 0; i < (uint32_t)DigitalController::Button::Count; i++)
{
if (s_net_session.m_net_input[0][i] >= 0.25f)
inp.button_data |= 1 << i;
}
return inp;
}
std::string& Netplay::Session::GetGamePath()
{
return s_net_session.m_game_path;
}
void Netplay::Session::SetGamePath(std::string& path)
{
s_net_session.m_game_path = path;
}
void Netplay::Session::SendMsg(const char* msg)
{
ggpo_client_chat(s_net_session.p_ggpo, msg);
}
GGPOErrorCode Netplay::Session::SyncInput(Netplay::Input inputs[2], int* disconnect_flags)
{
return ggpo_synchronize_input(s_net_session.p_ggpo, inputs, sizeof(Netplay::Input) * 2, disconnect_flags);
}
GGPOErrorCode Netplay::Session::AddLocalInput(Netplay::Input input)
{
return ggpo_add_local_input(s_net_session.p_ggpo, s_net_session.m_local_handle, &input, sizeof(Netplay::Input));
}
GGPONetworkStats& Netplay::Session::GetNetStats(int32_t handle)
{
ggpo_get_network_stats(s_net_session.p_ggpo, handle, &s_net_session.m_last_net_stats);
return s_net_session.m_last_net_stats;
}
int32_t Netplay::Session::GetPing()
{
const int handle = GetLocalHandle() == 1 ? 2 : 1;
ggpo_get_network_stats(s_net_session.p_ggpo, handle, &s_net_session.m_last_net_stats);
return s_net_session.m_last_net_stats.network.ping;
}
uint32_t Netplay::Session::GetMaxPrediction()
{
return s_net_session.m_max_pred;
}
GGPOPlayerHandle Netplay::Session::GetLocalHandle()
{
return s_net_session.m_local_handle;
}
void Netplay::Session::SetInputs(Netplay::Input inputs[2])
{
for (u32 i = 0; i < 2; i++)
{
auto cont = Pad::GetController(i);
std::bitset<sizeof(u32) * 8> buttonBits(inputs[i].button_data);
for (u32 j = 0; j < (u32)DigitalController::Button::Count; j++)
cont->SetBindState(j, buttonBits.test(j) ? 1.0f : 0.0f);
}
}
Netplay::LoopTimer* Netplay::Session::GetTimer()
{
return &s_net_session.m_timer;
}
uint16_t Netplay::Session::Fletcher16(uint8_t* data, int count)
{
uint16_t sum1 = 0;
uint16_t sum2 = 0;
int index;
for (index = 0; index < count; ++index)
{
sum1 = (sum1 + data[index]) % 255;
sum2 = (sum2 + sum1) % 255;
}
return (sum2 << 8) | sum1;
}
void Netplay::LoopTimer::Init(uint32_t fps, uint32_t frames_to_spread_wait)
{
m_us_per_game_loop = 1000000 / fps;
m_us_ahead = 0;
m_us_extra_to_wait = 0;
m_frames_to_spread_wait = frames_to_spread_wait;
m_last_advantage = 0.0f;
}
void Netplay::LoopTimer::OnGGPOTimeSyncEvent(float frames_ahead)
{
m_last_advantage = (1000.0f * frames_ahead / 60.0f);
m_last_advantage /= 2;
if (m_last_advantage < 0)
{
int t = 0;
t++;
}
m_us_extra_to_wait = (int)(m_last_advantage * 1000);
if (m_us_extra_to_wait)
{
m_us_extra_to_wait /= m_frames_to_spread_wait;
m_wait_count = m_frames_to_spread_wait;
}
}
int32_t Netplay::LoopTimer::UsToWaitThisLoop()
{
int32_t timetoWait = m_us_per_game_loop;
if (m_wait_count)
{
timetoWait += m_us_extra_to_wait;
m_wait_count--;
if (!m_wait_count)
m_us_extra_to_wait = 0;
}
return timetoWait;
}

102
src/core/netplay.h Normal file
View File

@ -0,0 +1,102 @@
#pragma once
#ifndef _NETPLAY_H
#define _NETPLAY_H
#include <array>
#include <ggponet.h>
#include <stdint.h>
#include <string.h>
#include <string>
#include "common/timer.h"
#include "digital_controller.h"
#include "types.h"
// C GGPO Event Callbacks. Should be defined in system.cpp
extern "C" {
bool NpAdvFrameCb(void* ctx, int flags);
bool NpSaveFrameCb(void* ctx, uint8_t** buffer, int* len, int* checksum, int frame);
bool NpLoadFrameCb(void* ctx, uint8_t* buffer, int len, int rb_frames, int frame_to_load);
bool NpBeginGameCb(void* ctx, const char* game_name);
void NpFreeBuffCb(void* ctx, void* buffer);
bool NpOnEventCb(void* ctx, GGPOEvent* ev);
}
namespace Netplay {
struct Input
{
uint32_t button_data;
};
struct LoopTimer
{
public:
void Init(uint32_t fps, uint32_t frames_to_spread_wait);
void OnGGPOTimeSyncEvent(float frames_ahead);
// Call every loop, to get the amount of time the current iteration of gameloop should take
int32_t UsToWaitThisLoop();
private:
float m_last_advantage = 0.0f;
int32_t m_us_per_game_loop = 0;
int32_t m_us_ahead = 0;
int32_t m_us_extra_to_wait = 0;
int32_t m_frames_to_spread_wait = 0;
int32_t m_wait_count = 0;
};
class Session
{
public:
Session();
~Session();
// l = local, r = remote
static int32_t Start(int32_t lhandle, uint16_t lport, std::string& raddr, uint16_t rport, int32_t ldelay,
uint32_t pred);
static void Close();
static bool IsActive();
static void RunIdle();
static void AdvanceFrame(uint16_t checksum = 0);
static void RunFrame(int32_t& waitTime);
static int32_t CurrentFrame();
static void CollectInput(uint32_t slot, uint32_t bind, float value);
static Netplay::Input ReadLocalInput();
static std::string& GetGamePath();
static void SetGamePath(std::string& path);
static void SendMsg(const char* msg);
static GGPOErrorCode SyncInput(Netplay::Input inputs[2], int* disconnect_flags);
static GGPOErrorCode AddLocalInput(Netplay::Input input);
static GGPONetworkStats& GetNetStats(int32_t handle);
static int32_t GetPing();
static uint32_t GetMaxPrediction();
static GGPOPlayerHandle GetLocalHandle();
static void SetInputs(Netplay::Input inputs[2]);
static Netplay::LoopTimer* GetTimer();
static uint16_t Fletcher16(uint8_t* data, int count);
private:
Netplay::LoopTimer m_timer;
std::string m_game_path;
uint32_t m_max_pred = 0;
GGPOPlayerHandle m_local_handle = GGPO_INVALID_HANDLE;
GGPONetworkStats m_last_net_stats{};
GGPOSession* p_ggpo = nullptr;
std::array<std::array<float, 32>, NUM_CONTROLLER_AND_CARD_PORTS> m_net_input;
};
} // namespace Netplay
// Netplay Instance
static Netplay::Session s_net_session = Netplay::Session();
#endif // !_NETPLAY_H

View File

@ -169,12 +169,12 @@ void Pad::Reset()
bool Pad::ShouldAvoidSavingToState()
{
// Currently only runahead, will also be used for netplay.
return g_settings.IsRunaheadEnabled();
return g_settings.IsRunaheadEnabled() || Netplay::Session::IsActive();
}
u32 Pad::GetMaximumRollbackFrames()
{
return g_settings.runahead_frames;
return (Netplay::Session::IsActive() ? Netplay::Session::GetMaxPrediction() : g_settings.runahead_frames);
}
bool Pad::DoStateController(StateWrapper& sw, u32 i)

View File

@ -33,6 +33,7 @@
#include "mdec.h"
#include "memory_card.h"
#include "multitap.h"
#include "netplay.h"
#include "pad.h"
#include "pgxp.h"
#include "psf_loader.h"
@ -207,6 +208,8 @@ static std::deque<MemorySaveState> s_runahead_states;
static bool s_runahead_replay_pending = false;
static u32 s_runahead_frames = 0;
static std::deque<MemorySaveState> s_netplay_states;
static TinyString GetTimestampStringForFileName()
{
return TinyString::FromFmt("{:%Y-%m-%d_%H-%M-%S}", fmt::localtime(std::time(nullptr)));
@ -1538,7 +1541,7 @@ void System::Execute()
// this can shut us down
Host::PumpMessagesOnCPUThread();
if (!IsValid())
if (!IsValid() || Netplay::Session::IsActive())
return;
if (s_frame_step_request)
@ -1565,6 +1568,38 @@ void System::Execute()
}
}
void System::ExecuteNetplay()
{
// frame timing
s32 timeToWait;
std::chrono::steady_clock::time_point start, next, now;
start = next = now = std::chrono::steady_clock::now();
while (Netplay::Session::IsActive() && System::IsRunning())
{
now = std::chrono::steady_clock::now();
if (now >= next)
{
Netplay::Session::RunFrame(timeToWait);
next = now + std::chrono::microseconds(timeToWait);
s_next_frame_time += timeToWait;
// this can shut us down
Host::PumpMessagesOnCPUThread();
if (!IsValid() || !Netplay::Session::IsActive())
break;
const bool skip_present = g_host_display->ShouldSkipDisplayingFrame();
Host::RenderDisplay(skip_present);
if (!skip_present && g_host_display->IsGPUTimingEnabled())
{
s_accumulated_gpu_time += g_host_display->GetAndResetAccumulatedGPUTime();
s_presents_since_last_update++;
}
System::UpdatePerformanceCounters();
}
}
}
void System::RecreateSystem()
{
Assert(!IsShutdown());
@ -4431,3 +4466,163 @@ void System::SetTimerResolutionIncreased(bool enabled)
timeEndPeriod(1);
#endif
}
void System::StartNetplaySession(s32 local_handle, u16 local_port, std::string& remote_addr, u16 remote_port,
s32 input_delay, std::string& game_path)
{
// dont want to start a session when theres already one going on.
if (Netplay::Session::IsActive())
return;
// set game path for later loading during the begin game callback
Netplay::Session::SetGamePath(game_path);
// set netplay timer
const u32 fps = (s_region == ConsoleRegion::PAL ? 50 : 60);
Netplay::Session::GetTimer()->Init(fps, 180);
// create session
int result = Netplay::Session::Start(local_handle, local_port, remote_addr, remote_port, input_delay, 8);
if (result != GGPO_OK)
{
Log_ErrorPrintf("Failed to Create Netplay Session! Error: %d", result);
}
}
void System::StopNetplaySession()
{
if (!Netplay::Session::IsActive())
return;
s_netplay_states.clear();
Netplay::Session::Close();
}
void System::NetplayAdvanceFrame(Netplay::Input inputs[], int disconnect_flags)
{
Netplay::Session::SetInputs(inputs);
System::DoRunFrame();
Netplay::Session::AdvanceFrame();
}
bool NpBeginGameCb(void* ctx, const char* game_name)
{
// close system if its already running
if (System::IsValid())
System::ShutdownSystem(false);
// fast boot the selected game and wait for the other player
auto param = SystemBootParameters(Netplay::Session::GetGamePath());
param.override_fast_boot = true;
if (!System::BootSystem(param))
{
System::StopNetplaySession();
return false;
}
// Fast Forward to Game Start
SPU::SetAudioOutputMuted(true);
while (s_internal_frame_number < 2)
System::DoRunFrame();
SPU::SetAudioOutputMuted(false);
return true;
}
bool NpAdvFrameCb(void* ctx, int flags)
{
Netplay::Input inputs[2] = {};
int disconnectFlags;
Netplay::Session::SyncInput(inputs, &disconnectFlags);
System::NetplayAdvanceFrame(inputs, disconnectFlags);
return true;
}
bool NpSaveFrameCb(void* ctx, uint8_t** buffer, int* len, int* checksum, int frame)
{
bool result = false;
// give ggpo something so it doesnt complain.
u8 dummyData = 43;
*len = sizeof(u8);
*buffer = (unsigned char*)malloc(*len);
if (!*buffer)
return false;
memcpy(*buffer, &dummyData, *len);
// store state for later.
int pred = Netplay::Session::GetMaxPrediction();
if (frame < pred && s_netplay_states.size() < pred)
{
MemorySaveState save;
result = System::SaveMemoryState(&save);
s_netplay_states.push_back(std::move(save));
}
else
{
// reuse streams
result = System::SaveMemoryState(&s_netplay_states[frame % pred]);
}
return result;
}
bool NpLoadFrameCb(void* ctx, uint8_t* buffer, int len, int rb_frames, int frame_to_load)
{
// Disable Audio For upcoming rollback
SPU::SetAudioOutputMuted(true);
return System::LoadMemoryState(s_netplay_states[frame_to_load % Netplay::Session::GetMaxPrediction()]);
}
bool NpOnEventCb(void* ctx, GGPOEvent* ev)
{
char buff[128];
std::string msg;
switch (ev->code)
{
case GGPOEventCode::GGPO_EVENTCODE_CONNECTED_TO_PEER:
sprintf(buff, "Netplay Connected To Player: %d", ev->u.connected.player);
msg = buff;
break;
case GGPOEventCode::GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER:
sprintf(buff, "Netplay Synchronzing: %d/%d", ev->u.synchronizing.count, ev->u.synchronizing.total);
msg = buff;
break;
case GGPOEventCode::GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER:
sprintf(buff, "Netplay Synchronized With Player: %d", ev->u.synchronized.player);
msg = buff;
break;
case GGPOEventCode::GGPO_EVENTCODE_DISCONNECTED_FROM_PEER:
sprintf(buff, "Netplay Player: %d Disconnected", ev->u.disconnected.player);
msg = buff;
break;
case GGPOEventCode::GGPO_EVENTCODE_RUNNING:
msg = "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;
break;
case GGPOEventCode::GGPO_EVENTCODE_CONNECTION_RESUMED:
sprintf(buff, "Netplay Player: %d Connection Resumed", ev->u.connection_resumed.player);
msg = buff;
break;
case GGPOEventCode::GGPO_EVENTCODE_CHAT:
sprintf(buff, "%s", ev->u.chat.msg);
msg = buff;
break;
case GGPOEventCode::GGPO_EVENTCODE_TIMESYNC:
Netplay::Session::GetTimer()->OnGGPOTimeSyncEvent(ev->u.timesync.frames_ahead);
break;
case GGPOEventCode::GGPO_EVENTCODE_DESYNC:
sprintf(buff, "Netplay Desync Detected!: Frame: %d, L:%u, R:%u", ev->u.desync.nFrameOfDesync,
ev->u.desync.ourCheckSum, ev->u.desync.remoteChecksum);
msg = buff;
break;
default:
sprintf(buff, "Netplay Event Code: %d", ev->code);
msg = buff;
}
if (!msg.empty())
{
Host::OnNetplayMessage(msg);
Log_InfoPrintf("%s", msg.c_str());
}
return true;
}
void NpFreeBuffCb(void* ctx, void* buffer)
{
free(buffer);
}

View File

@ -3,6 +3,7 @@
#pragma once
#include "common/timer.h"
#include "netplay.h"
#include "settings.h"
#include "timing_event.h"
#include "types.h"
@ -19,11 +20,10 @@ class Controller;
struct CheatCode;
class CheatList;
namespace BIOS
{
namespace BIOS {
struct ImageInfo;
struct Hash;
}
} // namespace BIOS
struct SystemBootParameters
{
@ -227,6 +227,9 @@ bool SaveResumeState();
/// Runs the VM until the CPU execution is canceled.
void Execute();
/// Runs the VM and netplay loop. when the netplay loop cancels it switches to normal execute mode.
void ExecuteNetplay();
/// Switches the GPU renderer by saving state, recreating the display window, and restoring state (if needed).
void RecreateSystem();
@ -452,6 +455,11 @@ void UpdateMemorySaveStateSettings();
bool LoadRewindState(u32 skip_saves = 0, bool consume_state = true);
void SetRunaheadReplayFlag();
/// Netplay
void StartNetplaySession(s32 local_handle, u16 local_port, std::string& remote_addr, u16 remote_port, s32 input_delay,
std::string& game_path);
void StopNetplaySession();
void NetplayAdvanceFrame(Netplay::Input inputs[], int disconnect_flags);
} // namespace System
namespace Host {
@ -500,4 +508,6 @@ bool IsFullscreen();
/// Alters fullscreen state of hosting application.
void SetFullscreen(bool enabled);
// netplay
void OnNetplayMessage(std::string& message);
} // namespace Host

View File

@ -50,6 +50,7 @@
#ifdef _WIN32
#include "common/windows_headers.h"
#include <Dbt.h>
#include <WinSock2.h>
#endif
Log_SetChannel(MainWindow);
@ -105,6 +106,12 @@ MainWindow::MainWindow() : QMainWindow(nullptr)
#if !defined(_WIN32) && !defined(__APPLE__)
s_use_central_widget = DisplayContainer::isRunningOnWayland();
#endif
#if defined(_WIN32)
// Setup WinSock
WSADATA wd = {};
WSAStartup(MAKEWORD(2, 2), &wd);
#endif
}
MainWindow::~MainWindow()
@ -119,6 +126,8 @@ MainWindow::~MainWindow()
#ifdef _WIN32
unregisterForDeviceNotifications();
// Cleanup WinSock
WSACleanup();
#endif
#ifdef __APPLE__
FrontendCommon::RemoveThemeChangeHandler(this);
@ -1556,9 +1565,13 @@ void MainWindow::setupAdditionalUi()
m_status_fps_widget->hide();
m_status_vps_widget = new QLabel(m_ui.statusBar);
m_status_vps_widget->setFixedSize(125, 16);
m_status_vps_widget->setFixedSize(120, 16);
m_status_vps_widget->hide();
m_status_ping_widget = new QLabel(m_ui.statusBar);
m_status_ping_widget->setFixedSize(110, 16);
m_status_ping_widget->hide();
m_settings_toolbar_menu = new QMenu(m_ui.toolBar);
m_settings_toolbar_menu->addAction(m_ui.actionSettings);
m_settings_toolbar_menu->addAction(m_ui.actionViewGameProperties);
@ -1764,6 +1777,7 @@ void MainWindow::updateStatusBarWidgetVisibility()
Update(m_status_resolution_widget, s_system_valid && !s_system_paused, 0);
Update(m_status_fps_widget, s_system_valid && !s_system_paused, 0);
Update(m_status_vps_widget, s_system_valid && !s_system_paused, 0);
Update(m_status_ping_widget, s_system_valid && !s_system_paused && m_netplay_window != nullptr, 0);
}
void MainWindow::updateWindowTitle()
@ -2089,6 +2103,9 @@ void MainWindow::connectSignals()
addThemeToMenu(tr("Dark Fusion (Blue)"), QStringLiteral("darkfusionblue"));
addThemeToMenu(tr("QDarkStyle"), QStringLiteral("qdarkstyle"));
updateMenuSelectedTheme();
// Netplay UI , TODO
connect(m_ui.actionCreateNetplaySession, &QAction::triggered, this, &MainWindow::onNetplaySessionCreated);
}
void MainWindow::addThemeToMenu(const QString& name, const QString& key)
@ -2752,6 +2769,28 @@ void MainWindow::onCPUDebuggerClosed()
m_debugger_window = nullptr;
}
void MainWindow::onNetplaySessionCreated()
{
Assert(!m_netplay_window);
m_netplay_window = new NetplayWidget(this);
m_netplay_window->setWindowIcon(windowIcon());
m_netplay_window->setWindowTitle("Netplay Session");
m_netplay_window->setWindowFlags(windowFlags() | Qt::WindowMinimizeButtonHint);
m_netplay_window->show();
m_ui.menuNetplay->setDisabled(true);
connect(m_netplay_window, &NetplayWidget::finished, [this]() {
Assert(m_netplay_window);
m_netplay_window->deleteLater();
m_netplay_window = nullptr;
m_ui.menuNetplay->setDisabled(false);
});
}
void MainWindow::onToolsOpenDataDirectoryTriggered()
{
QtUtils::OpenURL(this, QUrl::fromLocalFile(QString::fromStdString(EmuFolders::DataRoot)));

View File

@ -16,6 +16,7 @@
#include "displaywidget.h"
#include "settingsdialog.h"
#include "ui_mainwindow.h"
#include <netplaywidget.h>
class QLabel;
class QThread;
@ -89,6 +90,7 @@ public:
ALWAYS_INLINE QLabel* getStatusResolutionWidget() const { return m_status_resolution_widget; }
ALWAYS_INLINE QLabel* getStatusFPSWidget() const { return m_status_fps_widget; }
ALWAYS_INLINE QLabel* getStatusVPSWidget() const { return m_status_vps_widget; }
ALWAYS_INLINE QLabel* getStatusPingWidget() const { return m_status_ping_widget; }
public Q_SLOTS:
/// Updates debug menu visibility (hides if disabled).
@ -168,6 +170,8 @@ private Q_SLOTS:
void openCPUDebugger();
void onCPUDebuggerClosed();
void onNetplaySessionCreated();
protected:
void showEvent(QShowEvent* event) override;
void closeEvent(QCloseEvent* event) override;
@ -262,6 +266,7 @@ private:
QLabel* m_status_renderer_widget = nullptr;
QLabel* m_status_fps_widget = nullptr;
QLabel* m_status_vps_widget = nullptr;
QLabel* m_status_ping_widget = nullptr;
QLabel* m_status_resolution_widget = nullptr;
QMenu* m_settings_toolbar_menu = nullptr;
@ -273,6 +278,7 @@ private:
MemoryCardEditorDialog* m_memory_card_editor_dialog = nullptr;
CheatManagerDialog* m_cheat_manager_dialog = nullptr;
DebuggerWindow* m_debugger_window = nullptr;
NetplayWidget* m_netplay_window = nullptr;
std::string m_current_game_title;
std::string m_current_game_serial;

View File

@ -17,7 +17,7 @@
<string>DuckStation</string>
</property>
<property name="windowIcon">
<iconset>
<iconset resource="resources/resources.qrc">
<normaloff>:/icons/duck.png</normaloff>:/icons/duck.png</iconset>
</property>
<widget class="QStackedWidget" name="mainContainer"/>
@ -27,7 +27,7 @@
<x>0</x>
<y>0</y>
<width>800</width>
<height>22</height>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menuSystem">
@ -237,8 +237,15 @@
<addaction name="separator"/>
<addaction name="actionOpenDataDirectory"/>
</widget>
<widget class="QMenu" name="menuNetplay">
<property name="title">
<string>Netplay</string>
</property>
<addaction name="actionCreateNetplaySession"/>
</widget>
<addaction name="menuSystem"/>
<addaction name="menuSettings"/>
<addaction name="menuNetplay"/>
<addaction name="menu_View"/>
<addaction name="menu_Tools"/>
<addaction name="menuDebug"/>
@ -472,7 +479,7 @@
</action>
<action name="actionGitHubRepository">
<property name="icon">
<iconset>
<iconset resource="resources/resources.qrc">
<normaloff>:/icons/github.png</normaloff>:/icons/github.png</iconset>
</property>
<property name="text">
@ -481,7 +488,7 @@
</action>
<action name="actionIssueTracker">
<property name="icon">
<iconset>
<iconset resource="resources/resources.qrc">
<normaloff>:/icons/IssueTracker.png</normaloff>:/icons/IssueTracker.png</iconset>
</property>
<property name="text">
@ -490,7 +497,7 @@
</action>
<action name="actionDiscordServer">
<property name="icon">
<iconset>
<iconset resource="resources/resources.qrc">
<normaloff>:/icons/discord.png</normaloff>:/icons/discord.png</iconset>
</property>
<property name="text">
@ -508,7 +515,7 @@
</action>
<action name="actionAboutQt">
<property name="icon">
<iconset>
<iconset resource="resources/resources.qrc">
<normaloff>:/icons/QT.png</normaloff>:/icons/QT.png</iconset>
</property>
<property name="text">
@ -517,7 +524,7 @@
</action>
<action name="actionAbout">
<property name="icon">
<iconset>
<iconset resource="resources/resources.qrc">
<normaloff>:/icons/duck_64.png</normaloff>:/icons/duck_64.png</iconset>
</property>
<property name="text">
@ -976,6 +983,11 @@
<string>Cover Downloader</string>
</property>
</action>
<action name="actionCreateNetplaySession">
<property name="text">
<string>Create Session</string>
</property>
</action>
</widget>
<resources>
<include location="resources/resources.qrc"/>

View File

@ -0,0 +1,210 @@
#include "netplaywidget.h"
#include "ui_netplaywidget.h"
#include <QtWidgets/qmessagebox.h>
#include <common/log.h>
#include <core/controller.h>
#include <qthost.h>
Log_SetChannel(NetplayWidget);
NetplayWidget::NetplayWidget(QWidget* parent) : QDialog(parent), m_ui(new Ui::NetplayWidget)
{
m_ui->setupUi(this);
FillGameList();
SetupConnections();
SetupConstraints();
CheckControllersSet();
}
NetplayWidget::~NetplayWidget()
{
StopSession();
delete m_ui;
}
void NetplayWidget::FillGameList()
{
// Get all games and fill the list later to know which game to boot.
s32 numGames = GameList::GetEntryCount();
for (s32 i = 0; i < numGames; i++)
{
const auto& entry = GameList::GetEntryByIndex(i);
std::string baseFilename = entry->path.substr(entry->path.find_last_of("/\\") + 1);
m_ui->cbSelectedGame->addItem(
QString::fromStdString("[" + entry->serial + "] " + entry->title + " | " + baseFilename));
m_available_games.push_back(entry->path);
}
}
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
auto msg = m_ui->tbNetplayChat->toPlainText().trimmed();
QString completeMsg = m_ui->lePlayerName->text().trimmed() + ": " + msg;
if (completeMsg.length() > 120)
return;
m_ui->lwChatWindow->addItem(completeMsg);
m_ui->tbNetplayChat->clear();
if (!g_emu_thread)
return;
g_emu_thread->sendNetplayMessage(completeMsg);
});
// switch between DIRECT IP and traversal options
connect(m_ui->cbConnMode, &QComboBox::currentIndexChanged, [this]() {
// zero is DIRECT IP mode
const bool action = (m_ui->cbConnMode->currentIndex() == 0 ? true : false);
m_ui->frDirectIP->setVisible(action);
m_ui->frDirectIP->setEnabled(action);
m_ui->btnStartSession->setEnabled(action);
m_ui->tabTraversal->setEnabled(!action);
m_ui->btnTraversalJoin->setEnabled(!action);
m_ui->btnTraversalHost->setEnabled(!action);
});
// actions to be taken when stopping a session.
auto fnOnStopSession = [this]() {
m_ui->btnSendMsg->setEnabled(false);
m_ui->tbNetplayChat->setEnabled(false);
m_ui->btnStopSession->setEnabled(false);
m_ui->btnStartSession->setEnabled(true);
m_ui->btnTraversalHost->setEnabled(true);
m_ui->btnTraversalJoin->setEnabled(true);
m_ui->lblHostCodeResult->setText("XXXXXXXXX-");
StopSession();
};
// check session when start button pressed if there is the needed info depending on the connection mode
auto fnCheckValid = [this, fnOnStopSession]() {
const bool action = (m_ui->cbConnMode->currentIndex() == 0 ? true : false);
if (CheckInfoValid(action))
{
m_ui->btnSendMsg->setEnabled(true);
m_ui->tbNetplayChat->setEnabled(true);
m_ui->btnStopSession->setEnabled(true);
m_ui->btnStartSession->setEnabled(false);
m_ui->btnTraversalHost->setEnabled(false);
m_ui->btnTraversalJoin->setEnabled(false);
if (!StartSession(action))
fnOnStopSession();
}
};
connect(m_ui->btnStartSession, &QPushButton::pressed, fnCheckValid);
connect(m_ui->btnTraversalJoin, &QPushButton::pressed, fnCheckValid);
connect(m_ui->btnTraversalHost, &QPushButton::pressed, fnCheckValid);
// when pressed revert back to the previous ui state so people can start a new session.
connect(m_ui->btnStopSession, &QPushButton::pressed, fnOnStopSession);
}
void NetplayWidget::SetupConstraints()
{
m_ui->lwChatWindow->setWordWrap(true);
m_ui->sbLocalPort->setRange(0, 65535);
m_ui->sbRemotePort->setRange(0, 65535);
m_ui->sbInputDelay->setRange(0, 10);
m_ui->leRemoteAddr->setMaxLength(15);
m_ui->lePlayerName->setMaxLength(12);
QString IpRange = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])";
QRegularExpression IpRegex("^" + IpRange + "(\\." + IpRange + ")" + "(\\." + IpRange + ")" + "(\\." + IpRange + ")$");
QRegularExpressionValidator* ipValidator = new QRegularExpressionValidator(IpRegex, this);
m_ui->leRemoteAddr->setValidator(ipValidator);
}
bool NetplayWidget::CheckInfoValid(bool direct_ip)
{
if (!direct_ip)
{
QMessageBox errBox;
errBox.setFixedSize(500, 200);
errBox.information(this, "Netplay Session", "Traversal Mode is not supported yet!");
errBox.show();
return false;
}
bool err = false;
// check nickname, game selected and player selected.
if (m_ui->lePlayerName->text().trimmed().isEmpty() || m_ui->cbSelectedGame->currentIndex() == 0 ||
m_ui->cbLocalPlayer->currentIndex() == 0)
err = true;
// check if direct ip details have been filled in
if (direct_ip && (m_ui->leRemoteAddr->text().trimmed().isEmpty() || m_ui->sbRemotePort->value() == 0 ||
m_ui->sbLocalPort->value() == 0))
err = true;
// check if host code has been filled in
if (!direct_ip && m_ui->leHostCode->text().trimmed().isEmpty() &&
m_ui->tabTraversal->currentWidget() == m_ui->tabJoin)
err = true;
// if an err has been found throw
if (err)
{
QMessageBox errBox;
errBox.setFixedSize(500, 200);
errBox.information(this, "Netplay Session", "Please fill in all the needed fields!");
errBox.show();
return !err;
}
// check if controllers are set
err = !CheckControllersSet();
// everything filled in. inverse cuz we would like to return true if the info is valid.
return !err;
}
bool NetplayWidget::CheckControllersSet()
{
bool err = false;
// check whether its controllers are set right
for (u32 i = 0; i < 2; i++)
{
const Controller::ControllerInfo* cinfo = Controller::GetControllerInfo(g_settings.controller_types[i]);
if (!cinfo || cinfo->type != ControllerType::DigitalController)
{
err = true;
}
}
// if an err has been found throw popup
if (err)
{
QMessageBox errBox;
errBox.information(this, "Netplay Session",
"Please make sure the controllers are both enabled and set as Digital Controllers");
errBox.setFixedSize(500, 200);
errBox.show();
}
// controllers are set right
return !err;
}
bool NetplayWidget::StartSession(bool direct_ip)
{
if (!g_emu_thread)
return false;
int localHandle = m_ui->cbLocalPlayer->currentIndex();
int inputDelay = m_ui->sbInputDelay->value();
quint16 localPort = m_ui->sbLocalPort->value();
const QString& remoteAddr = m_ui->leRemoteAddr->text();
quint16 remotePort = m_ui->sbRemotePort->value();
const QString& gamePath = QString::fromStdString(m_available_games[m_ui->cbSelectedGame->currentIndex() - 1]);
if (!direct_ip)
return false; // TODO: Handle Nat Traversal and use that information by overriding the information above.
g_emu_thread->startNetplaySession(localHandle, localPort, remoteAddr, remotePort, inputDelay, gamePath);
return true;
}
void NetplayWidget::StopSession()
{
if (!g_emu_thread)
return;
g_emu_thread->stopNetplaySession();
}
void NetplayWidget::OnMsgReceived(const QString& msg)
{
m_ui->lwChatWindow->addItem(msg);
}

View File

@ -0,0 +1,34 @@
#ifndef NETPLAYWIDGET_H
#define NETPLAYWIDGET_H
#include <QtWidgets/QDialog>
#include <frontend-common/game_list.h>
namespace Ui {
class NetplayWidget;
}
class NetplayWidget : public QDialog
{
Q_OBJECT
public:
explicit NetplayWidget(QWidget* parent = nullptr);
~NetplayWidget();
private:
void FillGameList();
void SetupConnections();
void SetupConstraints();
bool CheckInfoValid(bool direct_ip);
bool CheckControllersSet();
bool StartSession(bool direct_ip);
void StopSession();
void OnMsgReceived(const QString& msg);
private:
Ui::NetplayWidget* m_ui;
std::vector<std::string> m_available_games;
};
#endif // NETPLAYWIDGET_H

View File

@ -0,0 +1,710 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NetplayWidget</class>
<widget class="QDialog" name="NetplayWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>700</width>
<height>448</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>700</width>
<height>448</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>700</width>
<height>448</height>
</size>
</property>
<property name="font">
<font>
<kerning>true</kerning>
</font>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="modal">
<bool>false</bool>
</property>
<widget class="QFrame" name="frMain">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>701</width>
<height>461</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>701</width>
<height>461</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>701</width>
<height>461</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<widget class="Line" name="line_2">
<property name="geometry">
<rect>
<x>0</x>
<y>40</y>
<width>711</width>
<height>20</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
<widget class="QPlainTextEdit" name="tbNetplayChat">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>380</x>
<y>400</y>
<width>231</width>
<height>41</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="plainText">
<string>...
</string>
</property>
</widget>
<widget class="QPushButton" name="btnSendMsg">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>620</x>
<y>400</y>
<width>61</width>
<height>41</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Send</string>
</property>
</widget>
<widget class="Line" name="line">
<property name="geometry">
<rect>
<x>350</x>
<y>50</y>
<width>21</width>
<height>411</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>9</pointsize>
<italic>false</italic>
<bold>false</bold>
<underline>false</underline>
<kerning>true</kerning>
</font>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
<widget class="QListWidget" name="lwChatWindow">
<property name="geometry">
<rect>
<x>380</x>
<y>60</y>
<width>301</width>
<height>331</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAsNeeded</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<item>
<property name="text">
<string/>
</property>
</item>
</widget>
<widget class="QFrame" name="frDirectIP">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>9</x>
<y>220</y>
<width>341</width>
<height>171</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<widget class="QWidget" name="formLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>0</y>
<width>321</width>
<height>161</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="2" column="1">
<widget class="QSpinBox" name="sbRemotePort">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="stepType">
<enum>QAbstractSpinBox::DefaultStepType</enum>
</property>
<property name="value">
<number>2000</number>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QLineEdit" name="leRemoteAddr">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="lblLocalPort">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Local Port :</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lblRemoteAddr">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Remote Address :</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lblRemotePort">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Remote Port :</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="btnStartSession">
<property name="enabled">
<bool>true</bool>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Start Session</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QSpinBox" name="sbLocalPort">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="stepType">
<enum>QAbstractSpinBox::DefaultStepType</enum>
</property>
<property name="value">
<number>1000</number>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QLabel" name="lblGame">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>61</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Game : </string>
</property>
</widget>
<widget class="QComboBox" name="cbSelectedGame">
<property name="geometry">
<rect>
<x>70</x>
<y>10</y>
<width>611</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
<bold>true</bold>
<kerning>true</kerning>
</font>
</property>
<property name="maxVisibleItems">
<number>8</number>
</property>
<item>
<property name="text">
<string>SELECT A GAME</string>
</property>
</item>
</widget>
<widget class="QPushButton" name="btnStopSession">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>100</x>
<y>400</y>
<width>161</width>
<height>41</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Stop Session</string>
</property>
</widget>
<widget class="QTabWidget" name="tabTraversal">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>20</x>
<y>220</y>
<width>321</width>
<height>171</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tabJoin">
<attribute name="title">
<string>Join</string>
</attribute>
<widget class="QPushButton" name="btnTraversalJoin">
<property name="geometry">
<rect>
<x>220</x>
<y>60</y>
<width>81</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Join</string>
</property>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget_2">
<property name="geometry">
<rect>
<x>10</x>
<y>20</y>
<width>291</width>
<height>28</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="lblHostCode">
<property name="enabled">
<bool>true</bool>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Host Code :</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="leHostCode">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="tabHost">
<property name="font">
<font>
<pointsize>10</pointsize>
<kerning>true</kerning>
</font>
</property>
<attribute name="title">
<string>Host</string>
</attribute>
<widget class="QPushButton" name="btnTraversalHost">
<property name="geometry">
<rect>
<x>10</x>
<y>20</y>
<width>81</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Host</string>
</property>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>60</y>
<width>281</width>
<height>31</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="lblHostCode2">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Host Code :</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblHostCodeResult">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>XXXXXXXXX-</string>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>60</y>
<width>321</width>
<height>151</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<widget class="QLabel" name="lblPlayer">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Player :</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="cbLocalPlayer">
<property name="font">
<font>
<pointsize>11</pointsize>
<bold>true</bold>
<kerning>true</kerning>
</font>
</property>
<item>
<property name="text">
<string>NOT SET</string>
</property>
</item>
<item>
<property name="text">
<string>1</string>
</property>
</item>
<item>
<property name="text">
<string>2</string>
</property>
</item>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lblInputDelay">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Input Delay :</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lePlayerName">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Duck!</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="sbInputDelay">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="maximum">
<number>30</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="stepType">
<enum>QAbstractSpinBox::DefaultStepType</enum>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="cbConnMode">
<property name="font">
<font>
<pointsize>11</pointsize>
<bold>true</bold>
<kerning>true</kerning>
</font>
</property>
<item>
<property name="text">
<string>DIRECT IP</string>
</property>
</item>
<item>
<property name="text">
<string>TRAVERSAL</string>
</property>
</item>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="lblConnMode">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Connection Mode :</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lblPlayerName">
<property name="font">
<font>
<pointsize>11</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>Nickname : </string>
</property>
</widget>
</item>
</layout>
</widget>
<zorder>tabTraversal</zorder>
<zorder>frDirectIP</zorder>
<zorder>line_2</zorder>
<zorder>tbNetplayChat</zorder>
<zorder>btnSendMsg</zorder>
<zorder>line</zorder>
<zorder>lwChatWindow</zorder>
<zorder>lblGame</zorder>
<zorder>cbSelectedGame</zorder>
<zorder>btnStopSession</zorder>
<zorder>gridLayoutWidget</zorder>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -461,6 +461,12 @@ 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())
@ -1065,6 +1071,48 @@ void EmuThread::reloadPostProcessingShaders()
System::ReloadPostProcessingShaders();
}
void EmuThread::startNetplaySession(int local_handle, quint16 local_port, const QString& remote_addr,
quint16 remote_port, int input_delay, const QString& game_path)
{
if (!isOnThread())
{
QMetaObject::invokeMethod(this, "startNetplaySession", Qt::QueuedConnection, Q_ARG(int, local_handle),
Q_ARG(quint16, local_port), Q_ARG(const QString&, remote_addr),
Q_ARG(quint16, remote_port), Q_ARG(int, input_delay), Q_ARG(const QString&, game_path));
return;
}
// disable block linking and disable rewind and runahead during a netplay session
g_settings.cpu_recompiler_block_linking = false;
g_settings.rewind_enable = false;
g_settings.runahead_frames = 0;
Log_WarningPrintf("Disabling block linking, runahead and rewind due to rollback.");
auto remAddr = remote_addr.trimmed().toStdString();
auto gamePath = game_path.trimmed().toStdString();
System::StartNetplaySession(local_handle, local_port, remAddr, remote_port, input_delay, gamePath);
}
void EmuThread::sendNetplayMessage(const QString& message)
{
if (!isOnThread())
{
QMetaObject::invokeMethod(this, "sendNetplayMessage", Qt::QueuedConnection, Q_ARG(const QString&, message));
return;
}
Netplay::Session::SendMsg(message.toStdString().c_str());
}
void EmuThread::stopNetplaySession()
{
if (!isOnThread())
{
QMetaObject::invokeMethod(this, "stopNetplaySession", Qt::QueuedConnection);
return;
}
System::StopNetplaySession();
}
void EmuThread::runOnEmuThread(std::function<void()> callback)
{
callback();
@ -1416,7 +1464,10 @@ void EmuThread::run()
{
if (System::IsRunning())
{
System::Execute();
if (Netplay::Session::IsActive())
System::ExecuteNetplay();
else
System::Execute();
}
else
{
@ -1659,6 +1710,14 @@ void EmuThread::updatePerformanceCounters()
m_last_speed = speed;
m_last_video_fps = vfps;
}
const s32 ping = Netplay::Session::GetPing();
if (m_last_ping != ping)
{
QMetaObject::invokeMethod(g_main_window->getStatusPingWidget(), "setText", Qt::QueuedConnection,
Q_ARG(const QString&, tr("Netplay Ping: %1 ").arg(ping, 0, 'f', 0)));
m_last_ping = ping;
}
}
void EmuThread::resetPerformanceCounters()

View File

@ -143,6 +143,7 @@ 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);
@ -187,6 +188,10 @@ public Q_SLOTS:
void setCheatEnabled(quint32 index, bool enabled);
void applyCheat(quint32 index);
void reloadPostProcessingShaders();
void startNetplaySession(int local_handle, quint16 local_port, const QString& remote_addr, quint16 remote_port,
int input_delay, const QString& game_path);
void stopNetplaySession();
void sendNetplayMessage(const QString& message);
private Q_SLOTS:
void stopInThread();
@ -232,6 +237,7 @@ private:
float m_last_video_fps = std::numeric_limits<float>::infinity();
u32 m_last_render_width = std::numeric_limits<u32>::max();
u32 m_last_render_height = std::numeric_limits<u32>::max();
u32 m_last_ping = std::numeric_limits<u32>::max();
GPURenderer m_last_renderer = GPURenderer::Count;
};

View File

@ -11,6 +11,7 @@
#include "core/controller.h"
#include "core/host.h"
#include "core/system.h"
#include "core/netplay.h"
#include "imgui_manager.h"
#include "input_source.h"
@ -713,6 +714,12 @@ void InputManager::AddPadBindings(SettingsInterface& si, const std::string& sect
if (!System::IsValid())
return;
if (Netplay::Session::IsActive())
{
Netplay::Session::CollectInput(pad_index, bind_index, value);
return;
}
Controller* c = System::GetController(pad_index);
if (c)
c->SetBindState(bind_index, value);