Netplay: Send session details to clients
This commit is contained in:
parent
a6a7a1613c
commit
e1e2dcd435
|
@ -376,6 +376,35 @@ std::optional<std::vector<u8>> BIOS::FindBIOSImageInDirectory(ConsoleRegion regi
|
|||
return fallback_image;
|
||||
}
|
||||
|
||||
std::string BIOS::FindBIOSPathWithHash(const char* directory, const Hash& hash)
|
||||
{
|
||||
FileSystem::FindResultsArray files;
|
||||
FileSystem::FindFiles(directory, "*",
|
||||
FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES | FILESYSTEM_FIND_RELATIVE_PATHS, &files);
|
||||
|
||||
std::string ret;
|
||||
|
||||
for (FILESYSTEM_FIND_DATA& fd : files)
|
||||
{
|
||||
if (fd.Size != BIOS_SIZE && fd.Size != BIOS_SIZE_PS2 && fd.Size != BIOS_SIZE_PS3)
|
||||
continue;
|
||||
|
||||
std::string full_path(Path::Combine(directory, fd.FileName));
|
||||
std::optional<Image> found_image = LoadImageFromFile(full_path.c_str());
|
||||
if (!found_image)
|
||||
continue;
|
||||
|
||||
const BIOS::Hash found_hash = GetImageHash(found_image.value());
|
||||
if (found_hash == hash)
|
||||
{
|
||||
ret = std::move(full_path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, const BIOS::ImageInfo*>> BIOS::FindBIOSImagesInDirectory(const char* directory)
|
||||
{
|
||||
std::vector<std::pair<std::string, const ImageInfo*>> results;
|
||||
|
|
|
@ -81,6 +81,9 @@ std::optional<std::vector<u8>> GetBIOSImage(ConsoleRegion region);
|
|||
/// BIOS image within 512KB and 4MB will be used.
|
||||
std::optional<std::vector<u8>> FindBIOSImageInDirectory(ConsoleRegion region, const char* directory);
|
||||
|
||||
/// Returns a BIOS image which matches the specified hash.
|
||||
std::string FindBIOSPathWithHash(const char* directory, const BIOS::Hash& hash);
|
||||
|
||||
/// Returns a list of filenames and descriptions for BIOS images in a directory.
|
||||
std::vector<std::pair<std::string, const BIOS::ImageInfo*>> FindBIOSImagesInDirectory(const char* directory);
|
||||
|
||||
|
|
|
@ -140,6 +140,7 @@
|
|||
<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" />
|
||||
|
|
|
@ -129,5 +129,6 @@
|
|||
<ClInclude Include="input_types.h" />
|
||||
<ClInclude Include="pcdrv.h" />
|
||||
<ClInclude Include="netplay.h" />
|
||||
<ClInclude Include="netplay_packets.h" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
|
@ -1,4 +1,5 @@
|
|||
#include "netplay.h"
|
||||
#include "bios.h"
|
||||
#include "common/byte_stream.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/gpu_texture.h"
|
||||
|
@ -13,9 +14,11 @@
|
|||
#include "netplay_packets.h"
|
||||
#include "pad.h"
|
||||
#include "save_state_version.h"
|
||||
#include "settings.h"
|
||||
#include "spu.h"
|
||||
#include "system.h"
|
||||
#include <bitset>
|
||||
#include <cinttypes>
|
||||
#include <deque>
|
||||
#include <gsl/span>
|
||||
#include <xxhash.h>
|
||||
|
@ -30,6 +33,9 @@ Log_SetChannel(Netplay);
|
|||
#include "enet/enet.h"
|
||||
#include "ggponet.h"
|
||||
|
||||
// TODO: We don't want a core->frontend-common dependency. I'll move GameList to core at some point...
|
||||
#include "frontend-common/game_list.h"
|
||||
|
||||
namespace Netplay {
|
||||
|
||||
using SaveStateBuffer = std::unique_ptr<System::MemorySaveState>;
|
||||
|
@ -57,7 +63,7 @@ static void SetInputs(Input inputs[2]);
|
|||
|
||||
static void SetSettings();
|
||||
|
||||
static bool CreateSystem(std::string game_path, bool hosting);
|
||||
static bool CreateDummySystem();
|
||||
static void CloseSessionWithError(const std::string_view& message);
|
||||
static void RequestCloseSession(CloseSessionMessage::Reason reason);
|
||||
|
||||
|
@ -85,6 +91,7 @@ static void SendConnectRequest();
|
|||
static void HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt);
|
||||
static void HandleControlMessage(s32 player_id, const ENetPacket* pkt);
|
||||
static void HandleConnectResponseMessage(s32 player_id, const ENetPacket* pkt);
|
||||
static void HandleJoinResponseMessage(s32 player_id, const ENetPacket* pkt);
|
||||
static void HandleResetMessage(s32 player_id, const ENetPacket* pkt);
|
||||
static void HandleResetCompleteMessage(s32 player_id, const ENetPacket* pkt);
|
||||
static void HandleResumeSessionMessage(s32 player_id, const ENetPacket* pkt);
|
||||
|
@ -297,7 +304,7 @@ bool Netplay::Start(bool is_hosting, std::string nickname, const std::string& re
|
|||
Log_ErrorPrintf("Can't host a netplay session without a valid VM");
|
||||
return false;
|
||||
}
|
||||
else if (!is_hosting && !CreateSystem(std::string(), false))
|
||||
else if (!is_hosting && !CreateDummySystem())
|
||||
{
|
||||
Log_ErrorPrintf("Failed to create VM for joining session");
|
||||
return false;
|
||||
|
@ -403,27 +410,16 @@ bool Netplay::IsActive()
|
|||
return (s_state >= SessionState::Initializing && s_state <= SessionState::Running);
|
||||
}
|
||||
|
||||
bool Netplay::CreateSystem(std::string game_path, bool is_hosting)
|
||||
bool Netplay::CreateDummySystem()
|
||||
{
|
||||
// close system if its already running
|
||||
if (System::IsValid())
|
||||
System::ShutdownSystem(false);
|
||||
|
||||
// fast boot the selected game and wait for the other player
|
||||
auto param = SystemBootParameters(std::move(game_path));
|
||||
param.override_fast_boot = true;
|
||||
if (!System::BootSystem(param))
|
||||
if (!System::BootSystem(SystemBootParameters()))
|
||||
return false;
|
||||
|
||||
if (is_hosting)
|
||||
{
|
||||
// Fast Forward to Game Start if needed.
|
||||
SPU::SetAudioOutputMuted(true);
|
||||
while (System::GetInternalFrameNumber() < 2)
|
||||
System::RunFrame();
|
||||
SPU::SetAudioOutputMuted(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -589,7 +585,7 @@ void Netplay::HandleEnetEvent(const ENetEvent* event)
|
|||
if (player_id < 0)
|
||||
{
|
||||
// If it's a new connection, we need to handle connection request messages.
|
||||
if (event->channelID == ENET_CHANNEL_CONTROL)
|
||||
if (event->channelID == ENET_CHANNEL_CONTROL && IsHost())
|
||||
HandleMessageFromNewPeer(event->peer, event->packet);
|
||||
enet_packet_destroy(event->packet);
|
||||
return;
|
||||
|
@ -786,6 +782,10 @@ void Netplay::HandleControlMessage(s32 player_id, const ENetPacket* pkt)
|
|||
HandleConnectResponseMessage(player_id, pkt);
|
||||
break;
|
||||
|
||||
case ControlMessage::JoinResponse:
|
||||
HandleJoinResponseMessage(player_id, pkt);
|
||||
break;
|
||||
|
||||
case ControlMessage::Reset:
|
||||
HandleResetMessage(player_id, pkt);
|
||||
break;
|
||||
|
@ -826,15 +826,125 @@ void Netplay::HandleControlMessage(s32 player_id, const ENetPacket* pkt)
|
|||
|
||||
void Netplay::HandlePeerConnectionAsHost(ENetPeer* peer)
|
||||
{
|
||||
// don't do anything until they send a connect request
|
||||
// TODO: we might want to put an idle timeout here...
|
||||
Log_InfoPrintf(fmt::format("New peer connection from {}", PeerAddressString(peer)).c_str());
|
||||
|
||||
// send them the session details
|
||||
const std::string& game_title = System::GetGameTitle();
|
||||
const std::string& game_serial = System::GetGameSerial();
|
||||
auto pkt = NewControlPacket<ConnectResponseMessage>(
|
||||
sizeof(ConnectResponseMessage) + static_cast<u32>(game_serial.length()) + static_cast<u32>(game_title.length()));
|
||||
pkt->num_players = s_num_players;
|
||||
pkt->max_players = MAX_PLAYERS;
|
||||
pkt->console_region = System::GetRegion();
|
||||
pkt->game_title_length = static_cast<u32>(game_title.length());
|
||||
pkt->game_serial_length = static_cast<u32>(game_serial.length());
|
||||
pkt->game_hash = System::GetGameHash();
|
||||
pkt->bios_hash = System::GetBIOSHash();
|
||||
pkt->was_fast_booted = System::WasFastBooted();
|
||||
std::memcpy(pkt.pkt->data + sizeof(ConnectResponseMessage), game_serial.c_str(), pkt->game_serial_length);
|
||||
std::memcpy(pkt.pkt->data + sizeof(ConnectResponseMessage) + pkt->game_serial_length, game_title.c_str(),
|
||||
pkt->game_title_length);
|
||||
SendControlPacket(peer, pkt);
|
||||
}
|
||||
|
||||
void Netplay::HandleConnectResponseMessage(s32 player_id, const ENetPacket* pkt)
|
||||
{
|
||||
const ConnectResponseMessage* msg = CheckReceivedPacket<ConnectResponseMessage>(-1, pkt);
|
||||
if (!msg || player_id != s_host_player_id)
|
||||
{
|
||||
Log_ErrorPrintf("Received unexpected connect response from player %d", player_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore these messages when reconnecting
|
||||
if (s_state != SessionState::Connecting)
|
||||
{
|
||||
Log_DevPrintf("Ignoring connect response because we're not initially connecting");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!msg->Validate())
|
||||
{
|
||||
CloseSessionWithError(Host::TranslateStdString("Netplay", "Cannot join session: Invalid details recieved."));
|
||||
return;
|
||||
}
|
||||
|
||||
Log_InfoPrintf("Received session details from host: ");
|
||||
Log_InfoPrintf(" Console Region: %s", Settings::GetConsoleRegionDisplayName(msg->console_region));
|
||||
Log_InfoPrintf(" BIOS Hash: %s%s", msg->bios_hash.ToString().c_str(), msg->was_fast_booted ? " (fast booted)" : "");
|
||||
Log_InfoPrintf(" Game Serial: %.*s", msg->game_serial_length, msg->GetGameSerial().data());
|
||||
Log_InfoPrintf(" Game Title: %.*s", msg->game_title_length, msg->GetGameTitle().data());
|
||||
Log_InfoPrintf(" Game Hash: %" PRIX64, msg->game_hash);
|
||||
|
||||
// Find a matching BIOS.
|
||||
const std::string bios_path = BIOS::FindBIOSPathWithHash(EmuFolders::Bios.c_str(), msg->bios_hash);
|
||||
if (bios_path.empty())
|
||||
{
|
||||
CloseSessionWithError(fmt::format(
|
||||
Host::TranslateString("Netplay", "Cannot join session: Unable to find BIOS with hash {}.").GetCharArray(),
|
||||
msg->bios_hash.ToString()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the matching game.
|
||||
const GameList::Entry* entry = GameList::GetEntryBySerialAndHash(msg->GetGameSerial(), msg->game_hash);
|
||||
if (!entry)
|
||||
{
|
||||
CloseSessionWithError(fmt::format(
|
||||
Host::TranslateString("Netplay", "Cannot join session: Unable to find game \"{}\".\nSerial: {}\nHash: {}")
|
||||
.GetCharArray(),
|
||||
msg->GetGameTitle(), msg->GetGameSerial(), System::GetGameHashId(msg->game_hash)));
|
||||
return;
|
||||
}
|
||||
|
||||
Log_InfoPrintf("Found matching BIOS: %s", bios_path.c_str());
|
||||
Log_InfoPrintf("Found matching game: %s", entry->path.c_str());
|
||||
|
||||
// Reboot created system with host details.
|
||||
if (!System::ReinitializeSystem(msg->console_region, bios_path.c_str(), entry->path.c_str(), msg->was_fast_booted))
|
||||
{
|
||||
CloseSessionWithError(Host::TranslateStdString("Netplay", "Cannot join session: Failed to reinitialize system."));
|
||||
return;
|
||||
}
|
||||
|
||||
// We're ready to go, send the connection request.
|
||||
SendConnectRequest();
|
||||
}
|
||||
|
||||
void Netplay::SendConnectRequest()
|
||||
{
|
||||
DebugAssert(!IsHost());
|
||||
|
||||
Log_DevPrintf("Sending connect request to host with player id %d", s_player_id);
|
||||
|
||||
auto pkt = NewControlPacket<JoinRequestMessage>();
|
||||
pkt->mode = JoinRequestMessage::Mode::Player;
|
||||
pkt->requested_player_id = s_player_id;
|
||||
std::memset(pkt->nickname, 0, sizeof(pkt->nickname));
|
||||
std::memset(pkt->session_password, 0, sizeof(pkt->session_password));
|
||||
StringUtil::Strlcpy(pkt->nickname, s_local_nickname, std::size(pkt->nickname));
|
||||
SendControlPacket(s_peers[s_host_player_id].peer, pkt);
|
||||
}
|
||||
|
||||
void Netplay::UpdateConnectingState()
|
||||
{
|
||||
if (s_reset_start_time.GetTimeSeconds() >= MAX_CONNECT_TIME)
|
||||
{
|
||||
CloseSessionWithError(Host::TranslateStdString("Netplay", "Timed out connecting to server."));
|
||||
return;
|
||||
}
|
||||
|
||||
// still waiting for connection to host..
|
||||
PollEnet(Common::Timer::GetCurrentValue() + Common::Timer::ConvertMillisecondsToValue(16));
|
||||
Host::DisplayLoadingScreen("Connecting to host...");
|
||||
Host::PumpMessagesOnCPUThread();
|
||||
}
|
||||
|
||||
void Netplay::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt)
|
||||
{
|
||||
const ConnectRequestMessage* msg = CheckReceivedPacket<ConnectRequestMessage>(-1, pkt);
|
||||
if (!msg || msg->header.type != ControlMessage::ConnectRequest)
|
||||
const JoinRequestMessage* msg = CheckReceivedPacket<JoinRequestMessage>(-1, pkt);
|
||||
if (!msg || msg->header.type != ControlMessage::JoinRequest)
|
||||
{
|
||||
Log_WarningPrintf("Received unknown packet from unknown player");
|
||||
enet_peer_reset(peer);
|
||||
|
@ -845,13 +955,13 @@ void Netplay::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt)
|
|||
msg->requested_player_id)
|
||||
.c_str());
|
||||
|
||||
PacketWrapper<ConnectResponseMessage> response = NewControlPacket<ConnectResponseMessage>();
|
||||
PacketWrapper<JoinResponseMessage> response = NewControlPacket<JoinResponseMessage>();
|
||||
response->player_id = -1;
|
||||
|
||||
// TODO: Spectators shouldn't get assigned a real player ID, they should go into a separate peer list.
|
||||
if (msg->mode != ConnectRequestMessage::Mode::Player)
|
||||
if (msg->mode != JoinRequestMessage::Mode::Player)
|
||||
{
|
||||
response->result = ConnectResponseMessage::Result::SessionClosed;
|
||||
response->result = JoinResponseMessage::Result::SessionClosed;
|
||||
SendControlPacket(peer, response);
|
||||
return;
|
||||
}
|
||||
|
@ -862,7 +972,7 @@ void Netplay::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt)
|
|||
if (msg->requested_player_id >= 0 && IsValidPlayerId(msg->requested_player_id))
|
||||
{
|
||||
Log_ErrorPrintf("Player ID %d is already in use, rejecting connection.", msg->requested_player_id);
|
||||
response->result = ConnectResponseMessage::Result::PlayerIDInUse;
|
||||
response->result = JoinResponseMessage::Result::PlayerIDInUse;
|
||||
SendControlPacket(peer, response);
|
||||
return;
|
||||
}
|
||||
|
@ -872,14 +982,14 @@ void Netplay::HandleMessageFromNewPeer(ENetPeer* peer, const ENetPacket* pkt)
|
|||
if (new_player_id < 0)
|
||||
{
|
||||
Log_ErrorPrintf("Server full, rejecting connection.");
|
||||
response->result = ConnectResponseMessage::Result::ServerFull;
|
||||
response->result = JoinResponseMessage::Result::ServerFull;
|
||||
SendControlPacket(peer, response);
|
||||
return;
|
||||
}
|
||||
|
||||
Log_VerbosePrint(
|
||||
fmt::format("New connection from {} assigned to player ID {}", PeerAddressString(peer), new_player_id).c_str());
|
||||
response->result = ConnectResponseMessage::Result::Success;
|
||||
response->result = JoinResponseMessage::Result::Success;
|
||||
response->player_id = new_player_id;
|
||||
SendControlPacket(peer, response);
|
||||
|
||||
|
@ -900,19 +1010,16 @@ void Netplay::HandlePeerConnectionAsNonHost(ENetPeer* peer, s32 claimed_player_i
|
|||
{
|
||||
if (s_state == SessionState::Connecting)
|
||||
{
|
||||
if (peer == s_peers[s_host_player_id].peer)
|
||||
{
|
||||
SendConnectRequest();
|
||||
return;
|
||||
}
|
||||
else
|
||||
if (peer != s_peers[s_host_player_id].peer)
|
||||
{
|
||||
Log_ErrorPrintf(
|
||||
fmt::format("Unexpected connection from {} claiming player ID {}", PeerAddressString(peer), claimed_player_id)
|
||||
.c_str());
|
||||
enet_peer_disconnect_now(peer, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// wait for session details
|
||||
return;
|
||||
}
|
||||
|
||||
Log_VerbosePrint(
|
||||
|
@ -942,45 +1049,16 @@ void Netplay::HandlePeerConnectionAsNonHost(ENetPeer* peer, s32 claimed_player_i
|
|||
s_peers[claimed_player_id].peer = peer;
|
||||
}
|
||||
|
||||
void Netplay::SendConnectRequest()
|
||||
{
|
||||
DebugAssert(!IsHost());
|
||||
|
||||
Log_DevPrintf("Sending connect request to host with player id %d", s_player_id);
|
||||
|
||||
auto pkt = NewControlPacket<ConnectRequestMessage>();
|
||||
pkt->mode = ConnectRequestMessage::Mode::Player;
|
||||
pkt->requested_player_id = s_player_id;
|
||||
std::memset(pkt->nickname, 0, sizeof(pkt->nickname));
|
||||
std::memset(pkt->session_password, 0, sizeof(pkt->session_password));
|
||||
StringUtil::Strlcpy(pkt->nickname, s_local_nickname, std::size(pkt->nickname));
|
||||
SendControlPacket(s_peers[s_host_player_id].peer, pkt);
|
||||
}
|
||||
|
||||
void Netplay::UpdateConnectingState()
|
||||
{
|
||||
if (s_reset_start_time.GetTimeSeconds() >= MAX_CONNECT_TIME)
|
||||
{
|
||||
CloseSessionWithError(Host::TranslateStdString("Netplay", "Timed out connecting to server."));
|
||||
return;
|
||||
}
|
||||
|
||||
// still waiting for connection to host..
|
||||
PollEnet(Common::Timer::GetCurrentValue() + Common::Timer::ConvertMillisecondsToValue(16));
|
||||
Host::DisplayLoadingScreen("Connecting to host...");
|
||||
Host::PumpMessagesOnCPUThread();
|
||||
}
|
||||
|
||||
void Netplay::HandleConnectResponseMessage(s32 player_id, const ENetPacket* pkt)
|
||||
void Netplay::HandleJoinResponseMessage(s32 player_id, const ENetPacket* pkt)
|
||||
{
|
||||
if (s_state != SessionState::Connecting)
|
||||
{
|
||||
Log_ErrorPrintf("Received unexpected connect response from player %d", player_id);
|
||||
Log_ErrorPrintf("Received unexpected join response from player %d", player_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const ConnectResponseMessage* msg = CheckReceivedPacket<ConnectResponseMessage>(player_id, pkt);
|
||||
if (msg->result != ConnectResponseMessage::Result::Success)
|
||||
const JoinResponseMessage* msg = CheckReceivedPacket<JoinResponseMessage>(player_id, pkt);
|
||||
if (msg->result != JoinResponseMessage::Result::Success)
|
||||
{
|
||||
CloseSessionWithError(
|
||||
fmt::format("Connection rejected by server with error code {}", static_cast<u32>(msg->result)));
|
||||
|
@ -1025,7 +1103,7 @@ void Netplay::Reset()
|
|||
// TODO: This save state has the bloody path to the disc in it. We need a new save state format.
|
||||
// We also want to use maximum compression.
|
||||
GrowableMemoryByteStream state(nullptr, System::MAX_SAVE_STATE_SIZE);
|
||||
if (!System::SaveStateToStream(&state, 0, SAVE_STATE_HEADER::COMPRESSION_TYPE_ZSTD))
|
||||
if (!System::SaveStateToStream(&state, 0, SAVE_STATE_HEADER::COMPRESSION_TYPE_ZSTD, true))
|
||||
Panic("Failed to save state...");
|
||||
|
||||
const u32 state_data_size = static_cast<u32>(state.GetPosition());
|
||||
|
@ -1086,7 +1164,7 @@ void Netplay::Reset()
|
|||
// having a different number of cycles.
|
||||
// CPU::CodeCache::Flush();
|
||||
state.SeekAbsolute(0);
|
||||
if (!System::LoadStateFromStream(&state, true))
|
||||
if (!System::LoadStateFromStream(&state, true, true))
|
||||
Panic("Failed to reload host state");
|
||||
|
||||
s_state = SessionState::Resetting;
|
||||
|
@ -1177,7 +1255,7 @@ void Netplay::HandleResetMessage(s32 player_id, const ENetPacket* pkt)
|
|||
// Load state from packet.
|
||||
Log_VerbosePrintf("Loading state from host");
|
||||
ReadOnlyMemoryByteStream stream(pkt->data + sizeof(ResetMessage), msg->state_data_size);
|
||||
if (!System::LoadStateFromStream(&stream, true))
|
||||
if (!System::LoadStateFromStream(&stream, true, true))
|
||||
Panic("Failed to load state from host");
|
||||
|
||||
s_state = SessionState::Resetting;
|
||||
|
@ -1429,9 +1507,25 @@ void Netplay::SetSettings()
|
|||
si.SetStringValue(Controller::GetSettingsSection(i).c_str(), "Type",
|
||||
Settings::GetControllerTypeName(ControllerType::DigitalController));
|
||||
}
|
||||
for (u32 i = MAX_PLAYERS; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
|
||||
{
|
||||
si.SetStringValue(Controller::GetSettingsSection(i).c_str(), "Type",
|
||||
Settings::GetControllerTypeName(ControllerType::None));
|
||||
}
|
||||
// We want all players to have the same memory card contents.
|
||||
for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
|
||||
{
|
||||
si.SetStringValue("MemoryCards", fmt::format("Card{}Type", i + 1).c_str(),
|
||||
Settings::GetMemoryCardTypeName((i == 0) ? MemoryCardType::NonPersistent : MemoryCardType::None));
|
||||
}
|
||||
|
||||
// si.SetStringValue("CPU", "ExecutionMode", "Interpreter");
|
||||
|
||||
// BIOS patching must be the same.
|
||||
si.SetBoolValue("BIOS", "PatchTTYEnable", false);
|
||||
si.SetBoolValue("BIOS", "PatchFastBoot", true);
|
||||
si.SetBoolValue("CDROM", "LoadImagePatches", false);
|
||||
|
||||
// No runahead or rewind, that'd be a disaster.
|
||||
si.SetIntValue("Main", "RunaheadFrameCount", 0);
|
||||
si.SetBoolValue("Main", "RewindEnable", false);
|
||||
|
@ -1642,30 +1736,14 @@ void Netplay::SetInputs(Netplay::Input inputs[2])
|
|||
}
|
||||
}
|
||||
|
||||
void Netplay::TestNetplaySession(s32 local_handle, u16 local_port, const std::string& remote_addr, u16 remote_port,
|
||||
s32 input_delay, std::string game_path)
|
||||
{
|
||||
const bool is_hosting = (local_handle == 1);
|
||||
if (!CreateSystem(std::move(game_path), is_hosting))
|
||||
{
|
||||
Log_ErrorPrintf("Failed to create system.");
|
||||
return;
|
||||
}
|
||||
|
||||
// create session
|
||||
std::string nickname = fmt::format("NICKNAME{}", local_handle);
|
||||
if (!Netplay::Start(is_hosting, std::move(nickname), remote_addr, is_hosting ? local_port : remote_port, input_delay))
|
||||
{
|
||||
// this'll call back to us to shut everything netplay-related down
|
||||
Log_ErrorPrint("Failed to Create Netplay Session!");
|
||||
System::ShutdownSystem(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool Netplay::CreateSession(std::string nickname, s32 port, s32 max_players, std::string password)
|
||||
{
|
||||
// TODO: Password
|
||||
|
||||
// TODO: This is going to blow away our memory cards, because for sync purposes we want all clients
|
||||
// to have the same data, and we don't want to trash their local memcards. We should therefore load
|
||||
// the memory cards for this game (based on game/global settings), and copy that to the temp card.
|
||||
|
||||
const s32 input_delay = 1;
|
||||
|
||||
if (!Netplay::Start(true, std::move(nickname), std::string(), port, input_delay))
|
||||
|
@ -1673,13 +1751,11 @@ bool Netplay::CreateSession(std::string nickname, s32 port, s32 max_players, std
|
|||
CloseSession();
|
||||
return false;
|
||||
}
|
||||
else if (IsHost())
|
||||
{
|
||||
// Load savestate if available and only when you are the host.
|
||||
// the other peers will get state from the host
|
||||
auto save_path = fmt::format("{}\\netplay\\{}.sav", EmuFolders::SaveStates, System::GetGameSerial());
|
||||
System::LoadState(save_path.c_str());
|
||||
}
|
||||
|
||||
// Load savestate if available and only when you are the host.
|
||||
// the other peers will get state from the host
|
||||
auto save_path = fmt::format("{}\\netplay\\{}.sav", EmuFolders::SaveStates, System::GetGameSerial());
|
||||
System::LoadState(save_path.c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -28,8 +28,6 @@ enum : u8
|
|||
NUM_ENET_CHANNELS,
|
||||
};
|
||||
|
||||
void TestNetplaySession(s32 local_handle, u16 local_port, const std::string& remote_addr, u16 remote_port,
|
||||
s32 input_delay, std::string game_path);
|
||||
bool CreateSession(std::string nickname, s32 port, s32 max_players, std::string password);
|
||||
bool JoinSession(std::string nickname, const std::string& hostname, s32 port, std::string password);
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "bios.h"
|
||||
#include "host.h"
|
||||
#include "types.h"
|
||||
|
||||
|
@ -24,6 +25,7 @@ enum class ControlMessage : u32
|
|||
{
|
||||
// host->player
|
||||
ConnectResponse,
|
||||
JoinResponse,
|
||||
Reset,
|
||||
ResumeSession,
|
||||
PlayerJoined,
|
||||
|
@ -31,7 +33,7 @@ enum class ControlMessage : u32
|
|||
CloseSession,
|
||||
|
||||
// player->host
|
||||
ConnectRequest,
|
||||
JoinRequest,
|
||||
ResetComplete,
|
||||
ResetRequest,
|
||||
|
||||
|
@ -53,7 +55,39 @@ struct ControlMessageHeader
|
|||
u32 size;
|
||||
};
|
||||
|
||||
struct ConnectRequestMessage
|
||||
struct ConnectResponseMessage
|
||||
{
|
||||
ControlMessageHeader header;
|
||||
|
||||
s32 num_players;
|
||||
s32 max_players;
|
||||
u64 game_hash;
|
||||
u32 game_serial_length;
|
||||
u32 game_title_length;
|
||||
ConsoleRegion console_region;
|
||||
BIOS::Hash bios_hash;
|
||||
bool was_fast_booted;
|
||||
|
||||
// <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>(console_region) < static_cast<unsigned>(ConsoleRegion::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
|
||||
{
|
||||
|
@ -80,10 +114,10 @@ struct ConnectRequestMessage
|
|||
return std::string_view(session_password, len);
|
||||
}
|
||||
|
||||
static ControlMessage MessageType() { return ControlMessage::ConnectRequest; }
|
||||
static ControlMessage MessageType() { return ControlMessage::JoinRequest; }
|
||||
};
|
||||
|
||||
struct ConnectResponseMessage
|
||||
struct JoinResponseMessage
|
||||
{
|
||||
enum class Result : u32
|
||||
{
|
||||
|
@ -98,7 +132,7 @@ struct ConnectResponseMessage
|
|||
Result result;
|
||||
s32 player_id;
|
||||
|
||||
static ControlMessage MessageType() { return ControlMessage::ConnectResponse; }
|
||||
static ControlMessage MessageType() { return ControlMessage::JoinResponse; }
|
||||
};
|
||||
|
||||
struct ResetMessage
|
||||
|
|
|
@ -250,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,
|
||||
|
@ -269,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
|
||||
|
@ -281,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)
|
||||
|
|
|
@ -1364,6 +1364,44 @@ bool System::BootSystem(SystemBootParameters parameters)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool System::ReinitializeSystem(ConsoleRegion region, const char* bios_path, const char* media_path, bool fast_boot)
|
||||
{
|
||||
std::optional<BIOS::Image> bios_image = FileSystem::ReadBinaryFile(bios_path);
|
||||
if (!bios_image.has_value())
|
||||
{
|
||||
Log_ErrorPrintf("Failed to read replacement BIOS at '%s'", bios_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InsertMedia(media_path))
|
||||
{
|
||||
Log_ErrorPrintf("Failed to insert media at '%s'", media_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Replace the BIOS.
|
||||
s_bios_hash = BIOS::GetImageHash(bios_image.value());
|
||||
s_bios_image_info = BIOS::GetInfoForImage(bios_image.value(), s_bios_hash);
|
||||
if (s_bios_image_info)
|
||||
Log_InfoPrintf("Replacing BIOS: %s", s_bios_image_info->description);
|
||||
else
|
||||
Log_WarningPrintf("Replacing with an unknown BIOS: %s", s_bios_hash.ToString().c_str());
|
||||
|
||||
std::memcpy(Bus::g_bios, bios_image->data(), Bus::BIOS_SIZE);
|
||||
|
||||
if (s_bios_image_info && s_bios_image_info->patch_compatible)
|
||||
BIOS::PatchBIOSEnableTTY(Bus::g_bios, Bus::BIOS_SIZE);
|
||||
|
||||
s_was_fast_booted = false;
|
||||
if (s_bios_image_info && s_bios_image_info->patch_compatible && fast_boot)
|
||||
{
|
||||
BIOS::PatchBIOSFastBoot(Bus::g_bios, Bus::BIOS_SIZE);
|
||||
s_was_fast_booted = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool System::Initialize(bool force_software_renderer)
|
||||
{
|
||||
g_ticks_per_second = ScaleTicksToOverclock(MASTER_CLOCK);
|
||||
|
@ -1942,7 +1980,7 @@ std::string System::GetMediaPathFromSaveState(const char* path)
|
|||
return ret;
|
||||
}
|
||||
|
||||
bool System::LoadStateFromStream(ByteStream* state, bool update_display)
|
||||
bool System::LoadStateFromStream(ByteStream* state, bool update_display, bool ignore_media)
|
||||
{
|
||||
Assert(IsValid());
|
||||
|
||||
|
@ -1971,90 +2009,93 @@ bool System::LoadStateFromStream(ByteStream* state, bool update_display)
|
|||
return false;
|
||||
}
|
||||
|
||||
Common::Error error;
|
||||
std::string media_filename;
|
||||
std::unique_ptr<CDImage> media;
|
||||
if (header.media_filename_length > 0)
|
||||
if (!ignore_media)
|
||||
{
|
||||
media_filename.resize(header.media_filename_length);
|
||||
if (!state->SeekAbsolute(header.offset_to_media_filename) ||
|
||||
!state->Read2(media_filename.data(), header.media_filename_length))
|
||||
Common::Error error;
|
||||
std::string media_filename;
|
||||
std::unique_ptr<CDImage> media;
|
||||
if (header.media_filename_length > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<CDImage> old_media = CDROM::RemoveMedia(false);
|
||||
if (old_media && old_media->GetFileName() == media_filename)
|
||||
{
|
||||
Log_InfoPrintf("Re-using same media '%s'", media_filename.c_str());
|
||||
media = std::move(old_media);
|
||||
}
|
||||
else
|
||||
{
|
||||
media = CDImage::Open(media_filename.c_str(), g_settings.cdrom_load_image_patches, &error);
|
||||
if (!media)
|
||||
media_filename.resize(header.media_filename_length);
|
||||
if (!state->SeekAbsolute(header.offset_to_media_filename) ||
|
||||
!state->Read2(media_filename.data(), header.media_filename_length))
|
||||
{
|
||||
if (old_media)
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<CDImage> old_media = CDROM::RemoveMedia(false);
|
||||
if (old_media && old_media->GetFileName() == media_filename)
|
||||
{
|
||||
Log_InfoPrintf("Re-using same media '%s'", media_filename.c_str());
|
||||
media = std::move(old_media);
|
||||
}
|
||||
else
|
||||
{
|
||||
media = CDImage::Open(media_filename.c_str(), g_settings.cdrom_load_image_patches, &error);
|
||||
if (!media)
|
||||
{
|
||||
Host::AddFormattedOSDMessage(
|
||||
30.0f,
|
||||
Host::TranslateString("OSDMessage", "Failed to open CD image from save state '%s': %s. Using "
|
||||
"existing image '%s', this may result in instability."),
|
||||
media_filename.c_str(), error.GetCodeAndMessage().GetCharArray(), old_media->GetFileName().c_str());
|
||||
media = std::move(old_media);
|
||||
header.media_subimage_index = media->GetCurrentSubImage();
|
||||
}
|
||||
else
|
||||
{
|
||||
Host::ReportFormattedErrorAsync(
|
||||
"Error", Host::TranslateString("System", "Failed to open CD image '%s' used by save state: %s."),
|
||||
media_filename.c_str(), error.GetCodeAndMessage().GetCharArray());
|
||||
return false;
|
||||
if (old_media)
|
||||
{
|
||||
Host::AddFormattedOSDMessage(
|
||||
30.0f,
|
||||
Host::TranslateString("OSDMessage", "Failed to open CD image from save state '%s': %s. Using "
|
||||
"existing image '%s', this may result in instability."),
|
||||
media_filename.c_str(), error.GetCodeAndMessage().GetCharArray(), old_media->GetFileName().c_str());
|
||||
media = std::move(old_media);
|
||||
header.media_subimage_index = media->GetCurrentSubImage();
|
||||
}
|
||||
else
|
||||
{
|
||||
Host::ReportFormattedErrorAsync(
|
||||
"Error", Host::TranslateString("System", "Failed to open CD image '%s' used by save state: %s."),
|
||||
media_filename.c_str(), error.GetCodeAndMessage().GetCharArray());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UpdateRunningGame(media_filename.c_str(), media.get(), false);
|
||||
UpdateRunningGame(media_filename.c_str(), media.get(), false);
|
||||
|
||||
if (media && header.version >= 51)
|
||||
{
|
||||
const u32 num_subimages = media->HasSubImages() ? media->GetSubImageCount() : 1;
|
||||
if (header.media_subimage_index >= num_subimages ||
|
||||
(media->HasSubImages() && media->GetCurrentSubImage() != header.media_subimage_index &&
|
||||
!media->SwitchSubImage(header.media_subimage_index, &error)))
|
||||
if (media && header.version >= 51)
|
||||
{
|
||||
Host::ReportFormattedErrorAsync(
|
||||
"Error",
|
||||
Host::TranslateString("System", "Failed to switch to subimage %u in CD image '%s' used by save state: %s."),
|
||||
header.media_subimage_index + 1u, media_filename.c_str(), error.GetCodeAndMessage().GetCharArray());
|
||||
return false;
|
||||
const u32 num_subimages = media->HasSubImages() ? media->GetSubImageCount() : 1;
|
||||
if (header.media_subimage_index >= num_subimages ||
|
||||
(media->HasSubImages() && media->GetCurrentSubImage() != header.media_subimage_index &&
|
||||
!media->SwitchSubImage(header.media_subimage_index, &error)))
|
||||
{
|
||||
Host::ReportFormattedErrorAsync(
|
||||
"Error",
|
||||
Host::TranslateString("System", "Failed to switch to subimage %u in CD image '%s' used by save state: %s."),
|
||||
header.media_subimage_index + 1u, media_filename.c_str(), error.GetCodeAndMessage().GetCharArray());
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_InfoPrintf("Switched to subimage %u in '%s'", header.media_subimage_index, media_filename.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
CDROM::Reset();
|
||||
if (media)
|
||||
{
|
||||
const DiscRegion region = GetRegionForImage(media.get());
|
||||
CDROM::InsertMedia(std::move(media), region);
|
||||
if (g_settings.cdrom_load_image_to_ram)
|
||||
CDROM::PrecacheMedia();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_InfoPrintf("Switched to subimage %u in '%s'", header.media_subimage_index, media_filename.c_str());
|
||||
CDROM::RemoveMedia(false);
|
||||
}
|
||||
|
||||
// ensure the correct card is loaded
|
||||
if (g_settings.HasAnyPerGameMemoryCards())
|
||||
UpdatePerGameMemoryCards();
|
||||
}
|
||||
|
||||
ClearMemorySaveStates();
|
||||
|
||||
CDROM::Reset();
|
||||
if (media)
|
||||
{
|
||||
const DiscRegion region = GetRegionForImage(media.get());
|
||||
CDROM::InsertMedia(std::move(media), region);
|
||||
if (g_settings.cdrom_load_image_to_ram)
|
||||
CDROM::PrecacheMedia();
|
||||
}
|
||||
else
|
||||
{
|
||||
CDROM::RemoveMedia(false);
|
||||
}
|
||||
|
||||
// ensure the correct card is loaded
|
||||
if (g_settings.HasAnyPerGameMemoryCards())
|
||||
UpdatePerGameMemoryCards();
|
||||
|
||||
#ifdef WITH_CHEEVOS
|
||||
// Updating game/loading settings can turn on hardcore mode. Catch this.
|
||||
if (Achievements::ChallengeModeActive())
|
||||
|
@ -2097,7 +2138,8 @@ bool System::LoadStateFromStream(ByteStream* state, bool update_display)
|
|||
}
|
||||
|
||||
bool System::SaveStateToStream(ByteStream* state, u32 screenshot_size /* = 256 */,
|
||||
u32 compression_method /* = SAVE_STATE_HEADER::COMPRESSION_TYPE_NONE*/)
|
||||
u32 compression_method /* = SAVE_STATE_HEADER::COMPRESSION_TYPE_NONE*/,
|
||||
bool ignore_media /* = false*/)
|
||||
{
|
||||
if (IsShutdown())
|
||||
return false;
|
||||
|
@ -2114,7 +2156,7 @@ bool System::SaveStateToStream(ByteStream* state, u32 screenshot_size /* = 256 *
|
|||
StringUtil::Strlcpy(header.title, s_running_game_title.c_str(), sizeof(header.title));
|
||||
StringUtil::Strlcpy(header.serial, s_running_game_serial.c_str(), sizeof(header.serial));
|
||||
|
||||
if (CDROM::HasMedia())
|
||||
if (CDROM::HasMedia() && !ignore_media)
|
||||
{
|
||||
const std::string& media_filename = CDROM::GetMediaFileName();
|
||||
header.offset_to_media_filename = static_cast<u32>(state->GetPosition());
|
||||
|
|
|
@ -223,6 +223,7 @@ void ApplySettings(bool display_osd_messages);
|
|||
bool ReloadGameSettings(bool display_osd_messages);
|
||||
|
||||
bool BootSystem(SystemBootParameters parameters);
|
||||
bool ReinitializeSystem(ConsoleRegion region, const char* bios_path, const char* media_path, bool fast_boot);
|
||||
void PauseSystem(bool paused);
|
||||
void ResetSystem();
|
||||
|
||||
|
@ -239,8 +240,9 @@ struct MemorySaveState
|
|||
};
|
||||
bool SaveMemoryState(MemorySaveState* mss);
|
||||
bool LoadMemoryState(const MemorySaveState& mss);
|
||||
bool LoadStateFromStream(ByteStream* stream, bool update_display);
|
||||
bool SaveStateToStream(ByteStream* state, u32 screenshot_size = 256, u32 compression_method = 0);
|
||||
bool LoadStateFromStream(ByteStream* stream, bool update_display, bool ignore_media = false);
|
||||
bool SaveStateToStream(ByteStream* state, u32 screenshot_size = 256, u32 compression_method = 0,
|
||||
bool ignore_media = false);
|
||||
|
||||
/// Runs the VM until the CPU execution is canceled.
|
||||
void Execute();
|
||||
|
|
|
@ -1078,14 +1078,15 @@ void EmuThread::createNetplaySession(const QString& nickname, qint32 port, qint3
|
|||
return;
|
||||
}
|
||||
|
||||
// need a valid system to make a session
|
||||
if (!System::IsValid())
|
||||
return;
|
||||
|
||||
if (!Netplay::CreateSession(nickname.toStdString(), port, max_players, password.toStdString()))
|
||||
{
|
||||
errorReported(tr("Netplay Error"), tr("Failed to create netplay session. The log may contain more information."));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Fix this junk.. for some reason, it stays sleeping...
|
||||
g_emu_thread->wakeThread();
|
||||
}
|
||||
|
||||
void EmuThread::joinNetplaySession(const QString& nickname, const QString& hostname, qint32 port,
|
||||
|
@ -1103,9 +1104,6 @@ void EmuThread::joinNetplaySession(const QString& nickname, const QString& hostn
|
|||
errorReported(tr("Netplay Error"), tr("Failed to join netplay session. The log may contain more information."));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Fix this junk.. for some reason, it stays sleeping...
|
||||
g_emu_thread->wakeThread();
|
||||
}
|
||||
|
||||
void EmuThread::runOnEmuThread(std::function<void()> callback)
|
||||
|
@ -2228,15 +2226,22 @@ int main(int argc, char* argv[])
|
|||
const bool first = (s_netplay_test == 0);
|
||||
QtHost::RunOnUIThread([first]() { g_main_window->move(QPoint(first ? 300 : 1400, 500)); });
|
||||
|
||||
const int h = first ? 1 : 2;
|
||||
const int nh = first ? 2 : 1;
|
||||
const int port_base = 31200;
|
||||
std::string remote = "127.0.0.1";
|
||||
const int port = 31200;
|
||||
const QString remote = QStringLiteral("127.0.0.1");
|
||||
std::string game = "D:\\PSX\\chd\\padtest.chd";
|
||||
Netplay::TestNetplaySession(h, port_base + h, remote, port_base + nh, 1, game);
|
||||
|
||||
// TODO: Fix this junk.. for some reason, it stays sleeping...
|
||||
g_emu_thread->wakeThread();
|
||||
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());
|
||||
}
|
||||
else
|
||||
{
|
||||
g_emu_thread->joinNetplaySession(nickname, remote, port, QString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -140,7 +140,6 @@ void ImGuiManager::DrawNetplayStats()
|
|||
const float scale = ImGuiManager::GetGlobalScale();
|
||||
const float shadow_offset = 1.0f * scale;
|
||||
const float margin = 10.0f * scale;
|
||||
const float spacing = 5.0f * scale;
|
||||
ImFont* font = ImGuiManager::GetFixedFont();
|
||||
const float position_y = ImGui::GetIO().DisplaySize.y - margin - (100.0f * scale);
|
||||
|
||||
|
|
Loading…
Reference in New Issue