Netplay: Add work-in-progress rollback netplay implementation

Co-authored-by: Jamie Meyer <45072324+HeatXD@users.noreply.github.com>
This commit is contained in:
Stenzek 2023-08-30 18:23:47 +10:00
parent fd6aeefd93
commit 3293626898
30 changed files with 5044 additions and 33 deletions

View File

@ -62,6 +62,7 @@
<ClCompile Include="multitap.cpp" />
<ClCompile Include="guncon.cpp" />
<ClCompile Include="negcon.cpp" />
<ClCompile Include="netplay.cpp" />
<ClCompile Include="pad.cpp" />
<ClCompile Include="controller.cpp" />
<ClCompile Include="pcdrv.cpp" />
@ -138,6 +139,8 @@
<ClInclude Include="multitap.h" />
<ClInclude Include="guncon.h" />
<ClInclude Include="negcon.h" />
<ClInclude Include="netplay.h" />
<ClInclude Include="netplay_packets.h" />
<ClInclude Include="pad.h" />
<ClInclude Include="controller.h" />
<ClInclude Include="pcdrv.h" />

View File

@ -60,6 +60,7 @@
<ClCompile Include="host.cpp" />
<ClCompile Include="game_database.cpp" />
<ClCompile Include="pcdrv.cpp" />
<ClCompile Include="netplay.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="types.h" />
@ -127,5 +128,7 @@
<ClInclude Include="game_database.h" />
<ClInclude Include="input_types.h" />
<ClInclude Include="pcdrv.h" />
<ClInclude Include="netplay.h" />
<ClInclude Include="netplay_packets.h" />
</ItemGroup>
</Project>

2589
src/core/netplay.cpp Normal file

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,60 @@
#pragma once
#include "types.h"
#include <string>
namespace Netplay {
enum : s32
{
// Maximum number of emulated controllers.
MAX_PLAYERS = 2,
// Maximum number of spectators allowed to watch the session.
MAX_SPECTATORS = 4,
// Maximum netplay prediction frames
MAX_ROLLBACK_FRAMES = 8,
// Maximum length of a nickname
MAX_NICKNAME_LENGTH = 128,
// Maximum name of password for session
MAX_SESSION_PASSWORD_LENGTH = 128,
};
enum : u8
{
ENET_CHANNEL_CONTROL = 0,
ENET_CHANNEL_GGPO = 1,
NUM_ENET_CHANNELS,
};
bool CreateSession(std::string nickname, s32 port, s32 max_players, std::string password, int inputdelay, bool traversal);
bool JoinSession(std::string nickname, const std::string& hostname, s32 port, std::string password, bool spectating,
int inputdelay, bool traversal, const std::string& hostcode);
bool IsActive();
/// Frees up resources associated with the current netplay session.
/// Should only be called by System::ShutdownSystem().
void SystemDestroyed();
/// Runs the VM and netplay loop. when the netplay loop cancels it switches to normal execute mode.
void ExecuteNetplay();
void CollectInput(u32 slot, u32 bind, float value);
void SendChatMessage(const std::string_view& msg);
s32 GetPing();
u32 GetMaxPrediction();
std::string_view GetHostCode();
/// Updates the throttle period, call when target emulation speed changes.
void UpdateThrottlePeriod();
void ToggleDesyncNotifications();
} // namespace Netplay

320
src/core/netplay_packets.h Normal file
View File

