Netplay: Add work-in-progress rollback netplay implementation
Co-authored-by: Jamie Meyer <45072324+HeatXD@users.noreply.github.com>
This commit is contained in:
parent
fd6aeefd93
commit
3293626898
|
@ -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" />
|
||||
|
|
|
@ -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>
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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
|
|
@ -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))
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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><html><head/><body><p>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.</p></body></html></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>
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
|
@ -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)));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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());
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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>
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -8,6 +8,9 @@
|
|||
namespace ImGuiManager {
|
||||
void RenderTextOverlays();
|
||||
void RenderOverlayWindows();
|
||||
|
||||
void RenderNetplayOverlays();
|
||||
void OpenNetplayChat();
|
||||
}
|
||||
|
||||
namespace SaveStateSelectorUI {
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue