Netplay: Add experimental 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 5f99fda9d7
commit 1e585e74e8
No known key found for this signature in database
26 changed files with 4250 additions and 28 deletions

View File

@ -10,6 +10,7 @@ class LayeredSettingsInterface final : public SettingsInterface
public:
enum Layer : u32
{
LAYER_NETPLAY,
LAYER_CMDLINE,
LAYER_GAME,
LAYER_INPUT,
@ -61,7 +62,7 @@ public:
using SettingsInterface::GetUIntValue;
private:
static constexpr Layer FIRST_LAYER = LAYER_CMDLINE;
static constexpr Layer FIRST_LAYER = LAYER_NETPLAY;
static constexpr Layer LAST_LAYER = LAYER_BASE;
std::array<SettingsInterface*, NUM_LAYERS> m_layers{};

View File

@ -79,6 +79,9 @@ add_library(core
multitap.h
negcon.cpp
negcon.h
netplay.cpp
netplay.h
netplay_packets.h
pad.cpp
pad.h
pcdrv.cpp
@ -126,6 +129,10 @@ target_include_directories(core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..")
target_link_libraries(core PUBLIC Threads::Threads common util zlib)
target_link_libraries(core PRIVATE stb xxhash imgui rapidjson rcheevos)
if(NOT ANDROID)
target_link_libraries(core PRIVATE enet ggpo-x)
endif()
if(${CPU_ARCH} STREQUAL "x64")
target_include_directories(core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../../dep/xbyak/xbyak")
target_compile_definitions(core PUBLIC "XBYAK_NO_EXCEPTION=1" "ENABLE_RECOMPILER=1" "ENABLE_MMAP_FASTMEM=1")

View File

@ -18,4 +18,9 @@
<AdditionalIncludeDirectories Condition="'$(Platform)'=='ARM' Or '$(Platform)'=='ARM64'">%(AdditionalIncludeDirectories);$(SolutionDir)dep\vixl\include</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<Link>
<AdditionalDependencies>%(AdditionalDependencies);ws2_32.lib</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
</Project>

View File

@ -58,6 +58,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" />
@ -129,6 +130,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" />
@ -152,6 +155,12 @@
<ProjectReference Include="..\..\dep\discord-rpc\discord-rpc.vcxproj">
<Project>{4266505b-dbaf-484b-ab31-b53b9c8235b3}</Project>
</ProjectReference>
<ProjectReference Include="..\..\dep\enet\enet.vcxproj">
<Project>{460a096b-fcc7-465c-8e4b-434af490a9ea}</Project>
</ProjectReference>
<ProjectReference Include="..\..\dep\ggpo-x\ggpo-x.vcxproj">
<Project>{edf3634a-ce8a-4625-92bd-27bad5d30a9a}</Project>
</ProjectReference>
<ProjectReference Include="..\..\dep\imgui\imgui.vcxproj">
<Project>{bb08260f-6fbc-46af-8924-090ee71360c6}</Project>
</ProjectReference>
@ -190,6 +199,7 @@
<Import Project="core.props" />
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)dep\enet\include;$(SolutionDir)dep\ggpo-x\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>ZYDIS_DISABLE_ENCODER;ZYDIS_DISABLE_AVX512;ZYDIS_DISABLE_KNC;ZYDIS_STATIC_BUILD;ZYCORE_STATIC_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories Condition="'$(Platform)'=='x64'">$(SolutionDir)dep\zydis\include;$(SolutionDir)dep\zydis\dependencies\zycore\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<ObjectFileName>$(IntDir)/%(RelativeDir)/</ObjectFileName>

View File

@ -60,6 +60,7 @@
<ClCompile Include="hotkeys.cpp" />
<ClCompile Include="gpu_shadergen.cpp" />
<ClCompile Include="pch.cpp" />
<ClCompile Include="netplay.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="types.h" />
@ -124,5 +125,7 @@
<ClInclude Include="shader_cache_version.h" />
<ClInclude Include="gpu_shadergen.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="netplay.h" />
<ClInclude Include="netplay_packets.h" />
</ItemGroup>
</Project>
</Project>

View File

@ -217,6 +217,12 @@ void Host::Internal::SetInputSettingsLayer(SettingsInterface* sif)
s_layered_settings_interface.SetLayer(LayeredSettingsInterface::LAYER_INPUT, sif);
}
void Host::Internal::SetNetplaySettingsLayer(SettingsInterface* sif)
{
std::unique_lock lock(s_settings_mutex);
s_layered_settings_interface.SetLayer(LayeredSettingsInterface::LAYER_NETPLAY, sif);
}
void Host::ReportFormattedDebuggerMessage(const char* format, ...)
{
std::va_list ap;

View File

@ -119,5 +119,8 @@ void SetGameSettingsLayer(SettingsInterface* sif);
/// Sets the input profile settings layer. Called by VMManager when the game changes.
void SetInputSettingsLayer(SettingsInterface* sif);
/// Sets the netplay settings layer. Use once a session is established.
void SetNetplaySettingsLayer(SettingsInterface* sif);
} // namespace Internal
} // namespace Host

View File

@ -7,6 +7,7 @@
#include "gpu.h"
#include "host.h"
#include "imgui_overlays.h"
#include "netplay.h"
#include "pgxp.h"
#include "settings.h"
#include "spu.h"
@ -107,6 +108,17 @@ DEFINE_HOTKEY("OpenPauseMenu", TRANSLATE_NOOP("Hotkeys", "General"), TRANSLATE_N
if (!pressed)
FullscreenUI::OpenPauseMenu();
})
// Netplay on Android? Give me a break....
DEFINE_HOTKEY("OpenNetplayChat", TRANSLATE_NOOP("Hotkeys", "Netplay"), TRANSLATE_NOOP("Hotkeys", "Open Netplay Chat"),
[](s32 pressed) {
if (!pressed)
Netplay::OpenChat();
})
DEFINE_HOTKEY("ToggleDesyncNotifications", TRANSLATE_NOOP("Hotkeys", "Netplay"),
TRANSLATE_NOOP("Hotkeys", "Toggle Desync Notifications"), [](s32 pressed) {
if (!pressed)
Netplay::ToggleDesyncNotifications();
})
#endif
DEFINE_HOTKEY("FastForward", TRANSLATE_NOOP("Hotkeys", "General"), TRANSLATE_NOOP("Hotkeys", "Fast Forward"),

2914
src/core/netplay.cpp Normal file

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,76 @@
// 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 "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();
/// Called when a frame is completed (System::FrameDone()).
void FrameDone();
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();
// OSD
void RenderOverlays();
void OpenChat();
} // namespace Netplay
namespace Host {
void OnNetplaySessionOpened(bool is_host);
void OnNetplaySessionClosed();
} // namespace Host

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