@ -0,0 +1,320 @@
// SPDX-FileCopyrightText: 2023 Connor McLaughlin <stenzek@gmail.com> and contributors.
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#pragma once
#include "bios.h"
#include "host.h"
#include "types.h"
#include "fmt/format.h"
namespace Netplay {
enum class SessionState
{
Inactive,
Initializing,
Connecting,
Resetting,
Running,
ClosingSession,
};
enum class ControlMessage : u32
{
// host->player
ConnectResponse,
JoinResponse,
PreReset,
Reset,
ResumeSession,
PlayerJoined,
DropPlayer,
CloseSession,
// player->host
JoinRequest,
ResetComplete,
ResetRequest,
// bi-directional
SetNickname,
ChatMessage,
};
enum class DropPlayerReason : u32
{
ConnectTimeout,
DisconnectedFromHost,
};
#pragma pack(push, 1)
struct ControlMessageHeader
{
ControlMessage type;
u32 size;
};
struct ConnectResponseMessage
{
ControlMessageHeader header;
s32 num_players;
s32 max_players;
u64 game_hash;
u32 game_serial_length;
u32 game_title_length;
BIOS::Hash bios_hash;
struct
{
ConsoleRegion console_region;
CPUExecutionMode cpu_execution_mode;
u32 cpu_overclock_numerator;
u32 cpu_overclock_denominator;
bool cpu_overclock_enable;
bool cpu_recompiler_memory_exceptions;
bool cpu_recompiler_icache;
bool gpu_disable_interlacing;
bool gpu_force_ntsc_timings;
bool gpu_widescreen_hack;
bool gpu_pgxp_enable;
bool gpu_pgxp_culling;
bool gpu_pgxp_cpu;
bool gpu_pgxp_preserve_proj_fp;
bool cdrom_region_check;
bool disable_all_enhancements;
bool use_old_mdec_routines;
bool bios_patch_tty_enable;
bool was_fast_booted;
bool enable_8mb_ram;
DisplayAspectRatio display_aspect_ratio;
u16 display_aspect_ratio_custom_numerator;
u16 display_aspect_ratio_custom_denominator;
MultitapMode multitap_mode;
TickCount dma_max_slice_ticks;
TickCount dma_halt_ticks;
u32 gpu_fifo_size;
TickCount gpu_max_run_ahead;
} settings;
// <char> * game_serial_length + game_title_length follows
// TODO: Include the settings overlays required to match the host config.
bool Validate() const
{
return (static_cast<unsigned>(settings.console_region) < static_cast<unsigned>(ConsoleRegion::Count) &&
static_cast<unsigned>(settings.cpu_execution_mode) < static_cast<unsigned>(CPUExecutionMode::Count) &&
static_cast<unsigned>(settings.display_aspect_ratio) < static_cast<unsigned>(DisplayAspectRatio::Count) &&
static_cast<unsigned>(settings.multitap_mode) < static_cast<unsigned>(MultitapMode::Count));
}
std::string_view GetGameSerial() const
{
return std::string_view(reinterpret_cast<const char*>(this) + sizeof(ConnectResponseMessage), game_serial_length);
}
std::string_view GetGameTitle() const
{
return std::string_view(reinterpret_cast<const char*>(this) + sizeof(ConnectResponseMessage) + game_serial_length,
game_title_length);
}
static ControlMessage MessageType() { return ControlMessage::ConnectResponse; }
};
struct JoinRequestMessage
{
enum class Mode
{
Player,
Spectator,
};
ControlMessageHeader header;
Mode mode;
s32 requested_player_id;
char nickname[MAX_NICKNAME_LENGTH];
char session_password[MAX_SESSION_PASSWORD_LENGTH];
std::string_view GetNickname() const
{
const size_t len = strnlen(nickname, std::size(nickname));
return std::string_view(nickname, len);
}
std::string_view GetSessionPassword() const
{
const size_t len = strnlen(session_password, std::size(session_password));
return std::string_view(session_password, len);
}
static ControlMessage MessageType() { return ControlMessage::JoinRequest; }
};
struct JoinResponseMessage
{
enum class Result : u32
{
Success = 0,
ServerFull,
PlayerIDInUse,
SessionClosed,
InvalidPassword,
};
ControlMessageHeader header;
Result result;
s32 player_id;
static ControlMessage MessageType() { return ControlMessage::JoinResponse; }
};
struct PreResetMessage
{
ControlMessageHeader header;
static ControlMessage MessageType() { return ControlMessage::PreReset; }
};
struct ResetMessage
{
struct PlayerAddress
{
u32 host;
u16 port;
s16 controller_port; // -1 if not present
char nickname[MAX_NICKNAME_LENGTH];
std::string_view GetNickname() const
{
const size_t len = strnlen(nickname, std::size(nickname));
return std::string_view(nickname, len);
}
};
ControlMessageHeader header;
u32 cookie;
s32 num_players;
PlayerAddress players[MAX_PLAYERS];
u32 state_data_size;
// state_data_size bytes of state data follows
static ControlMessage MessageType() { return ControlMessage::Reset; }
};
struct ResetCompleteMessage
{
ControlMessageHeader header;
u32 cookie;
static ControlMessage MessageType() { return ControlMessage::ResetComplete; }
};
struct ResumeSessionMessage
{
ControlMessageHeader header;
static ControlMessage MessageType() { return ControlMessage::ResumeSession; }
};
struct PlayerJoinedMessage
{
ControlMessageHeader header;
s32 player_id;
static ControlMessage MessageType() { return ControlMessage::PlayerJoined; }
};
struct DropPlayerMessage
{
ControlMessageHeader header;
DropPlayerReason reason;
s32 player_id;
static ControlMessage MessageType() { return ControlMessage::DropPlayer; }
};
struct ResetRequestMessage
{
enum class Reason : u32
{
ConnectionLost,
};
ControlMessageHeader header;
Reason reason;
s32 causing_player_id;
std::string ReasonToString() const
{
switch (reason)
{
case Reason::ConnectionLost:
return fmt::format(Host::TranslateString("Netplay", "Connection lost to player {}.").GetCharArray(),
causing_player_id);
default:
return "Unknown";
}
}
static ControlMessage MessageType() { return ControlMessage::ResetRequest; }
};
struct CloseSessionMessage
{
enum class Reason : u32
{
HostRequest,
HostShutdown,
};
ControlMessageHeader header;
Reason reason;
std::string ReasonToString() const
{
switch (reason)
{
case Reason::HostRequest:
return Host::TranslateStdString("Netplay", "Session closed due to host request.");
case Reason::HostShutdown:
return Host::TranslateStdString("Netplay", "Session closed due to host shutdown.");
default:
return "Unknown";
}
}
static ControlMessage MessageType() { return ControlMessage::CloseSession; }
};
struct SetNicknameMessage
{
ControlMessageHeader header;
static ControlMessage MessageType() { return ControlMessage::SetNickname; }
};
struct ChatMessage
{
ControlMessageHeader header;
std::string_view GetMessage() const
{
return (header.size > sizeof(ChatMessage)) ?
std::string_view(reinterpret_cast<const char*>(this) + sizeof(ChatMessage),
header.size - sizeof(ChatMessage)) :
std::string_view();
}
static ControlMessage MessageType() { return ControlMessage::ChatMessage; }
};
#pragma pack(pop)
} // namespace Netplay

View File

