978 lines
27 KiB
C++
978 lines
27 KiB
C++
/*
|
|
Copyright 2021 flyinghead
|
|
|
|
This file is part of Flycast.
|
|
|
|
Flycast is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Flycast is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "ggpo.h"
|
|
#include "hw/maple/maple_cfg.h"
|
|
#include "hw/maple/maple_devs.h"
|
|
#include "input/gamepad_device.h"
|
|
#include "input/keyboard_device.h"
|
|
#include "input/mouse.h"
|
|
#include "cfg/option.h"
|
|
#include "oslib/oslib.h"
|
|
#include <algorithm>
|
|
|
|
namespace ggpo
|
|
{
|
|
|
|
bool inRollback;
|
|
|
|
static void getLocalInput(MapleInputState inputState[4])
|
|
{
|
|
if (!config::ThreadedRendering)
|
|
os_UpdateInputState();
|
|
std::lock_guard<std::mutex> lock(relPosMutex);
|
|
for (int player = 0; player < 4; player++)
|
|
{
|
|
MapleInputState& state = inputState[player];
|
|
state.kcode = kcode[player];
|
|
state.halfAxes[PJTI_L] = lt[player];
|
|
state.halfAxes[PJTI_R] = rt[player];
|
|
state.halfAxes[PJTI_L2] = lt2[player];
|
|
state.halfAxes[PJTI_R2] = rt2[player];
|
|
state.fullAxes[PJAI_X1] = joyx[player];
|
|
state.fullAxes[PJAI_Y1] = joyy[player];
|
|
state.fullAxes[PJAI_X2] = joyrx[player];
|
|
state.fullAxes[PJAI_Y2] = joyry[player];
|
|
state.fullAxes[PJAI_X3] = joy3x[player];
|
|
state.fullAxes[PJAI_Y3] = joy3y[player];
|
|
state.mouseButtons = mo_buttons[player];
|
|
state.absPos.x = mo_x_abs[player];
|
|
state.absPos.y = mo_y_abs[player];
|
|
state.keyboard.shift = kb_shift[player];
|
|
memcpy(state.keyboard.key, kb_key[player], sizeof(kb_key[player]));
|
|
int relX = std::round(mo_x_delta[player]);
|
|
int relY = std::round(mo_y_delta[player]);
|
|
int wheel = std::round(mo_wheel_delta[player]);
|
|
state.relPos.x += relX;
|
|
state.relPos.y += relY;
|
|
state.relPos.wheel += wheel;
|
|
mo_x_delta[player] -= relX;
|
|
mo_y_delta[player] -= relY;
|
|
mo_wheel_delta[player] -= wheel;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
#ifdef USE_GGPO
|
|
#include "ggponet.h"
|
|
#include "emulator.h"
|
|
#include "ui/gui.h"
|
|
#include "ui/gui_util.h"
|
|
#include "hw/mem/mem_watch.h"
|
|
#include <string.h>
|
|
#include <chrono>
|
|
#include <thread>
|
|
#include <mutex>
|
|
#include <unordered_map>
|
|
#include <numeric>
|
|
#include "imgui.h"
|
|
#include "miniupnp.h"
|
|
#include "hw/naomi/naomi_cart.h"
|
|
|
|
//#define SYNC_TEST 1
|
|
|
|
#ifdef SYNC_TEST
|
|
#include <xxhash.h>
|
|
#endif
|
|
|
|
namespace ggpo
|
|
{
|
|
using namespace std::chrono;
|
|
|
|
constexpr int MAX_PLAYERS = 2;
|
|
constexpr int SERVER_PORT = 19713;
|
|
|
|
constexpr u32 BTN_TRIGGER_LEFT = DC_BTN_BITMAPPED_LAST << 1;
|
|
constexpr u32 BTN_TRIGGER_RIGHT = DC_BTN_BITMAPPED_LAST << 2;
|
|
|
|
#pragma pack(push, 1)
|
|
struct VerificationData
|
|
{
|
|
const int protocol = 2;
|
|
u8 gameMD5[16] { };
|
|
u8 stateMD5[16] { };
|
|
} ;
|
|
#pragma pack(pop)
|
|
|
|
static GGPOSession *ggpoSession;
|
|
static int localPlayerNum;
|
|
static GGPOPlayerHandle localPlayer;
|
|
static GGPOPlayerHandle remotePlayer;
|
|
static bool synchronized;
|
|
static std::recursive_mutex ggpoMutex;
|
|
static std::array<int, 5> msPerFrame;
|
|
static int msPerFrameIndex;
|
|
static time_point<steady_clock> lastFrameTime;
|
|
static int msPerFrameAvg;
|
|
static bool _endOfFrame;
|
|
static MiniUPnP miniupnp;
|
|
static int analogAxes;
|
|
static bool absPointerPos;
|
|
static bool keyboardGame;
|
|
static bool mouseGame;
|
|
static int inputSize;
|
|
static void (*chatCallback)(int playerNum, const std::string& msg);
|
|
|
|
struct MemPages
|
|
{
|
|
void load()
|
|
{
|
|
memwatch::ramWatcher.getPages(ram);
|
|
memwatch::vramWatcher.getPages(vram);
|
|
memwatch::aramWatcher.getPages(aram);
|
|
memwatch::elanWatcher.getPages(elanram);
|
|
}
|
|
memwatch::PageMap ram;
|
|
memwatch::PageMap vram;
|
|
memwatch::PageMap aram;
|
|
memwatch::PageMap elanram;
|
|
};
|
|
static std::unordered_map<int, MemPages> deltaStates;
|
|
static int lastSavedFrame = -1;
|
|
|
|
static int timesyncOccurred;
|
|
|
|
#pragma pack(push, 1)
|
|
struct Inputs
|
|
{
|
|
u32 kcode:20;
|
|
u32 mouseButtons:4;
|
|
u32 kbModifiers:8;
|
|
|
|
union {
|
|
struct {
|
|
u8 x;
|
|
u8 y;
|
|
} analog;
|
|
struct {
|
|
s16 x;
|
|
s16 y;
|
|
} absPos;
|
|
struct {
|
|
s16 x;
|
|
s16 y;
|
|
s16 wheel;
|
|
} relPos;
|
|
u8 keys[6];
|
|
} u;
|
|
};
|
|
static_assert(sizeof(Inputs) == 10, "wrong Inputs size");
|
|
static_assert(BTN_TRIGGER_RIGHT < (1 << 20));
|
|
|
|
struct GameEvent
|
|
{
|
|
enum : char {
|
|
Chat,
|
|
VF4Card
|
|
} type;
|
|
union {
|
|
struct {
|
|
u8 playerNum;
|
|
char message[512 - sizeof(playerNum) - sizeof(type)];
|
|
} chat;
|
|
struct {
|
|
u8 playerNum;
|
|
u8 data[128];
|
|
} card;
|
|
} u;
|
|
|
|
constexpr static int chatMessageLen(int len) { return len - sizeof(u.chat.playerNum) - sizeof(type); }
|
|
};
|
|
#pragma pack(pop)
|
|
|
|
/*
|
|
* begin_game callback - This callback has been deprecated. You must
|
|
* implement it, but should ignore the 'game' parameter.
|
|
*/
|
|
static bool begin_game(const char *)
|
|
{
|
|
DEBUG_LOG(NETWORK, "Game begin");
|
|
rend_allow_rollback();
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* on_event - Notification that something has happened. See the GGPOEventCode
|
|
* structure for more information.
|
|
*/
|
|
static bool on_event(GGPOEvent *info)
|
|
{
|
|
switch (info->code) {
|
|
case GGPO_EVENTCODE_CONNECTED_TO_PEER:
|
|
INFO_LOG(NETWORK, "Connected to peer %d", info->u.connected.player);
|
|
os_notify("Connected to peer", 2000);
|
|
break;
|
|
case GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER:
|
|
INFO_LOG(NETWORK, "Synchronizing with peer %d", info->u.synchronizing.player);
|
|
os_notify("Synchronizing with peer", 2000);
|
|
break;
|
|
case GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER:
|
|
INFO_LOG(NETWORK, "Synchronized with peer %d", info->u.synchronized.player);
|
|
os_notify("Synchronized with peer", 2000);
|
|
break;
|
|
case GGPO_EVENTCODE_RUNNING:
|
|
INFO_LOG(NETWORK, "Running");
|
|
os_notify("Running", 2000);
|
|
synchronized = true;
|
|
break;
|
|
case GGPO_EVENTCODE_DISCONNECTED_FROM_PEER:
|
|
INFO_LOG(NETWORK, "Disconnected from peer %d", info->u.disconnected.player);
|
|
throw FlycastException("Disconnected from peer");
|
|
break;
|
|
case GGPO_EVENTCODE_TIMESYNC:
|
|
INFO_LOG(NETWORK, "Timesync: %d frames ahead", info->u.timesync.frames_ahead);
|
|
timesyncOccurred += 5;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1000 / (msPerFrameAvg >= 25 ? 30 : 60)));
|
|
break;
|
|
case GGPO_EVENTCODE_CONNECTION_INTERRUPTED:
|
|
INFO_LOG(NETWORK, "Connection interrupted with player %d", info->u.connection_interrupted.player);
|
|
os_notify("Connection interrupted", 2000);
|
|
break;
|
|
case GGPO_EVENTCODE_CONNECTION_RESUMED:
|
|
INFO_LOG(NETWORK, "Connection resumed with player %d", info->u.connection_resumed.player);
|
|
os_notify("Connection resumed", 2000);
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* advance_frame - Called during a rollback. You should advance your game
|
|
* state by exactly one frame. Before each frame, call ggpo_synchronize_input
|
|
* to retrieve the inputs you should use for that frame. After each frame,
|
|
* you should call ggpo_advance_frame to notify GGPO.net that you're
|
|
* finished.
|
|
*
|
|
* The flags parameter is reserved. It can safely be ignored at this time.
|
|
*/
|
|
static bool advance_frame(int)
|
|
{
|
|
INFO_LOG(NETWORK, "advance_frame");
|
|
settings.aica.muteAudio = true;
|
|
rend_enable_renderer(false);
|
|
inRollback = true;
|
|
|
|
emu.run();
|
|
ggpo_advance_frame(ggpoSession);
|
|
|
|
settings.aica.muteAudio = false;
|
|
rend_enable_renderer(true);
|
|
inRollback = false;
|
|
_endOfFrame = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* load_game_state - GGPO.net will call this function at the beginning
|
|
* of a rollback. The buffer and len parameters contain a previously
|
|
* saved state returned from the save_game_state function. The client
|
|
* should make the current game state match the state contained in the
|
|
* buffer.
|
|
*/
|
|
static bool load_game_state(unsigned char *buffer, int len)
|
|
{
|
|
INFO_LOG(NETWORK, "load_game_state");
|
|
|
|
rend_start_rollback();
|
|
// FIXME dynarecs
|
|
Deserializer deser(buffer, len, true);
|
|
int frame;
|
|
deser >> frame;
|
|
memwatch::unprotect();
|
|
for (int f = lastSavedFrame - 1; f >= frame; f--)
|
|
{
|
|
const MemPages& pages = deltaStates[f];
|
|
for (const auto& pair : pages.ram)
|
|
memcpy(memwatch::ramWatcher.getMemPage(pair.first), &pair.second.data[0], PAGE_SIZE);
|
|
for (const auto& pair : pages.vram)
|
|
memcpy(memwatch::vramWatcher.getMemPage(pair.first), &pair.second.data[0], PAGE_SIZE);
|
|
for (const auto& pair : pages.aram)
|
|
memcpy(memwatch::aramWatcher.getMemPage(pair.first), &pair.second.data[0], PAGE_SIZE);
|
|
for (const auto& pair : pages.elanram)
|
|
memcpy(memwatch::elanWatcher.getMemPage(pair.first), &pair.second.data[0], PAGE_SIZE);
|
|
DEBUG_LOG(NETWORK, "Restored frame %d pages: %d ram, %d vram, %d eram, %d aica ram", f, (u32)pages.ram.size(),
|
|
(u32)pages.vram.size(), (u32)pages.elanram.size(), (u32)pages.aram.size());
|
|
}
|
|
dc_deserialize(deser);
|
|
if (deser.size() != (u32)len)
|
|
{
|
|
ERROR_LOG(NETWORK, "load_game_state len %d used %d", len, (int)deser.size());
|
|
die("fatal");
|
|
}
|
|
rend_allow_rollback(); // ggpo might load another state right after this one
|
|
memwatch::reset();
|
|
memwatch::protect();
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* save_game_state - The client should allocate a buffer, copy the
|
|
* entire contents of the current game state into it, and copy the
|
|
* length into the *len parameter. Optionally, the client can compute
|
|
* a checksum of the data and store it in the *checksum argument.
|
|
*/
|
|
static bool save_game_state(unsigned char **buffer, int *len, int *checksum, int frame)
|
|
{
|
|
verify(!sh4_cpu.IsCpuRunning());
|
|
lastSavedFrame = frame;
|
|
// TODO this is way too much memory
|
|
size_t allocSize = settings.platform.isNaomi() ? 20_MB : 10_MB;
|
|
*buffer = (unsigned char *)malloc(allocSize);
|
|
if (*buffer == nullptr)
|
|
{
|
|
WARN_LOG(NETWORK, "Memory alloc failed");
|
|
*len = 0;
|
|
return false;
|
|
}
|
|
Serializer ser(*buffer, allocSize, true);
|
|
ser << frame;
|
|
dc_serialize(ser);
|
|
verify(ser.size() < allocSize);
|
|
*len = ser.size();
|
|
#ifdef SYNC_TEST
|
|
*checksum = XXH32(*buffer, usedSize, 7);
|
|
#endif
|
|
memwatch::protect();
|
|
if (frame > 0)
|
|
{
|
|
#ifdef SYNC_TEST
|
|
if (deltaStates.count(frame - 1) != 0)
|
|
{
|
|
MemPages memPages;
|
|
memPages.load();
|
|
const MemPages& savedPages = deltaStates[frame - 1];
|
|
//verify(memPages.ram.size() == savedPages.ram.size());
|
|
if (memPages.ram.size() != savedPages.ram.size())
|
|
{
|
|
ERROR_LOG(NETWORK, "old ram size %d new %d", (u32)savedPages.ram.size(), (u32)memPages.ram.size());
|
|
if (memPages.ram.size() > savedPages.ram.size())
|
|
for (const auto& pair : memPages.ram)
|
|
{
|
|
if (savedPages.ram.count(pair.first) == 0)
|
|
ERROR_LOG(NETWORK, "new page @ %x", pair.first);
|
|
else
|
|
DEBUG_LOG(NETWORK, "page ok @ %x", pair.first);
|
|
}
|
|
die("fatal");
|
|
}
|
|
for (const auto& pair : memPages.ram)
|
|
{
|
|
verify(savedPages.ram.count(pair.first) == 1);
|
|
verify(memcmp(&pair.second[0], &savedPages.ram.find(pair.first)->second[0], PAGE_SIZE) == 0);
|
|
}
|
|
verify(memPages.vram.size() == savedPages.vram.size());
|
|
for (const auto& pair : memPages.vram)
|
|
{
|
|
verify(savedPages.vram.count(pair.first) == 1);
|
|
verify(memcmp(&pair.second[0], &savedPages.vram.find(pair.first)->second[0], PAGE_SIZE) == 0);
|
|
}
|
|
//verify(memPages.aram.size() == savedPages.aram.size());
|
|
if (memPages.aram.size() != savedPages.aram.size())
|
|
{
|
|
ERROR_LOG(NETWORK, "old aram size %d new %d", (u32)savedPages.aram.size(), (u32)memPages.aram.size());
|
|
die("fatal");
|
|
}
|
|
for (const auto& pair : memPages.aram)
|
|
{
|
|
verify(savedPages.aram.count(pair.first) == 1);
|
|
verify(memcmp(&pair.second[0], &savedPages.aram.find(pair.first)->second[0], PAGE_SIZE) == 0);
|
|
}
|
|
}
|
|
#endif
|
|
// Save the delta to frame-1
|
|
deltaStates[frame - 1].load();
|
|
DEBUG_LOG(NETWORK, "Saved frame %d pages: %d ram, %d vram, %d eram, %d aica ram", frame - 1, (u32)deltaStates[frame - 1].ram.size(),
|
|
(u32)deltaStates[frame - 1].vram.size(), (u32)deltaStates[frame - 1].elanram.size(), (u32)deltaStates[frame - 1].aram.size());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* log_game_state - Used in diagnostic testing. The client should use
|
|
* the ggpo_log function to write the contents of the specified save
|
|
* state in a human readible form.
|
|
*/
|
|
static bool log_game_state(char *filename, unsigned char *buffer, int len)
|
|
{
|
|
#ifdef SYNC_TEST
|
|
static int lastLoggedFrame = -1;
|
|
static u8 *lastState;
|
|
int frame = *(u32 *)buffer;
|
|
DEBUG_LOG(NETWORK, "log_game_state frame %d len %d", frame, len);
|
|
if (lastLoggedFrame == frame) {
|
|
for (int i = 0; i < len; i++)
|
|
if (buffer[i] != lastState[i])
|
|
{
|
|
WARN_LOG(NETWORK, "States for frame %d differ at offset %d: now %x prev %x", frame, i, *(u32 *)&buffer[i & ~3], *(u32 *)&lastState[i & ~3]);
|
|
break;
|
|
}
|
|
}
|
|
lastState = buffer;
|
|
lastLoggedFrame = frame;
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* free_buffer - Frees a game state allocated in save_game_state. You
|
|
* should deallocate the memory contained in the buffer.
|
|
*/
|
|
static void free_buffer(void *buffer)
|
|
{
|
|
if (buffer != nullptr)
|
|
{
|
|
Deserializer deser(buffer, 1_MB, true);
|
|
int frame;
|
|
deser >> frame;
|
|
deltaStates.erase(frame);
|
|
free(buffer);
|
|
}
|
|
}
|
|
|
|
static void on_message(u8 *msg, int len)
|
|
{
|
|
if (len == 0)
|
|
return;
|
|
GameEvent *event = (GameEvent *)msg;
|
|
switch (event->type)
|
|
{
|
|
case GameEvent::Chat:
|
|
if (chatCallback != nullptr && GameEvent::chatMessageLen(len) > 0)
|
|
chatCallback(event->u.chat.playerNum, std::string(event->u.chat.message, GameEvent::chatMessageLen(len)));
|
|
break;
|
|
|
|
case GameEvent::VF4Card:
|
|
setRfidCardData(event->u.card.playerNum, event->u.card.data);
|
|
break;
|
|
|
|
default:
|
|
WARN_LOG(NETWORK, "Unknown app message type %d", event->type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void startSession(int localPort, int localPlayerNum)
|
|
{
|
|
GGPOSessionCallbacks cb{};
|
|
cb.begin_game = begin_game;
|
|
cb.advance_frame = advance_frame;
|
|
cb.load_game_state = load_game_state;
|
|
cb.save_game_state = save_game_state;
|
|
cb.free_buffer = free_buffer;
|
|
cb.on_event = on_event;
|
|
cb.log_game_state = log_game_state;
|
|
cb.on_message = on_message;
|
|
|
|
#ifdef SYNC_TEST
|
|
GGPOErrorCode result = ggpo_start_synctest(&ggpoSession, &cb, settings.content.gameId.c_str(), MAX_PLAYERS, sizeof(kcode[0]), 1);
|
|
if (result != GGPO_OK)
|
|
{
|
|
WARN_LOG(NETWORK, "GGPO start sync session failed: %d", result);
|
|
ggpoSession = nullptr;
|
|
throw FlycastException("GGPO start sync session failed");
|
|
}
|
|
ggpo_idle(ggpoSession, 0);
|
|
ggpo::localPlayerNum = localPlayerNum;
|
|
GGPOPlayer player{ sizeof(GGPOPlayer), GGPO_PLAYERTYPE_LOCAL, localPlayerNum + 1 };
|
|
result = ggpo_add_player(ggpoSession, &player, &localPlayer);
|
|
player.player_num = (1 - localPlayerNum) + 1;
|
|
result = ggpo_add_player(ggpoSession, &player, &remotePlayer);
|
|
synchronized = true;
|
|
analogAxes = 0;
|
|
NOTICE_LOG(NETWORK, "GGPO synctest session started");
|
|
#else
|
|
if (settings.platform.isConsole())
|
|
analogAxes = config::GGPOAnalogAxes;
|
|
else
|
|
{
|
|
analogAxes = 0;
|
|
absPointerPos = false;
|
|
keyboardGame = false;
|
|
mouseGame = false;
|
|
if (settings.input.lightgunGame)
|
|
absPointerPos = true;
|
|
else if (settings.input.keyboardGame)
|
|
keyboardGame = true;
|
|
else if (settings.input.mouseGame)
|
|
mouseGame = true;
|
|
else if (NaomiGameInputs != nullptr)
|
|
{
|
|
for (const auto& axis : NaomiGameInputs->axes)
|
|
{
|
|
if (axis.name == nullptr)
|
|
break;
|
|
if (axis.type == Full)
|
|
analogAxes = std::max(analogAxes, (int)axis.axis + 1);
|
|
}
|
|
}
|
|
NOTICE_LOG(NETWORK, "GGPO: Using %d full analog axes", analogAxes);
|
|
}
|
|
inputSize = sizeof(kcode[0]) + analogAxes + (int)absPointerPos * sizeof(Inputs::u.absPos)
|
|
+ (int)keyboardGame * sizeof(Inputs::u.keys) + (int)mouseGame * sizeof(Inputs::u.relPos);
|
|
|
|
VerificationData verif;
|
|
MD5Sum().add(settings.network.md5.bios)
|
|
.add(settings.network.md5.game)
|
|
.getDigest(verif.gameMD5);
|
|
auto& digest = settings.network.md5.savestate;
|
|
if (std::find_if(std::begin(digest), std::end(digest), [](u8 b) { return b != 0; }) != std::end(digest))
|
|
memcpy(verif.stateMD5, digest, sizeof(digest));
|
|
else
|
|
{
|
|
MD5Sum().add(settings.network.md5.nvmem)
|
|
.add(settings.network.md5.nvmem2)
|
|
.add(settings.network.md5.eeprom)
|
|
.add(settings.network.md5.vmu)
|
|
.getDigest(verif.stateMD5);
|
|
}
|
|
|
|
GGPOErrorCode result = ggpo_start_session(&ggpoSession, &cb, settings.content.gameId.c_str(), MAX_PLAYERS, inputSize, localPort,
|
|
&verif, sizeof(verif));
|
|
if (result != GGPO_OK)
|
|
{
|
|
WARN_LOG(NETWORK, "GGPO start session failed: %d", result);
|
|
ggpoSession = nullptr;
|
|
throw FlycastException("GGPO network initialization failed");
|
|
}
|
|
|
|
// automatically disconnect clients after 3000 ms and start our count-down timer
|
|
// for disconnects after 1000 ms. To completely disable disconnects, simply use
|
|
// a value of 0 for ggpo_set_disconnect_timeout.
|
|
ggpo_set_disconnect_timeout(ggpoSession, 3000);
|
|
ggpo_set_disconnect_notify_start(ggpoSession, 1000);
|
|
|
|
ggpo::localPlayerNum = localPlayerNum;
|
|
GGPOPlayer player{ sizeof(GGPOPlayer), GGPO_PLAYERTYPE_LOCAL, localPlayerNum + 1 };
|
|
result = ggpo_add_player(ggpoSession, &player, &localPlayer);
|
|
if (result != GGPO_OK)
|
|
{
|
|
WARN_LOG(NETWORK, "GGPO cannot add local player: %d", result);
|
|
stopSession();
|
|
throw FlycastException("GGPO cannot add local player");
|
|
}
|
|
ggpo_set_frame_delay(ggpoSession, localPlayer, config::GGPODelay.get());
|
|
|
|
size_t colon = config::NetworkServer.get().find(':');
|
|
std::string peerIp = config::NetworkServer.get().substr(0, colon);
|
|
if (peerIp.empty())
|
|
peerIp = "127.0.0.1";
|
|
u32 peerPort;
|
|
if (colon == std::string::npos)
|
|
{
|
|
if (peerIp == "127.0.0.1")
|
|
peerPort = localPort ^ 1;
|
|
else
|
|
peerPort = SERVER_PORT;
|
|
}
|
|
else
|
|
{
|
|
peerPort = atoi(config::NetworkServer.get().substr(colon + 1).c_str());
|
|
}
|
|
player.type = GGPO_PLAYERTYPE_REMOTE;
|
|
strcpy(player.u.remote.ip_address, peerIp.c_str());
|
|
player.u.remote.port = peerPort;
|
|
player.player_num = (1 - localPlayerNum) + 1;
|
|
result = ggpo_add_player(ggpoSession, &player, &remotePlayer);
|
|
if (result != GGPO_OK)
|
|
{
|
|
WARN_LOG(NETWORK, "GGPO cannot add remote player: %d", result);
|
|
stopSession();
|
|
throw FlycastException("GGPO cannot add remote player");
|
|
}
|
|
DEBUG_LOG(NETWORK, "GGPO session started");
|
|
#endif
|
|
}
|
|
|
|
void stopSession()
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(ggpoMutex);
|
|
if (ggpoSession == nullptr)
|
|
return;
|
|
ggpo_close_session(ggpoSession);
|
|
ggpoSession = nullptr;
|
|
miniupnp.Term();
|
|
emu.setNetworkState(false);
|
|
memwatch::unprotect();
|
|
memwatch::reset();
|
|
}
|
|
|
|
void getInput(MapleInputState inputState[4])
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(ggpoMutex);
|
|
if (ggpoSession == nullptr)
|
|
{
|
|
getLocalInput(inputState);
|
|
return;
|
|
}
|
|
for (int player = 0; player < 4; player++)
|
|
inputState[player] = {};
|
|
|
|
std::vector<u8> inputData(inputSize * MAX_PLAYERS);
|
|
// should not call any callback
|
|
GGPOErrorCode error = ggpo_synchronize_input(ggpoSession, (void *)&inputData[0], inputData.size(), nullptr);
|
|
if (error != GGPO_OK)
|
|
{
|
|
stopSession();
|
|
throw FlycastException("GGPO error");
|
|
}
|
|
|
|
for (int player = 0; player < MAX_PLAYERS; player++)
|
|
{
|
|
MapleInputState& state = inputState[player];
|
|
const Inputs *inputs = (Inputs *)&inputData[player * inputSize];
|
|
state.kcode = ~inputs->kcode;
|
|
if (analogAxes > 0)
|
|
{
|
|
state.fullAxes[PJAI_X1] = inputs->u.analog.x << 8;
|
|
if (analogAxes >= 2)
|
|
state.fullAxes[PJAI_Y1] = inputs->u.analog.y << 8;
|
|
}
|
|
else if (absPointerPos)
|
|
{
|
|
state.absPos.x = inputs->u.absPos.x;
|
|
state.absPos.y = inputs->u.absPos.y;
|
|
}
|
|
else if (keyboardGame)
|
|
{
|
|
memcpy(state.keyboard.key, inputs->u.keys, sizeof(state.keyboard.key));
|
|
state.keyboard.shift = inputs->kbModifiers;
|
|
}
|
|
else if (mouseGame)
|
|
{
|
|
state.relPos.x = inputs->u.relPos.x;
|
|
state.relPos.y = inputs->u.relPos.y;
|
|
state.relPos.wheel = inputs->u.relPos.wheel;
|
|
state.mouseButtons = ~inputs->mouseButtons;
|
|
}
|
|
state.halfAxes[PJTI_R] = (state.kcode & BTN_TRIGGER_RIGHT) == 0 ? 0xffff : 0;
|
|
state.halfAxes[PJTI_L] = (state.kcode & BTN_TRIGGER_LEFT) == 0 ? 0xffff : 0;
|
|
}
|
|
}
|
|
|
|
bool nextFrame()
|
|
{
|
|
if (!_endOfFrame)
|
|
return false;
|
|
_endOfFrame = false;
|
|
if (inRollback)
|
|
return true;
|
|
auto now = std::chrono::steady_clock::now();
|
|
if (lastFrameTime != time_point<steady_clock>())
|
|
{
|
|
msPerFrame[msPerFrameIndex++] = duration_cast<milliseconds>(now - lastFrameTime).count();
|
|
if (msPerFrameIndex >= (int)msPerFrame.size())
|
|
msPerFrameIndex = 0;
|
|
msPerFrameAvg = std::accumulate(msPerFrame.begin(), msPerFrame.end(), 0) / msPerFrame.size();
|
|
}
|
|
lastFrameTime = now;
|
|
|
|
std::lock_guard<std::recursive_mutex> lock(ggpoMutex);
|
|
if (ggpoSession == nullptr)
|
|
return false;
|
|
// will call save_game_state
|
|
GGPOErrorCode error = ggpo_advance_frame(ggpoSession);
|
|
|
|
// may rollback
|
|
if (error == GGPO_OK)
|
|
error = ggpo_idle(ggpoSession, 0);
|
|
if (error != GGPO_OK)
|
|
{
|
|
stopSession();
|
|
if (error == GGPO_ERRORCODE_INPUT_SIZE_DIFF)
|
|
throw FlycastException("GGPO analog settings are different from peer");
|
|
else
|
|
throw FlycastException("GGPO error");
|
|
}
|
|
|
|
// may call save_game_state
|
|
do {
|
|
if (!config::ThreadedRendering)
|
|
os_UpdateInputState();
|
|
Inputs inputs;
|
|
inputs.kcode = ~kcode[0];
|
|
if (rt[0] >= 0x4000)
|
|
inputs.kcode |= BTN_TRIGGER_RIGHT;
|
|
else
|
|
inputs.kcode &= ~BTN_TRIGGER_RIGHT;
|
|
if (lt[0] >= 0x4000)
|
|
inputs.kcode |= BTN_TRIGGER_LEFT;
|
|
else
|
|
inputs.kcode &= ~BTN_TRIGGER_LEFT;
|
|
inputs.mouseButtons = 0;
|
|
inputs.kbModifiers = 0;
|
|
if (analogAxes > 0)
|
|
{
|
|
inputs.u.analog.x = joyx[0] >> 8;
|
|
if (analogAxes >= 2)
|
|
inputs.u.analog.y = joyy[0] >> 8;
|
|
}
|
|
else if (absPointerPos)
|
|
{
|
|
inputs.u.absPos.x = mo_x_abs[0];
|
|
inputs.u.absPos.y = mo_y_abs[0];
|
|
}
|
|
else if (keyboardGame)
|
|
{
|
|
inputs.kbModifiers = kb_shift[0];
|
|
memcpy(inputs.u.keys, kb_key[0], sizeof(kb_key[0]));
|
|
}
|
|
else if (mouseGame)
|
|
{
|
|
std::lock_guard<std::mutex> lock(relPosMutex);
|
|
inputs.mouseButtons = ~mo_buttons[0];
|
|
inputs.u.relPos.x = std::round(mo_x_delta[0]);
|
|
inputs.u.relPos.y = std::round(mo_y_delta[0]);
|
|
inputs.u.relPos.wheel = std::round(mo_wheel_delta[0]);
|
|
mo_x_delta[0] -= inputs.u.relPos.x;
|
|
mo_y_delta[0] -= inputs.u.relPos.y;
|
|
mo_wheel_delta[0] -= inputs.u.relPos.wheel;
|
|
}
|
|
error = ggpo_add_local_input(ggpoSession, localPlayer, &inputs, inputSize);
|
|
if (error == GGPO_OK)
|
|
break;
|
|
if (error != GGPO_ERRORCODE_PREDICTION_THRESHOLD)
|
|
{
|
|
WARN_LOG(NETWORK, "ggpo_add_local_input failed %d", error);
|
|
stopSession();
|
|
throw FlycastException("GGPO error");
|
|
}
|
|
DEBUG_LOG(NETWORK, "ggpo_add_local_input prediction barrier reached");
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
error = ggpo_idle(ggpoSession, 0);
|
|
if (error != GGPO_OK)
|
|
{
|
|
stopSession();
|
|
throw FlycastException("GGPO error");
|
|
}
|
|
} while (active());
|
|
#ifdef SYNC_TEST
|
|
u32 input = ~kcode[1 - localPlayerNum];
|
|
GGPOErrorCode result = ggpo_add_local_input(ggpoSession, remotePlayer, &input, sizeof(input));
|
|
if (result != GGPO_OK)
|
|
WARN_LOG(NETWORK, "ggpo_add_local_input(2) failed %d", result);
|
|
#endif
|
|
return active();
|
|
}
|
|
|
|
bool active()
|
|
{
|
|
return ggpoSession != nullptr;
|
|
}
|
|
|
|
std::future<bool> startNetwork()
|
|
{
|
|
synchronized = false;
|
|
return std::async(std::launch::async, []{
|
|
{
|
|
ThreadName _("GGPO-start");
|
|
std::lock_guard<std::recursive_mutex> lock(ggpoMutex);
|
|
#ifdef SYNC_TEST
|
|
startSession(0, 0);
|
|
#else
|
|
if (config::EnableUPnP)
|
|
{
|
|
miniupnp.Init();
|
|
miniupnp.AddPortMapping(SERVER_PORT, false);
|
|
}
|
|
|
|
try {
|
|
if (config::ActAsServer)
|
|
startSession(SERVER_PORT, 0);
|
|
else
|
|
// Use SERVER_PORT-1 as local port if connecting to ourselves
|
|
startSession(config::NetworkServer.get().empty() || config::NetworkServer.get() == "127.0.0.1" ? SERVER_PORT - 1 : SERVER_PORT, 1);
|
|
} catch (...) {
|
|
miniupnp.Term();
|
|
throw;
|
|
}
|
|
#endif
|
|
}
|
|
while (!synchronized && active())
|
|
{
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(ggpoMutex);
|
|
if (ggpoSession == nullptr)
|
|
break;
|
|
GGPOErrorCode result = ggpo_idle(ggpoSession, 0);
|
|
if (result == GGPO_ERRORCODE_VERIFICATION_ERROR)
|
|
throw FlycastException("Peer verification failed");
|
|
else if (result != GGPO_OK)
|
|
{
|
|
WARN_LOG(NETWORK, "ggpo_idle failed %d", result);
|
|
throw FlycastException("GGPO error");
|
|
}
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
}
|
|
#ifdef SYNC_TEST
|
|
// save initial state (frame 0)
|
|
if (active())
|
|
{
|
|
MapleInputState state[4];
|
|
getInput(state);
|
|
}
|
|
#endif
|
|
if (active() && (settings.content.gameId == "VIRTUA FIGHTER 4 JAPAN"
|
|
|| settings.content.gameId == "VF4 EVOLUTION JAPAN"
|
|
|| settings.content.gameId == "VF4 FINAL TUNED JAPAN"))
|
|
{
|
|
// Send the local P1 card
|
|
const u8 *cardData = getRfidCardData(0);
|
|
if (cardData != nullptr)
|
|
{
|
|
GameEvent event;
|
|
event.type = GameEvent::VF4Card;
|
|
event.u.card.playerNum = config::ActAsServer ? 0 : 1;
|
|
memcpy(event.u.card.data, cardData, sizeof(event.u.card.data));
|
|
ggpo_send_message(ggpoSession, &event, sizeof(event.type) + sizeof(event.u.card), true);
|
|
}
|
|
}
|
|
emu.setNetworkState(active());
|
|
return active();
|
|
});
|
|
}
|
|
|
|
void displayStats()
|
|
{
|
|
if (!active())
|
|
return;
|
|
GGPONetworkStats stats;
|
|
ggpo_get_network_stats(ggpoSession, remotePlayer, &stats);
|
|
|
|
ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0);
|
|
ImguiStyleVar _1(ImGuiStyleVar_WindowBorderSize, 0);
|
|
ImGui::SetNextWindowPos(ImVec2(10, 10));
|
|
ImGui::SetNextWindowSize(ScaledVec2(95, 0));
|
|
ImGui::SetNextWindowBgAlpha(0.7f);
|
|
ImGui::Begin("##ggpostats", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs);
|
|
ImguiStyleColor _2(ImGuiCol_PlotHistogram, ImVec4(0.557f, 0.268f, 0.965f, 1.f));
|
|
|
|
// Send Queue
|
|
ImGui::Text("Send Q");
|
|
ImGui::ProgressBar(stats.network.send_queue_len / 10.f, ImVec2(-1, uiScaled(10.f)), "");
|
|
|
|
// Frame Delay
|
|
ImGui::Text("Delay");
|
|
std::string delay = std::to_string(config::GGPODelay.get());
|
|
ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(delay.c_str()).x);
|
|
ImGui::Text("%s", delay.c_str());
|
|
|
|
// Ping
|
|
ImGui::Text("Ping");
|
|
std::string ping = std::to_string(stats.network.ping);
|
|
ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(ping.c_str()).x);
|
|
ImGui::Text("%s", ping.c_str());
|
|
|
|
// Predicted Frames
|
|
if (stats.sync.predicted_frames >= 7)
|
|
// red
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(1, 0, 0, 1));
|
|
else if (stats.sync.predicted_frames >= 5)
|
|
// yellow
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(.9f, .9f, .1f, 1));
|
|
ImGui::Text("Predicted");
|
|
ImGui::ProgressBar(stats.sync.predicted_frames / 7.f, ImVec2(-1, uiScaled(10.f)), "");
|
|
if (stats.sync.predicted_frames >= 5)
|
|
ImGui::PopStyleColor();
|
|
|
|
// Frames behind
|
|
int timesync = timesyncOccurred;
|
|
if (timesync > 0)
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0, 0, 1));
|
|
ImGui::Text("Behind");
|
|
ImGui::ProgressBar(0.5f + stats.timesync.local_frames_behind / 16.f, ImVec2(-1, uiScaled(10.f)), "");
|
|
if (timesync > 0)
|
|
{
|
|
ImGui::PopStyleColor();
|
|
timesyncOccurred--;
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
void endOfFrame()
|
|
{
|
|
if (active())
|
|
{
|
|
_endOfFrame = true;
|
|
sh4_cpu.Stop();
|
|
}
|
|
}
|
|
|
|
void sendChatMessage(int playerNum, const std::string& msg) {
|
|
if (!active())
|
|
return;
|
|
GameEvent event;
|
|
event.type = GameEvent::Chat;
|
|
event.u.chat.playerNum = playerNum;
|
|
size_t msgLen = std::min(msg.length(), sizeof(event.u.chat.message));
|
|
memcpy(event.u.chat.message, msg.c_str(), msgLen);
|
|
ggpo_send_message(ggpoSession, &event, sizeof(GameEvent) - sizeof(event.u.chat.message) + msgLen, true);
|
|
}
|
|
|
|
void receiveChatMessages(void (*callback)(int playerNum, const std::string& msg))
|
|
{
|
|
chatCallback = callback;
|
|
}
|
|
|
|
}
|
|
|
|
#else // LIBRETRO
|
|
|
|
namespace ggpo
|
|
{
|
|
|
|
void stopSession() {
|
|
}
|
|
|
|
void getInput(MapleInputState inputState[4])
|
|
{
|
|
getLocalInput(inputState);
|
|
}
|
|
|
|
bool nextFrame() {
|
|
return true;
|
|
}
|
|
|
|
bool active() {
|
|
return false;
|
|
}
|
|
|
|
std::future<bool> startNetwork() {
|
|
return std::async(std::launch::deferred, []{ return false; });;
|
|
}
|
|
|
|
void displayStats() {
|
|
}
|
|
|
|
void endOfFrame() {
|
|
}
|
|
|
|
void sendChatMessage(int playerNum, const std::string& msg) {
|
|
}
|
|
|
|
void receiveChatMessages(void (*callback)(int playerNum, const std::string& msg)) {
|
|
}
|
|
|
|
}
|
|
#endif
|