@ -0,0 +1,319 @@
// 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_tty_logging;
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(TRANSLATE_FS("Netplay", "Connection lost to player {}."), 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_view ReasonToString() const
{
switch (reason)
{
case Reason::HostRequest:
return TRANSLATE_SV("Netplay", "Session closed due to host request.");
case Reason::HostShutdown:
return TRANSLATE_SV("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

@ -7,6 +7,7 @@
#include "interrupt_controller.h"
#include "memory_card.h"
#include "multitap.h"
#include "netplay.h"
#include "save_state_version.h"
#include "system.h"
#include "types.h"
@ -159,6 +160,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])
@ -175,12 +178,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)
@ -251,11 +254,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,
@ -269,7 +273,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
@ -281,7 +285,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)
@ -468,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

@ -24,6 +24,7 @@
#include "mdec.h"
#include "memory_card.h"
#include "multitap.h"
#include "netplay.h"
#include "pad.h"
#include "pcdrv.h"
#include "pgxp.h"
@ -71,6 +72,7 @@ Log_SetChannel(System);
#ifdef _WIN32
#include "common/windows_headers.h"
#include <mmsystem.h>
#include <WinSock2.h>
#endif
#ifdef ENABLE_DISCORD_PRESENCE
@ -241,6 +243,12 @@ static TinyString GetTimestampStringForFileName()
void System::Internal::ProcessStartup()
{
#if defined(_WIN32)
// Setup WinSock
WSADATA wd = {};
WSAStartup(MAKEWORD(2, 2), &wd);
#endif
if (!Bus::AllocateMemory())
Panic("Failed to allocate memory for emulated bus.");
@ -266,6 +274,11 @@ void System::Internal::ProcessShutdown()
InputManager::CloseSources();
Bus::ReleaseMemory();
#ifdef _WIN32
// Cleanup WinSock
WSACleanup();
#endif
}
void System::Internal::IdlePollUpdate()
@ -966,7 +979,7 @@ void System::ApplySettings(bool display_osd_messages)
CheckForSettingsChanges(old_config);
Host::CheckForSettingsChanges(old_config);
if (IsValid())
if (IsValid() && !Netplay::IsActive())
{
ResetPerformanceCounters();
if (s_system_executing)
@ -1077,7 +1090,7 @@ void System::ResetSystem()
void System::PauseSystem(bool paused)
{
if (paused == IsPaused() || !IsValid())
if (paused == IsPaused() || !IsValid() || Netplay::IsActive())
return;
SetState(paused ? State::Paused : State::Running);
@ -1117,7 +1130,7 @@ void System::PauseSystem(bool paused)
bool System::LoadState(const char* filename)
{
if (!IsValid())
if (!IsValid() || Netplay::IsActive())
return false;
if (Achievements::IsHardcoreModeActive() &&
@ -1748,6 +1761,12 @@ void System::FrameDone()
PollDiscordPresence();
#endif
if (Netplay::IsActive())
{
Netplay::FrameDone();
return;
}
if (s_frame_step_request)
{
s_frame_step_request = false;
@ -1855,6 +1874,9 @@ void System::UpdateThrottlePeriod()
}
ResetThrottler();
if (Netplay::IsActive())
Netplay::UpdateThrottlePeriod();
}
void System::ResetThrottler()
@ -2650,7 +2672,7 @@ bool System::IsFastForwardEnabled()
void System::SetFastForwardEnabled(bool enabled)
{
if (!IsValid())
if (!IsValid() || Netplay::IsActive())
return;
s_fast_forward_enabled = enabled;
@ -2664,7 +2686,7 @@ bool System::IsTurboEnabled()
void System::SetTurboEnabled(bool enabled)
{
if (!IsValid())
if (!IsValid() || Netplay::IsActive())
return;
s_turbo_enabled = enabled;
@ -2673,7 +2695,7 @@ void System::SetTurboEnabled(bool enabled)
void System::SetRewindState(bool enabled)
{
if (!System::IsValid())
if (!System::IsValid() || Netplay::IsActive())
return;
if (!g_settings.rewind_enable)
@ -2693,7 +2715,7 @@ void System::SetRewindState(bool enabled)
void System::DoFrameStep()
{
if (!IsValid())
if (!IsValid() || Netplay::IsActive())
return;
if (Achievements::IsHardcoreModeActive() && !Achievements::ConfirmHardcoreModeDisable("Frame stepping"))
@ -3993,6 +4015,9 @@ void System::ShutdownSystem(bool save_resume_state)
if (!IsValid())
return;
if (Netplay::IsActive())
Netplay::SystemDestroyed();
if (save_resume_state)
SaveResumeState();
@ -4598,6 +4623,8 @@ bool System::PresentDisplay(bool allow_skip_present)
if (!skip_present)
{
FullscreenUI::Render();
if (Netplay::IsActive())
Netplay::RenderOverlays();
ImGuiManager::RenderTextOverlays();
ImGuiManager::RenderOSDMessages();

View File

@ -123,6 +123,10 @@ set(SRCS
memorycardsettingswidget.h
memoryviewwidget.cpp
memoryviewwidget.h
netplaycreatesessiondialog.ui
netplayjoinsessiondialog.ui
netplaydialogs.cpp
netplaydialogs.h
postprocessingsettingswidget.cpp
postprocessingsettingswidget.h
postprocessingsettingswidget.ui

View File

@ -45,6 +45,7 @@
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="netplaydialogs.cpp" />
<ClCompile Include="postprocessingsettingswidget.cpp" />
<ClCompile Include="qttranslations.cpp" />
<ClCompile Include="qthost.cpp" />
@ -89,6 +90,7 @@
<QtMoc Include="memoryviewwidget.h" />
<QtMoc Include="logwindow.h" />
<ClInclude Include="pch.h" />
<QtMoc Include="netplaydialogs.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="settingwidgetbinder.h" />
<QtMoc Include="consolesettingswidget.h" />
@ -262,6 +264,7 @@
<ClCompile Include="$(IntDir)moc_inputbindingwidgets.cpp" />
<ClCompile Include="$(IntDir)moc_logwindow.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" />
@ -332,6 +335,12 @@
<QtUi Include="setupwizarddialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="netplaycreatesessiondialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="netplayjoinsessiondialog.ui">
<FileType>Document</FileType>
</QtUi>
<None Include="translations\duckstation-qt_es-es.ts" />
<None Include="translations\duckstation-qt_tr.ts" />
</ItemGroup>

View File

@ -96,6 +96,8 @@
<ClCompile Include="$(IntDir)moc_setupwizarddialog.cpp" />
<ClCompile Include="logwindow.cpp" />
<ClCompile Include="$(IntDir)moc_logwindow.cpp" />
<ClCompile Include="netplaydialogs.cpp" />
<ClCompile Include="$(IntDir)moc_netplaydialogs.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="qtutils.h" />
@ -158,6 +160,7 @@
<QtMoc Include="colorpickerbutton.h" />
<QtMoc Include="setupwizarddialog.h" />
<QtMoc Include="logwindow.h" />
<QtMoc Include="netplaydialogs.h" />
</ItemGroup>
<ItemGroup>
<QtUi Include="consolesettingswidget.ui" />
@ -199,6 +202,8 @@
<QtUi Include="coverdownloaddialog.ui" />
<QtUi Include="controllerledsettingsdialog.ui" />
<QtUi Include="setupwizarddialog.ui" />
<QtUi Include="netplaycreatesessiondialog.ui" />
<QtUi Include="netplayjoinsessiondialog.ui" />
</ItemGroup>
<ItemGroup>
<Natvis Include="qt5.natvis" />
@ -266,4 +271,4 @@
<Filter>translations</Filter>
</QtTs>
</ItemGroup>
</Project>
</Project>

View File

@ -14,6 +14,7 @@
#include "generalsettingswidget.h"
#include "logwindow.h"
#include "memorycardeditordialog.h"
#include "netplaydialogs.h"
#include "qthost.h"
#include "qtutils.h"
#include "settingsdialog.h"
@ -84,6 +85,7 @@ static bool s_use_central_widget = false;
// UI thread VM validity.
static bool s_system_valid = false;
static bool s_system_paused = false;
static bool s_netplay_active = false;
static QString s_current_game_title;
static QString s_current_game_serial;
static QString s_current_game_path;
@ -654,6 +656,18 @@ void MainWindow::onApplicationStateChanged(Qt::ApplicationState state)
}
}
void MainWindow::onNetplaySessionOpened(bool is_host)
{
s_netplay_active = true;
updateEmulationActions(false, s_system_valid, false);
}
void MainWindow::onNetplaySessionClosed()
{
s_netplay_active = false;
updateEmulationActions(false, s_system_valid, false);
}
void MainWindow::onStartFileActionTriggered()
{
QString filename = QDir::toNativeSeparators(
@ -1671,6 +1685,8 @@ void MainWindow::setupAdditionalUi()
void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevos_challenge_mode)
{
const bool netplay_active = s_netplay_active;
m_ui.actionStartFile->setDisabled(starting || running);
m_ui.actionStartDisc->setDisabled(starting || running);
m_ui.actionStartBios->setDisabled(starting || running);
@ -1680,25 +1696,30 @@ void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevo
m_ui.actionPowerOff->setDisabled(starting || !running);
m_ui.actionPowerOffWithoutSaving->setDisabled(starting || !running);
m_ui.actionReset->setDisabled(starting || !running);
m_ui.actionPause->setDisabled(starting || !running);
m_ui.actionChangeDisc->setDisabled(starting || !running);
m_ui.actionCheats->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionReset->setDisabled(starting || !running || netplay_active);
m_ui.actionPause->setDisabled(starting || !running || netplay_active);
m_ui.actionChangeDisc->setDisabled(starting || !running || netplay_active);
m_ui.actionCheats->setDisabled(starting || !running || cheevos_challenge_mode || netplay_active);
m_ui.actionScreenshot->setDisabled(starting || !running);
m_ui.menuChangeDisc->setDisabled(starting || !running);
m_ui.menuCheats->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionCheatManager->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionCPUDebugger->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.menuChangeDisc->setDisabled(starting || !running || netplay_active);
m_ui.menuCheats->setDisabled(starting || !running || cheevos_challenge_mode || netplay_active);
m_ui.actionCheatManager->setDisabled(starting || !running || cheevos_challenge_mode || netplay_active);
m_ui.actionCPUDebugger->setDisabled(starting || !running || cheevos_challenge_mode || netplay_active);
m_ui.actionDumpRAM->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionDumpVRAM->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionDumpSPURAM->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionSaveState->setDisabled(starting || !running);
m_ui.menuSaveState->setDisabled(starting || !running);
m_ui.actionLoadState->setDisabled(netplay_active);
m_ui.menuLoadState->setDisabled(netplay_active);
m_ui.actionSaveState->setDisabled(starting || !running || netplay_active);
m_ui.menuSaveState->setDisabled(starting || !running || netplay_active);
m_ui.menuWindowSize->setDisabled(starting || !running);
m_ui.actionViewGameProperties->setDisabled(starting || !running);
m_ui.actionCreateNetplaySession->setDisabled(!running || cheevos_challenge_mode || netplay_active);
m_ui.actionJoinNetplaySession->setDisabled(cheevos_challenge_mode || netplay_active);
if (starting || running)
{
if (!m_ui.toolBar->actions().contains(m_ui.actionPowerOff))
@ -2016,6 +2037,8 @@ void MainWindow::connectSignals()
connect(g_emu_thread, &EmuThread::achievementsLoginSucceeded, this, &MainWindow::onAchievementsLoginSucceeded);
connect(g_emu_thread, &EmuThread::achievementsChallengeModeChanged, this,
&MainWindow::onAchievementsChallengeModeChanged);
connect(g_emu_thread, &EmuThread::netplaySessionOpened, this, &MainWindow::onNetplaySessionOpened);
connect(g_emu_thread, &EmuThread::netplaySessionClosed, this, &MainWindow::onNetplaySessionClosed);
// These need to be queued connections to stop crashing due to menus opening/closing and switching focus.
connect(m_game_list_widget, &GameListWidget::refreshProgress, this, &MainWindow::onGameListRefreshProgress);
@ -2089,6 +2112,10 @@ void MainWindow::connectSignals()
}
updateMenuSelectedTheme();
// Netplay UI , TODO
connect(m_ui.actionCreateNetplaySession, &QAction::triggered, this, &MainWindow::onCreateNetplaySessionClicked);
connect(m_ui.actionJoinNetplaySession, &QAction::triggered, this, &MainWindow::onJoinNetplaySessionClicked);
}
void MainWindow::setTheme(const QString& theme)
@ -2550,6 +2577,7 @@ bool MainWindow::requestShutdown(bool allow_confirm /* = true */, bool allow_sav
// If we don't have a serial, we can't save state.
allow_save_to_state &= !s_current_game_serial.isEmpty();
allow_save_to_state &= !s_netplay_active;
save_state &= allow_save_to_state;
// Only confirm on UI thread because we need to display a msgbox.
@ -2793,6 +2821,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)));
@ -2876,7 +2916,7 @@ MainWindow::SystemLock MainWindow::pauseAndLockSystem()
#else
const bool was_fullscreen = false;
#endif
const bool was_paused = !s_system_valid || s_system_paused;
const bool was_paused = !s_system_valid || s_system_paused || s_netplay_active;
// We need to switch out of exclusive fullscreen before we can display our popup.
// However, we do not want to switch back to render-to-main, the window might have generated this event.

View File

@ -133,6 +133,8 @@ private Q_SLOTS:
quint32 unread_messages);
void onAchievementsChallengeModeChanged(bool enabled);
void onApplicationStateChanged(Qt::ApplicationState state);
void onNetplaySessionOpened(bool is_host);
void onNetplaySessionClosed();
void onStartFileActionTriggered();
void onStartDiscActionTriggered();
@ -177,6 +179,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

@ -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"/>
@ -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,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NetplayCreateSessionDialog</class>
<widget class="QDialog" name="NetplayCreateSessionDialog">
<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

@ -0,0 +1,129 @@
// SPDX-FileCopyrightText: 2023 Connor McLaughlin <stenzek@gmail.com> and contributors.
// 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> and contributors.
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#pragma once
#include "ui_netplaycreatesessiondialog.h"
#include "ui_netplayjoinsessiondialog.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::NetplayCreateSessionDialog 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::NetplayJoinSessionDialog m_ui;
};

View File

@ -0,0 +1,276 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NetplayJoinSessionDialog</class>
<widget class="QDialog" name="NetplayJoinSessionDialog">
<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

@ -20,6 +20,7 @@
#include "core/host.h"
#include "core/imgui_overlays.h"
#include "core/memory_card.h"
#include "core/netplay.h"
#include "core/spu.h"
#include "core/system.h"
@ -106,6 +107,9 @@ static bool s_start_fullscreen_ui = false;
static bool s_start_fullscreen_ui_fullscreen = false;
static bool s_run_setup_wizard = false;
// TODO: REMOVE ME
static int s_netplay_test = -1;
EmuThread* g_emu_thread;
GDBServer* g_gdb_server;
@ -975,6 +979,62 @@ void EmuThread::updatePostProcessingSettings()
PostProcessing::UpdateSettings();
}
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;
}
setInitialState(std::nullopt);
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 Host::OnNetplaySessionOpened(bool is_host)
{
emit g_emu_thread->netplaySessionOpened(is_host);
}
void Host::OnNetplaySessionClosed()
{
emit g_emu_thread->netplaySessionClosed();
}
void EmuThread::clearInputBindStateFromSource(InputBindingKey key)
{
if (!isOnThread())
@ -1327,11 +1387,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();
}
@ -1906,7 +1971,12 @@ bool QtHost::ParseCommandLineParametersAndInitializeConfig(QApplication& app,
continue;
}
#ifdef ENABLE_RAINTEGRATION
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"))
{
Achievements::SwitchToRAIntegration();
@ -2070,6 +2140,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.
result = app.exec();

View File

@ -147,6 +147,8 @@ Q_SIGNALS:
void achievementsRefreshed(quint32 id, const QString& game_info_string);
void achievementsChallengeModeChanged(bool enabled);
void cheatEnabled(quint32 index, bool enabled);
void netplaySessionOpened(bool is_host);
void netplaySessionClosed();
public Q_SLOTS:
void setDefaultSettings(bool system = true, bool controller = true);
@ -192,6 +194,10 @@ public Q_SLOTS:
void applyCheat(quint32 index);
void reloadPostProcessingShaders();
void updatePostProcessingSettings();
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:
@ -236,6 +242,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();
RenderAPI m_last_render_api = RenderAPI::None;
bool m_last_hardware_renderer = false;
};

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"
@ -714,6 +715,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);