@ -153,6 +153,8 @@ void Pad::Reset()
{
SoftReset();
s_last_memory_card_transfer_frame = 0;
for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
{
if (s_controllers[i])
@ -169,12 +171,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::IsActive();
}
u32 Pad::GetMaximumRollbackFrames()
{
return g_settings.runahead_frames;
return (Netplay::IsActive() ? Netplay::GetMaxPrediction() : g_settings.runahead_frames);
}
bool Pad::DoStateController(StateWrapper& sw, u32 i)
@ -248,11 +250,12 @@ bool Pad::DoStateController(StateWrapper& sw, u32 i)
bool Pad::DoStateMemcard(StateWrapper& sw, u32 i, bool is_memory_state)
{
const bool force_load = Netplay::IsActive();
bool card_present_in_state = static_cast<bool>(s_memory_cards[i]);
sw.Do(&card_present_in_state);
if (card_present_in_state && !s_memory_cards[i] && g_settings.load_devices_from_save_states)
if (card_present_in_state && !s_memory_cards[i] && g_settings.load_devices_from_save_states && !force_load)
{
Host::AddFormattedOSDMessage(
20.0f,
@ -267,7 +270,7 @@ bool Pad::DoStateMemcard(StateWrapper& sw, u32 i, bool is_memory_state)
if (card_present_in_state)
{
if (sw.IsReading() && !g_settings.load_devices_from_save_states)
if (sw.IsReading() && !g_settings.load_devices_from_save_states && !force_load)
{
// load memcard into a temporary: If the card datas match, take the one from the savestate
// since it has other useful non-data state information. Otherwise take the user's card
@ -279,7 +282,7 @@ bool Pad::DoStateMemcard(StateWrapper& sw, u32 i, bool is_memory_state)
return false;
}
if (sw.IsWriting())
if (sw.IsWriting() || force_load)
return true; // all done as far as writes concerned.
if (card_from_state)
@ -469,6 +472,9 @@ bool Pad::DoState(StateWrapper& sw, bool is_memory_state)
}
else
{
if (sw.IsReading())
s_last_memory_card_transfer_frame = 0;
for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
{
if ((sw.GetVersion() < 50) && (i >= 2))

View File

@ -33,6 +33,7 @@
#include "mdec.h"
#include "memory_card.h"
#include "multitap.h"
#include "netplay.h"
#include "pad.h"
#include "pcdrv.h"
#include "pgxp.h"
@ -92,7 +93,8 @@ static void ClearRunningGame();
static void DestroySystem();
static std::string GetMediaPathFromSaveState(const char* path);
static bool DoState(StateWrapper& sw, GPUTexture** host_texture, bool update_display, bool is_memory_state);
static void DoRunFrame();
static void WrappedRunFrame();
static void RunFramesToNow();
static bool CreateGPU(GPURenderer renderer);
static bool SaveUndoLoadState();
@ -106,6 +108,7 @@ static void DoRunahead();
static void DoMemorySaveStates();
static bool Initialize(bool force_software_renderer);
static bool FastForwardToFirstFrame();
static bool UpdateGameSettingsLayer();
static void UpdateRunningGame(const char* path, CDImage* image, bool booting);
@ -1347,6 +1350,9 @@ bool System::BootSystem(SystemBootParameters parameters)
if (parameters.load_image_to_ram || g_settings.cdrom_load_image_to_ram)
CDROM::PrecacheMedia();
if (parameters.fast_forward_to_first_frame)
FastForwardToFirstFrame();
if (g_settings.audio_dump_on_boot)
StartDumpingAudio();
@ -1551,14 +1557,32 @@ void System::ClearRunningGame()
#endif
}
bool System::FastForwardToFirstFrame()
{
// If we're taking more than 60 seconds to load the game, oof..
static constexpr u32 MAX_FRAMES_TO_SKIP = 30 * 60;
const u32 current_frame_number = s_frame_number;
const u32 current_internal_frame_number = s_internal_frame_number;
SPU::SetAudioOutputMuted(true);
while (s_internal_frame_number == current_internal_frame_number &&
(s_frame_number - current_frame_number) <= MAX_FRAMES_TO_SKIP)
{
System::RunFrame();
}
SPU::SetAudioOutputMuted(false);
return (s_internal_frame_number != current_internal_frame_number);
}
void System::Execute()
{
while (System::IsRunning())
while (IsRunning())
{
if (s_display_all_frames)
System::RunFrame();
WrappedRunFrame();
else
System::RunFrames();
RunFramesToNow();
// this can shut us down
Host::PumpMessagesOnCPUThread();
@ -1571,13 +1595,7 @@ void System::Execute()
PauseSystem(true);
}
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++;
}
PresentFrame();
if (s_throttler_enabled)
System::Throttle();
@ -1589,6 +1607,17 @@ void System::Execute()
}
}
void System::PresentFrame()
{
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++;
}
}
void System::RecreateSystem()
{
Assert(!IsShutdown());
@ -2188,7 +2217,7 @@ void System::SingleStepCPU()
g_gpu->ResetGraphicsAPIState();
}
void System::DoRunFrame()
void System::RunFrame()
{
g_gpu->RestoreGraphicsAPIState();
@ -2228,7 +2257,7 @@ void System::DoRunFrame()
g_gpu->ResetGraphicsAPIState();
}
void System::RunFrame()
void System::WrappedRunFrame()
{
if (s_rewind_load_counter >= 0)
{
@ -2239,7 +2268,7 @@ void System::RunFrame()
if (s_runahead_frames > 0)
DoRunahead();
DoRunFrame();
RunFrame();
s_next_frame_time += s_frame_period;
@ -2272,6 +2301,9 @@ void System::UpdateThrottlePeriod()
}
ResetThrottler();
if (Netplay::IsActive())
Netplay::UpdateThrottlePeriod();
}
void System::ResetThrottler()
@ -2301,7 +2333,7 @@ void System::Throttle()
#endif
}
void System::RunFrames()
void System::RunFramesToNow()
{
// If we're running more than this in a single loop... we're in for a bad time.
const u32 max_frames_to_run = 2;
@ -2313,7 +2345,7 @@ void System::RunFrames()
if (value < s_next_frame_time)
break;
RunFrame();
WrappedRunFrame();
frames_run++;
value = Common::Timer::GetCurrentValue();
@ -3722,7 +3754,7 @@ void System::DoRunahead()
while (frames_to_run > 0)
{
DoRunFrame();
RunFrame();
SaveRunaheadState();
frames_to_run--;
}
@ -3780,6 +3812,9 @@ void System::ShutdownSystem(bool save_resume_state)
if (!IsValid())
return;
if (Netplay::IsActive())
Netplay::SystemDestroyed();
if (save_resume_state)
SaveResumeState();

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"
@ -45,6 +46,7 @@ struct SystemBootParameters
u32 media_playlist_index = 0;
bool load_image_to_ram = false;
bool force_software_renderer = false;
bool fast_forward_to_first_frame = false;
};
struct SaveStateInfo
@ -254,7 +256,7 @@ bool RecreateGPU(GPURenderer renderer, bool force_recreate_display = false, bool
void SingleStepCPU();
void RunFrame();
void RunFrames();
void PresentFrame();
/// Sets target emulation speed.
float GetTargetSpeed();
@ -519,4 +521,7 @@ bool IsFullscreen();
/// Alters fullscreen state of hosting application.
void SetFullscreen(bool enabled);
// netplay
void OnNetplayMessage(std::string message);
void ClearNetplayMessages();
} // namespace Host

View File

@ -347,7 +347,7 @@ bool DoState(StateWrapper& sw)
sw.Do(&last_event_run_time);
}
Log_DevPrintf("Loaded %u events from save state.", event_count);
Log_DebugPrintf("Loaded %u events from save state.", event_count);
SortEvents();
}
else
@ -364,7 +364,7 @@ bool DoState(StateWrapper& sw)
sw.Do(&event->m_interval);
}
Log_DevPrintf("Wrote %u events to save state.", s_active_event_count);
Log_DebugPrintf("Wrote %u events to save state.", s_active_event_count);
}
return !sw.HasError();

View File

@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CreateNetplaySessionDialog</class>
<widget class="QDialog" name="CreateNetplaySessionDialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>496</width>
<height>302</height>
</rect>
</property>
<property name="windowTitle">
<string comment="Window title">Create Netplay Session</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="resources/resources.qrc">:/icons/emblem-person-blue.png</pixmap>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<pointsize>14</pointsize>
<bold>false</bold>
</font>
</property>
<property name="text">
<string comment="Header text">Create Netplay Session</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select a nickname and port to open your current game session to other players via netplay. A password may optionally be supplied to restrict who can join. The traversal mode option can be enabled to allow other players to join via a host code without needing to portforward.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Nickname:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="nickname">
<property name="text">
<string>Netplay Host</string>
</property>
<property name="maxLength">
<number>128</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Port:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="port">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>31200</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Players:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="maxPlayers">
<property name="minimum">
<number>2</number>
</property>
<property name="maximum">
<number>8</number>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="password">
<property name="maxLength">
<number>128</number>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Input Delay:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="inputDelay"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="traversal">
<property name="text">
<string>Enable Traversal Mode</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -41,6 +41,7 @@
<ClCompile Include="mainwindow.cpp" />
<ClCompile Include="memorycardsettingswidget.cpp" />
<ClCompile Include="memorycardeditordialog.cpp" />
<ClCompile Include="netplaydialogs.cpp" />
<ClCompile Include="postprocessingchainconfigwidget.cpp" />
<ClCompile Include="postprocessingshaderconfigwidget.cpp" />
<ClCompile Include="postprocessingsettingswidget.cpp" />
@ -83,6 +84,7 @@
<QtMoc Include="colorpickerbutton.h" />
<ClInclude Include="controllersettingwidgetbinder.h" />
<QtMoc Include="memoryviewwidget.h" />
<QtMoc Include="netplaydialogs.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="settingwidgetbinder.h" />
<QtMoc Include="consolesettingswidget.h" />
@ -260,6 +262,7 @@
<ClCompile Include="$(IntDir)moc_inputbindingdialog.cpp" />
<ClCompile Include="$(IntDir)moc_inputbindingwidgets.cpp" />
<ClCompile Include="$(IntDir)moc_mainwindow.cpp" />
<ClCompile Include="$(IntDir)moc_netplaydialogs.cpp" />
<ClCompile Include="$(IntDir)moc_memorycardsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_memorycardeditordialog.cpp" />
<ClCompile Include="$(IntDir)moc_memoryviewwidget.cpp" />
@ -325,6 +328,12 @@
<QtUi Include="controllerledsettingsdialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="createnetplaysessiondialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="joinnetplaysessiondialog.ui">
<FileType>Document</FileType>
</QtUi>
<None Include="translations\duckstation-qt_es-es.ts" />
<None Include="translations\duckstation-qt_tr.ts" />
</ItemGroup>

View File

@ -94,6 +94,9 @@
<ClCompile Include="coverdownloaddialog.cpp" />
<ClCompile Include="$(IntDir)moc_coverdownloaddialog.cpp" />
<ClCompile Include="colorpickerbutton.cpp" />
<ClCompile Include="$(IntDir)moc_colorpickerbutton.cpp" />
<ClCompile Include="netplaydialogs.cpp" />
<ClCompile Include="$(IntDir)moc_netplaydialogs.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="qtutils.h" />
@ -155,6 +158,7 @@
<QtMoc Include="foldersettingswidget.h" />
<QtMoc Include="coverdownloaddialog.h" />
<QtMoc Include="colorpickerbutton.h" />
<QtMoc Include="netplaydialogs.h" />
</ItemGroup>
<ItemGroup>
<QtUi Include="consolesettingswidget.ui" />
@ -196,6 +200,8 @@
<QtUi Include="controllerbindingwidget_mouse.ui" />
<QtUi Include="coverdownloaddialog.ui" />
<QtUi Include="controllerledsettingsdialog.ui" />
<QtUi Include="createnetplaysessiondialog.ui" />
<QtUi Include="joinnetplaysessiondialog.ui" />
</ItemGroup>
<ItemGroup>
<Natvis Include="qt5.natvis" />

View File

@ -0,0 +1,276 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>JoinNetplaySessionDialog</class>
<widget class="QDialog" name="JoinNetplaySessionDialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<height>308</height>
</rect>
</property>
<property name="windowTitle">
<string comment="Window title">Create Netplay Session</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="resources/resources.qrc">:/icons/emblem-person-blue.png</pixmap>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<pointsize>14</pointsize>
<bold>false</bold>
</font>
</property>
<property name="text">
<string comment="Header text">Join Netplay Session</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Choose a nickname and enter the address/port of the netplay session you wish to join.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QTabWidget" name="tabConnectMode">
<property name="minimumSize">
<size>
<width>0</width>
<height>190</height>
</size>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tabDirect">
<attribute name="title">
<string>Direct Mode</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Nickname:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="nickname">
<property name="text">
<string>Netplay Peer</string>
</property>
<property name="maxLength">
<number>128</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Input Delay:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="inputDelay"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Hostname:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="hostname">
<property name="text">
<string>localhost</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Port:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="port">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>31200</number>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="password">
<property name="maxLength">
<number>128</number>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabTraversal">
<attribute name="title">
<string>Traversal Mode</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Nickname:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="nicknameTraversal">
<property name="text">
<string>Netplay Peer</string>
</property>
<property name="maxLength">
<number>128</number>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="inputDelayTraversal"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="passwordTraversal">
<property name="maxLength">
<number>128</number>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Host Code:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="hostCode"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Input Delay:</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="spectating">
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Enable Spectator Mode</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -22,6 +22,7 @@
#include "gamelistsettingswidget.h"
#include "gamelistwidget.h"
#include "memorycardeditordialog.h"
#include "netplaydialogs.h"
#include "qthost.h"
#include "qtutils.h"
#include "settingsdialog.h"
@ -1707,6 +1708,9 @@ void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevo
m_ui.actionViewGameProperties->setDisabled(starting || !running);
m_ui.actionCreateNetplaySession->setDisabled(!running || cheevos_challenge_mode);
m_ui.actionJoinNetplaySession->setDisabled(cheevos_challenge_mode);
if (starting || running)
{
if (!m_ui.toolBar->actions().contains(m_ui.actionPowerOff))
@ -2089,6 +2093,10 @@ 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::onCreateNetplaySessionClicked);
connect(m_ui.actionJoinNetplaySession, &QAction::triggered, this, &MainWindow::onJoinNetplaySessionClicked);
}
void MainWindow::addThemeToMenu(const QString& name, const QString& key)
@ -2753,6 +2761,18 @@ void MainWindow::onCPUDebuggerClosed()
m_debugger_window = nullptr;
}
void MainWindow::onCreateNetplaySessionClicked()
{
CreateNetplaySessionDialog dlg(this);
dlg.exec();
}
void MainWindow::onJoinNetplaySessionClicked()
{
JoinNetplaySessionDialog dlg(this);
dlg.exec();
}
void MainWindow::onToolsOpenDataDirectoryTriggered()
{
QtUtils::OpenURL(this, QUrl::fromLocalFile(QString::fromStdString(EmuFolders::DataRoot)));

View File

@ -168,6 +168,9 @@ private Q_SLOTS:
void openCPUDebugger();
void onCPUDebuggerClosed();
void onCreateNetplaySessionClicked();
void onJoinNetplaySessionClicked();
protected:
void showEvent(QShowEvent* event) override;
void closeEvent(QCloseEvent* event) override;

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"/>
@ -237,8 +237,17 @@
<addaction name="separator"/>
<addaction name="actionOpenDataDirectory"/>
</widget>
<widget class="QMenu" name="menuNetplay">
<property name="title">
<string>Netplay</string>
</property>
<addaction name="separator"/>
<addaction name="actionCreateNetplaySession"/>
<addaction name="actionJoinNetplaySession"/>
</widget>
<addaction name="menuSystem"/>
<addaction name="menuSettings"/>
<addaction name="menuNetplay"/>
<addaction name="menu_View"/>
<addaction name="menu_Tools"/>
<addaction name="menuDebug"/>
@ -472,7 +481,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 +490,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 +499,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 +517,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 +526,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 +985,16 @@
<string>Cover Downloader</string>
</property>
</action>
<action name="actionCreateNetplaySession">
<property name="text">
<string>Create Session...</string>
</property>
</action>
<action name="actionJoinNetplaySession">
<property name="text">
<string>Join Session...</string>
</property>
</action>
</widget>
<resources>
<include location="resources/resources.qrc"/>

View File

@ -0,0 +1,129 @@
// SPDX-FileCopyrightText: 2023 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "netplaydialogs.h"
#include "qthost.h"
#include "core/netplay.h"
#include <QtWidgets/QPushButton>
CreateNetplaySessionDialog::CreateNetplaySessionDialog(QWidget* parent) : QDialog(parent)
{
m_ui.setupUi(this);
connect(m_ui.maxPlayers, &QSpinBox::valueChanged, this, &CreateNetplaySessionDialog::updateState);
connect(m_ui.port, &QSpinBox::valueChanged, this, &CreateNetplaySessionDialog::updateState);
connect(m_ui.inputDelay, &QSpinBox::valueChanged, this, &CreateNetplaySessionDialog::updateState);
connect(m_ui.nickname, &QLineEdit::textChanged, this, &CreateNetplaySessionDialog::updateState);
connect(m_ui.password, &QLineEdit::textChanged, this, &CreateNetplaySessionDialog::updateState);
connect(m_ui.buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, this,
&CreateNetplaySessionDialog::accept);
connect(m_ui.buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, this,
&CreateNetplaySessionDialog::reject);
updateState();
}
CreateNetplaySessionDialog::~CreateNetplaySessionDialog() = default;
void CreateNetplaySessionDialog::accept()
{
if (!validate())
return;
const int players = m_ui.maxPlayers->value();
const int port = m_ui.port->value();
const int inputdelay = m_ui.inputDelay->value();
const QString& nickname = m_ui.nickname->text();
const QString& password = m_ui.password->text();
const bool traversal = m_ui.traversal->isChecked();
QDialog::accept();
g_emu_thread->createNetplaySession(nickname.trimmed(), port, players, password, inputdelay, traversal);
}
bool CreateNetplaySessionDialog::validate()
{
const int players = m_ui.maxPlayers->value();
const int port = m_ui.port->value();
const int inputdelay = m_ui.inputDelay->value();
const QString& nickname = m_ui.nickname->text();
return (!nickname.isEmpty() && players >= 2 && players <= Netplay::MAX_PLAYERS && port > 0 && port <= 65535 &&
inputdelay >= 0 && inputdelay <= 10);
}
void CreateNetplaySessionDialog::updateState()
{
m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(validate());
}
JoinNetplaySessionDialog::JoinNetplaySessionDialog(QWidget* parent)
{
m_ui.setupUi(this);
connect(m_ui.port, &QSpinBox::valueChanged, this, &JoinNetplaySessionDialog::updateState);
connect(m_ui.inputDelay, &QSpinBox::valueChanged, this, &JoinNetplaySessionDialog::updateState);
connect(m_ui.inputDelayTraversal, &QSpinBox::valueChanged, this, &JoinNetplaySessionDialog::updateState);
connect(m_ui.nickname, &QLineEdit::textChanged, this, &JoinNetplaySessionDialog::updateState);
connect(m_ui.nicknameTraversal, &QLineEdit::textChanged, this, &JoinNetplaySessionDialog::updateState);
connect(m_ui.password, &QLineEdit::textChanged, this, &JoinNetplaySessionDialog::updateState);
connect(m_ui.passwordTraversal, &QLineEdit::textChanged, this, &JoinNetplaySessionDialog::updateState);
connect(m_ui.hostname, &QLineEdit::textChanged, this, &JoinNetplaySessionDialog::updateState);
connect(m_ui.hostCode, &QLineEdit::textChanged, this, &JoinNetplaySessionDialog::updateState);
connect(m_ui.tabConnectMode, &QTabWidget::currentChanged, this, &JoinNetplaySessionDialog::updateState);
connect(m_ui.buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, this,
&JoinNetplaySessionDialog::accept);
connect(m_ui.buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, this,
&JoinNetplaySessionDialog::reject);
updateState();
}
JoinNetplaySessionDialog::~JoinNetplaySessionDialog() = default;
void JoinNetplaySessionDialog::accept()
{
const bool direct_mode = m_ui.tabTraversal->isHidden();
const bool valid = direct_mode ? validate() : validateTraversal();
if (!valid)
return;
int port = m_ui.port->value();
int inputdelay = direct_mode ? m_ui.inputDelay->value() : m_ui.inputDelayTraversal->value();
const QString& nickname = direct_mode ? m_ui.nickname->text() : m_ui.nicknameTraversal->text();
const QString& password = direct_mode ? m_ui.password->text() : m_ui.passwordTraversal->text();
const QString& hostname = m_ui.hostname->text();
const QString& hostcode = m_ui.hostCode->text();
const bool spectating = m_ui.spectating->isChecked();
QDialog::accept();
g_emu_thread->joinNetplaySession(nickname.trimmed(), hostname.trimmed(), port, password, spectating, inputdelay,
!direct_mode, hostcode.trimmed());
}
bool JoinNetplaySessionDialog::validate()
{
const int port = m_ui.port->value();
const int inputdelay = m_ui.inputDelay->value();
const QString& nickname = m_ui.nickname->text();
const QString& hostname = m_ui.hostname->text();
return (!nickname.isEmpty() && !hostname.isEmpty() && port > 0 && port <= 65535 && inputdelay >= 0 &&
inputdelay <= 10);
}
bool JoinNetplaySessionDialog::validateTraversal()
{
const int inputdelay = m_ui.inputDelayTraversal->value();
const QString& nickname = m_ui.nicknameTraversal->text();
const QString& hostcode = m_ui.hostCode->text();
return (!nickname.isEmpty() && !hostcode.isEmpty() && inputdelay >= 0 && inputdelay <= 10);
}
void JoinNetplaySessionDialog::updateState()
{
m_ui.buttonBox->button(QDialogButtonBox::Ok)
->setEnabled(m_ui.tabTraversal->isHidden() ? validate() : validateTraversal());
}

View File

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2023 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#pragma once
#include "ui_createnetplaysessiondialog.h"
#include "ui_joinnetplaysessiondialog.h"
#include <QtWidgets/QDialog>
class CreateNetplaySessionDialog : public QDialog
{
Q_OBJECT
public:
CreateNetplaySessionDialog(QWidget* parent);
~CreateNetplaySessionDialog();
public Q_SLOTS:
void accept() override;
private Q_SLOTS:
void updateState();
private:
bool validate();
Ui::CreateNetplaySessionDialog m_ui;
};
class JoinNetplaySessionDialog : public QDialog
{
Q_OBJECT
public:
JoinNetplaySessionDialog(QWidget* parent);
~JoinNetplaySessionDialog();
public Q_SLOTS:
void accept() override;
private Q_SLOTS:
void updateState();
private:
bool validate();
bool validateTraversal();
private:
Ui::JoinNetplaySessionDialog m_ui;
};

View File

@ -0,0 +1,208 @@
#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 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

@ -17,6 +17,7 @@
#include "core/host.h"
#include "core/host_settings.h"
#include "core/memory_card.h"
#include "core/netplay.h"
#include "core/spu.h"
#include "core/system.h"
#include "displaywidget.h"
@ -97,6 +98,9 @@ static bool s_nogui_mode = false;
static bool s_start_fullscreen_ui = false;
static bool s_start_fullscreen_ui_fullscreen = false;
// TODO: REMOVE ME
static int s_netplay_test = -1;
EmuThread* g_emu_thread;
GDBServer* g_gdb_server;
@ -404,7 +408,9 @@ void EmuThread::applySettings(bool display_osd_messages /* = false */)
}
System::ApplySettings(display_osd_messages);
if (!FullscreenUI::IsInitialized() && System::IsPaused())
if (!FullscreenUI::IsInitialized() && !System::IsValid())
setInitialState(std::nullopt);
else if (!FullscreenUI::IsInitialized() && System::IsPaused())
redrawDisplayWindow();
}
@ -1065,6 +1071,50 @@ void EmuThread::reloadPostProcessingShaders()
System::ReloadPostProcessingShaders();
}
void EmuThread::createNetplaySession(const QString& nickname, qint32 port, qint32 max_players, const QString& password, int inputdelay, bool traversal)
{
if (!isOnThread())
{
QMetaObject::invokeMethod(this, "createNetplaySession", Qt::QueuedConnection, Q_ARG(const QString&, nickname),
Q_ARG(qint32, port), Q_ARG(qint32, max_players), Q_ARG(const QString&, password),
Q_ARG(int, inputdelay), Q_ARG(bool, traversal));
return;
}
// need a valid system to make a session
if (!System::IsValid())
return;
if (!Netplay::CreateSession(nickname.toStdString(), port, max_players, password.toStdString(), inputdelay, traversal))
{
errorReported(tr("Netplay Error"), tr("Failed to create netplay session. The log may contain more information."));
return;
}
}
void EmuThread::joinNetplaySession(const QString& nickname, const QString& hostname, qint32 port,
const QString& password, bool spectating, int inputdelay, bool traversal,
const QString& hostcode)
{
if (!isOnThread())
{
QMetaObject::invokeMethod(this, "joinNetplaySession", Qt::QueuedConnection, Q_ARG(const QString&, nickname),
Q_ARG(const QString&, hostname), Q_ARG(qint32, port), Q_ARG(const QString&, password),
Q_ARG(bool, spectating), Q_ARG(int, inputdelay), Q_ARG(bool, traversal),
Q_ARG(const QString&, hostcode));
return;
}
if (!Netplay::JoinSession(nickname.toStdString(), hostname.toStdString(), port, password.toStdString(), spectating, inputdelay, traversal, hostcode.toStdString()))
{
errorReported(tr("Netplay Error"), tr("Failed to join netplay session. The log may contain more information."));
return;
}
// Exit the event loop, we'll take it from here.
g_emu_thread->wakeThread();
}
void EmuThread::clearInputBindStateFromSource(InputBindingKey key)
{
if (!isOnThread())
@ -1421,11 +1471,16 @@ void EmuThread::run()
// bind buttons/axises
createBackgroundControllerPollTimer();
startBackgroundControllerPollTimer();
setInitialState(std::nullopt);
// main loop
while (!m_shutdown_flag)
{
if (System::IsRunning())
if (Netplay::IsActive())
{
Netplay::ExecuteNetplay();
}
else if (System::IsRunning())
{
System::Execute();
}
@ -1468,6 +1523,8 @@ void EmuThread::renderDisplay(bool skip_present)
if (!skip_present)
{
FullscreenUI::Render();
if (Netplay::IsActive())
ImGuiManager::RenderNetplayOverlays();
ImGuiManager::RenderTextOverlays();
ImGuiManager::RenderOSDMessages();
}
@ -2042,6 +2099,11 @@ bool QtHost::ParseCommandLineParametersAndInitializeConfig(QApplication& app,
InitializeEarlyConsole();
continue;
}
else if (CHECK_ARG_PARAM("-netplay"))
{
s_netplay_test = StringUtil::FromChars<int>(args[++i].toStdString()).value_or(0);
continue;
}
#ifdef WITH_RAINTEGRATION
else if (CHECK_ARG("-raintegration"))
{
@ -2179,6 +2241,31 @@ int main(int argc, char* argv[])
else if (!s_nogui_mode)
main_window->startupUpdateCheck();
if (s_netplay_test >= 0)
{
Host::RunOnCPUThread([]() {
const bool first = (s_netplay_test == 0);
QtHost::RunOnUIThread([first]() { g_main_window->move(QPoint(first ? 300 : 1400, 500)); });
const int port = 31200;
const QString remote = QStringLiteral("127.0.0.1");
std::string game = "D:\\PSX\\chd\\padtest.chd";
const QString nickname = QStringLiteral("NICKNAME%1").arg(s_netplay_test + 1);
if (first)
{
auto params = std::make_shared<SystemBootParameters>(std::move(game));
params->override_fast_boot = true;
params->fast_forward_to_first_frame = true;
g_emu_thread->bootSystem(std::move(params));
g_emu_thread->createNetplaySession(nickname, port, 2, QString(), 0, false);
}
else
{
g_emu_thread->joinNetplaySession(nickname, remote, port, QString(), false, 0, false, "");
}
});
}
// This doesn't return until we exit.
const int result = app.exec();

View File

@ -187,6 +187,10 @@ public Q_SLOTS:
void setCheatEnabled(quint32 index, bool enabled);
void applyCheat(quint32 index);
void reloadPostProcessingShaders();
void createNetplaySession(const QString& nickname, qint32 port, qint32 max_players, const QString& password,
int inputdelay, bool traversal);
void joinNetplaySession(const QString& nickname, const QString& hostname, qint32 port, const QString& password,
bool spectating, int inputdelay, bool traversal, const QString& hostcode);
void clearInputBindStateFromSource(InputBindingKey key);
private Q_SLOTS:
@ -233,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

@ -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

View File

@ -677,6 +677,16 @@ DEFINE_HOTKEY("OpenPauseMenu", TRANSLATABLE("Hotkeys", "General"), TRANSLATABLE(
[](s32 pressed) {
if (!pressed)
FullscreenUI::OpenPauseMenu();
})
DEFINE_HOTKEY("OpenNetplayChat", TRANSLATABLE("Hotkeys", "Netplay"), TRANSLATABLE("Hotkeys", "Open Netplay Chat"),
[](s32 pressed) {
if (!pressed)
ImGuiManager::OpenNetplayChat();
})
DEFINE_HOTKEY("ToggleDesyncNotifications", TRANSLATABLE("Hotkeys", "Netplay"), TRANSLATABLE("Hotkeys", "Toggle Desync Notifications"),
[](s32 pressed) {
if (!pressed)
Netplay::ToggleDesyncNotifications();
})
#endif

View File

@ -22,6 +22,7 @@
<ExcludedFromBuild Condition="'$(Platform)'=='ARM64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="imgui_manager.cpp" />
<ClCompile Include="imgui_netplay.cpp" />
<ClCompile Include="imgui_overlays.cpp" />
<ClCompile Include="input_manager.cpp" />
<ClCompile Include="input_source.cpp" />

View File

@ -30,6 +30,7 @@
<ClCompile Include="dinput_source.cpp" />
<ClCompile Include="imgui_overlays.cpp" />
<ClCompile Include="platform_misc_win32.cpp" />
<ClCompile Include="imgui_netplay.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="icon.h" />

View File

@ -0,0 +1,225 @@
// SPDX-FileCopyrightText: 2023 Connor McLaughlin <stenzek@gmail.com>
// 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 <atomic>
#include <chrono>
#include <cmath>
#include <deque>
#include <mutex>
#include <unordered_map>
#if defined(CPU_X64)
#include <emmintrin.h>
#elif defined(CPU_AARCH64)
#ifdef _MSC_VER
#include <arm64_neon.h>
#else
#include <arm_neon.h>
#endif
#endif
Log_SetChannel(ImGuiManager);
namespace ImGuiManager {
static void DrawNetplayMessages();
static void DrawNetplayStats();
static void DrawNetplayChatDialog();
} // namespace ImGuiManager
static std::deque<std::pair<std::string, Common::Timer::Value>> 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 Host::ClearNetplayMessages()
{
while (s_netplay_messages.size() > 0)
s_netplay_messages.pop_front();
}
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 - (100.0f * scale) - font->FontSize - spacing;
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<float>(Common::Timer::ConvertValueToSeconds(iter->second - ticks));
const float opacity = std::min(remainder / NETPLAY_MESSAGE_FADE_TIME, 1.0f);
const u32 alpha = static_cast<u32>(opacity * 255.0f);
const u32 shadow_alpha = static_cast<u32>(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(margin + shadow_offset, position_y + shadow_offset),
IM_COL32(0, 0, 0, shadow_alpha), text_start, text_end);
dl->AddText(font, font->FontSize, ImVec2(margin, 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: {}\n", Netplay::GetPing());
// temporary show the hostcode here for now
auto hostcode = Netplay::GetHostCode();
if (!hostcode.empty())
text.AppendFmtString("Host Code: {}", hostcode);
const float scale = ImGuiManager::GetGlobalScale();
const float shadow_offset = 1.0f * scale;
const float margin = 10.0f * scale;
ImFont* font = ImGuiManager::GetFixedFont();
const float position_y = ImGui::GetIO().DisplaySize.y - margin - (100.0f * scale);
ImDrawList* dl = ImGui::GetBackgroundDrawList();
dl->AddText(font, font->FontSize, ImVec2(margin + shadow_offset, position_y + shadow_offset), IM_COL32(0, 0, 0, 100),
text, text.GetCharArray() + text.GetLength());
dl->AddText(font, font->FontSize, ImVec2(margin, 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));
// sending netplay message
if (send_message && !s_netplay_chat_message.empty())
Netplay::SendChatMessage(s_netplay_chat_message);
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;
}

View File

@ -8,6 +8,9 @@
namespace ImGuiManager {
void RenderTextOverlays();
void RenderOverlayWindows();
void RenderNetplayOverlays();
void OpenNetplayChat();
}
namespace SaveStateSelectorUI {

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::IsActive())
{
Netplay::CollectInput(pad_index, bind_index, value);
return;
}
Controller* c = System::GetController(pad_index);
if (c)
c->SetBindState(bind_index, value);