From 99a801752bcebc38365ac191017844e93b5e255e Mon Sep 17 00:00:00 2001 From: r2dliu Date: Sat, 28 Nov 2020 20:31:30 -0500 Subject: [PATCH] pull in spectate class --- Source/Core/Common/Base64.hpp | 124 +++++++ Source/Core/Common/Version.cpp | 2 +- Source/Core/Core/CMakeLists.txt | 4 +- Source/Core/Core/ConfigManager.h | 3 + Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp | 20 +- Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h | 3 + Source/Core/Core/Slippi/SlippiSpectate.cpp | 367 +++++++++++++++++++ Source/Core/Core/Slippi/SlippiSpectate.h | 106 ++++++ 8 files changed, 625 insertions(+), 4 deletions(-) create mode 100644 Source/Core/Common/Base64.hpp create mode 100644 Source/Core/Core/Slippi/SlippiSpectate.cpp create mode 100644 Source/Core/Core/Slippi/SlippiSpectate.h diff --git a/Source/Core/Common/Base64.hpp b/Source/Core/Common/Base64.hpp new file mode 100644 index 0000000000..fde402364c --- /dev/null +++ b/Source/Core/Common/Base64.hpp @@ -0,0 +1,124 @@ +#ifndef _MACARON_BASE64_H_ +#define _MACARON_BASE64_H_ + +/** + * The MIT License (MIT) + * Copyright (c) 2016 tomykaira + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include + +namespace base64 { + + class Base64 { + public: + + static std::string Encode(const std::string data) { + static constexpr char sEncodingTable[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', '+', '/' + }; + + size_t in_len = data.size(); + size_t out_len = 4 * ((in_len + 2) / 3); + std::string ret(out_len, '\0'); + size_t i; + char* p = const_cast(ret.c_str()); + + for (i = 0; i < in_len - 2; i += 3) { + *p++ = sEncodingTable[(data[i] >> 2) & 0x3F]; + *p++ = sEncodingTable[((data[i] & 0x3) << 4) | ((int)(data[i + 1] & 0xF0) >> 4)]; + *p++ = sEncodingTable[((data[i + 1] & 0xF) << 2) | ((int)(data[i + 2] & 0xC0) >> 6)]; + *p++ = sEncodingTable[data[i + 2] & 0x3F]; + } + if (i < in_len) { + *p++ = sEncodingTable[(data[i] >> 2) & 0x3F]; + if (i == (in_len - 1)) { + *p++ = sEncodingTable[((data[i] & 0x3) << 4)]; + *p++ = '='; + } + else { + *p++ = sEncodingTable[((data[i] & 0x3) << 4) | ((int)(data[i + 1] & 0xF0) >> 4)]; + *p++ = sEncodingTable[((data[i + 1] & 0xF) << 2)]; + } + *p++ = '='; + } + + return ret; + } + + static std::string Decode(const std::string& input, std::string& out) { + static constexpr unsigned char kDecodingTable[] = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, + 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 64, + 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 + }; + + size_t in_len = input.size(); + if (in_len % 4 != 0) return "Input data size is not a multiple of 4"; + + size_t out_len = in_len / 4 * 3; + if (input[in_len - 1] == '=') out_len--; + if (input[in_len - 2] == '=') out_len--; + + out.resize(out_len); + + for (size_t i = 0, j = 0; i < in_len;) { + uint32_t a = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast(input[i++])]; + uint32_t b = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast(input[i++])]; + uint32_t c = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast(input[i++])]; + uint32_t d = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast(input[i++])]; + + uint32_t triple = (a << 3 * 6) + (b << 2 * 6) + (c << 1 * 6) + (d << 0 * 6); + + if (j < out_len) out[j++] = (triple >> 2 * 8) & 0xFF; + if (j < out_len) out[j++] = (triple >> 1 * 8) & 0xFF; + if (j < out_len) out[j++] = (triple >> 0 * 8) & 0xFF; + } + + return ""; + } + + }; + +} + +#endif /* _MACARON_BASE64_H_ */ diff --git a/Source/Core/Common/Version.cpp b/Source/Core/Common/Version.cpp index 88f65f135c..11ea98bdb1 100644 --- a/Source/Core/Common/Version.cpp +++ b/Source/Core/Common/Version.cpp @@ -18,7 +18,7 @@ namespace Common #define BUILD_TYPE_STR "" #endif -#define SLIPPI_REV_STR "2.2.1" +#define SLIPPI_REV_STR "2.2.3" const std::string scm_slippi_semver_str = SLIPPI_REV_STR; diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 41a3622cb3..091c5bd923 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -479,8 +479,8 @@ add_library(core Slippi/SlippiReplayComm.h Slippi/SlippiSavestate.cpp Slippi/SlippiSavestate.h - - + Slippi/SlippiSpectate.cpp + Slippi/SlippiSpectate.h Slippi/SlippiUser.cpp Slippi/SlippiUser.h ) diff --git a/Source/Core/Core/ConfigManager.h b/Source/Core/Core/ConfigManager.h index e386cb741f..a4fb5cce1c 100644 --- a/Source/Core/Core/ConfigManager.h +++ b/Source/Core/Core/ConfigManager.h @@ -147,6 +147,9 @@ struct SConfig bool m_is_mios = false; // Slippi + // enable Slippi Networking output + bool m_enableSpectator; + int m_spectator_local_port; std::string m_strSlippiInput; int m_slippiOnlineDelay = 2; bool m_slippiEnableSeek = true; diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp index 773137d44b..7bdc563884 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp +++ b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.cpp @@ -96,7 +96,6 @@ void appendHalfToBuffer(std::vector* buf, u16 word) CEXISlippi::CEXISlippi() { INFO_LOG(SLIPPI, "EXI SLIPPI Constructor called."); - user = std::make_unique(); g_playbackStatus = std::make_unique(); matchmaking = std::make_unique(user.get()); @@ -198,6 +197,9 @@ CEXISlippi::~CEXISlippi() m_fileWriteThread.join(); } + SlippiSpectateServer::getInstance().write(&empty[0], 0); + SlippiSpectateServer::getInstance().endGame(); + localSelections.Reset(); g_playbackStatus->resetPlayback(); @@ -2125,6 +2127,14 @@ void CEXISlippi::DMAWrite(u32 _uAddr, u32 _uSize) configureCommands(&memPtr[1], receiveCommandsLen); writeToFileAsync(&memPtr[0], receiveCommandsLen + 1, "create"); bufLoc += receiveCommandsLen + 1; + // g_needInputForFrame = true; + SlippiSpectateServer::getInstance().startGame(); + SlippiSpectateServer::getInstance().write(&memPtr[0], receiveCommandsLen + 1); + } + + if (byte == CMD_MENU_FRAME) + { + SlippiSpectateServer::getInstance().write(&memPtr[0], _uSize); } INFO_LOG(EXPANSIONINTERFACE, "EXI SLIPPI DMAWrite: addr: 0x%08x size: %d, bufLoc:[%02x %02x %02x %02x %02x]", @@ -2146,6 +2156,8 @@ void CEXISlippi::DMAWrite(u32 _uAddr, u32 _uSize) { case CMD_RECEIVE_GAME_END: writeToFileAsync(&memPtr[bufLoc], payloadLen + 1, "close"); + SlippiSpectateServer::getInstance().write(&memPtr[bufLoc], payloadLen + 1); + SlippiSpectateServer::getInstance().endGame(); break; case CMD_PREPARE_REPLAY: // log.open("log.txt"); @@ -2154,6 +2166,11 @@ void CEXISlippi::DMAWrite(u32 _uAddr, u32 _uSize) case CMD_READ_FRAME: prepareFrameData(&memPtr[bufLoc + 1]); break; + case CMD_FRAME_BOOKEND: + // g_needInputForFrame = true; + writeToFileAsync(&memPtr[bufLoc], payloadLen + 1, ""); + SlippiSpectateServer::getInstance().write(&memPtr[bufLoc], payloadLen + 1); + break; case CMD_IS_STOCK_STEAL: prepareIsStockSteal(&memPtr[bufLoc + 1]); break; @@ -2208,6 +2225,7 @@ void CEXISlippi::DMAWrite(u32 _uAddr, u32 _uSize) break; default: writeToFileAsync(&memPtr[bufLoc], payloadLen + 1, ""); + SlippiSpectateServer::getInstance().write(&memPtr[bufLoc], payloadLen + 1); break; } diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h index b0681e6bc8..9726bfdbe4 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h +++ b/Source/Core/Core/HW/EXI/EXI_DeviceSlippi.h @@ -16,6 +16,7 @@ #include "Core/Slippi/SlippiPlayback.h" #include "Core/Slippi/SlippiReplayComm.h" #include "Core/Slippi/SlippiSavestate.h" +#include "Core/Slippi/SlippiSpectate.h" #include "Core/Slippi/SlippiUser.h" #define ROLLBACK_MAX_FRAMES 7 @@ -46,6 +47,8 @@ namespace ExpansionInterface CMD_RECEIVE_GAME_INFO = 0x36, CMD_RECEIVE_POST_FRAME_UPDATE = 0x38, CMD_RECEIVE_GAME_END = 0x39, + CMD_FRAME_BOOKEND = 0x3C, + CMD_MENU_FRAME = 0x3E, // Playback CMD_PREPARE_REPLAY = 0x75, diff --git a/Source/Core/Core/Slippi/SlippiSpectate.cpp b/Source/Core/Core/Slippi/SlippiSpectate.cpp new file mode 100644 index 0000000000..d13c8dc8d5 --- /dev/null +++ b/Source/Core/Core/Slippi/SlippiSpectate.cpp @@ -0,0 +1,367 @@ +#include "SlippiSpectate.h" +#include "Common/Base64.hpp" +#include "Common/CommonTypes.h" +#include "Common/Logging/Log.h" +#include "Common/Version.h" +#include + +// Networking +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +inline bool isSpectatorEnabled() +{ + return SConfig::GetInstance().m_enableSpectator; +} + +// CALLED FROM DOLPHIN MAIN THREAD +SlippiSpectateServer& SlippiSpectateServer::getInstance() +{ + static SlippiSpectateServer instance; + return instance; +} + +// CALLED FROM DOLPHIN MAIN THREAD +void SlippiSpectateServer::write(u8 *payload, u32 length) +{ + if (isSpectatorEnabled()) { + std::string str_payload((char*)payload, length); + m_event_queue.Push(str_payload); + } +} + +// CALLED FROM DOLPHIN MAIN THREAD +void SlippiSpectateServer::startGame() +{ + if (isSpectatorEnabled()) + { + m_event_queue.Push("START_GAME"); + } +} + +// CALLED FROM DOLPHIN MAIN THREAD +void SlippiSpectateServer::endGame() +{ + if (isSpectatorEnabled()) + { + m_event_queue.Push("END_GAME"); + } +} + +// CALLED FROM SERVER THREAD +void SlippiSpectateServer::writeEvents(u16 peer_id) +{ + // Send menu events + if (!m_in_game && (m_sockets[peer_id]->m_menu_cursor != m_menu_cursor)) + { + ENetPacket *packet = enet_packet_create(m_menu_event.data(), m_menu_event.length(), ENET_PACKET_FLAG_RELIABLE); + // Batch for sending + enet_peer_send(m_sockets[peer_id]->m_peer, 0, packet); + // Record for the peer that it was sent + m_sockets[peer_id]->m_menu_cursor = m_menu_cursor; + } + + // Send game events + // Loop through each event that needs to be sent + // send all the events starting at their cursor + // If the client's cursor is beyond the end of the event buffer, then + // it's probably left over from an old game. (Or is invalid anyway) + // So reset it back to 0 + if (m_sockets[peer_id]->m_cursor > m_event_buffer.size()) + { + m_sockets[peer_id]->m_cursor = 0; + } + + for (u64 i = m_sockets[peer_id]->m_cursor; i < m_event_buffer.size(); i++) + { + ENetPacket *packet = + enet_packet_create(m_event_buffer[i].data(), m_event_buffer[i].size(), ENET_PACKET_FLAG_RELIABLE); + // Batch for sending + enet_peer_send(m_sockets[peer_id]->m_peer, 0, packet); + m_sockets[peer_id]->m_cursor++; + } +} + +// CALLED FROM SERVER THREAD +void SlippiSpectateServer::popEvents() +{ + // Loop through the event queue and keep popping off events and handling them + while (!m_event_queue.Empty()) + { + std::string event; + m_event_queue.Pop(event); + // These two are meta-events, used to signify the start/end of a game + // They are not sent over the wire + if (event == "END_GAME") + { + m_menu_cursor = 0; + if (m_event_buffer.size() > 0) + { + m_cursor_offset += m_event_buffer.size(); + } + m_menu_event.clear(); + m_in_game = false; + continue; + } + if (event == "START_GAME") + { + m_event_buffer.clear(); + m_in_game = true; + continue; + } + + // Make json wrapper for game event + json game_event; + + // An SLP event with an empty payload is a quasi-event that signifies + // the unclean exit of a game. Send this out as its own event + // (Since you can't meaningfully concat it with other events) + if (event.empty()) + { + game_event["payload"] = ""; + game_event["type"] = "game_event"; + m_event_buffer.push_back(game_event.dump()); + continue; + } + + if (!m_in_game) + { + game_event["payload"] = base64::Base64::Encode(event); + m_menu_cursor += 1; + game_event["type"] = "menu_event"; + m_menu_event = game_event.dump(); + continue; + } + + u8 command = (u8)event[0]; + m_event_concat = m_event_concat + event; + + static std::unordered_map sendEvents = { + {0x36, true}, // GAME_INIT + {0x3C, true}, // FRAME_END + {0x39, true}, // GAME_END + {0x10, true}, // SPLIT_MESSAGE + }; + + if (sendEvents.count(command)) + { + u32 cursor = (u32)(m_event_buffer.size() + m_cursor_offset); + game_event["payload"] = base64::Base64::Encode(m_event_concat); + game_event["type"] = "game_event"; + game_event["cursor"] = cursor; + game_event["next_cursor"] = cursor + 1; + m_event_buffer.push_back(game_event.dump()); + + m_event_concat = ""; + } + } +} + +// CALLED ONCE EVER, DOLPHIN MAIN THREAD +SlippiSpectateServer::SlippiSpectateServer() +{ + if (isSpectatorEnabled()) + { + m_in_game = false; + m_menu_cursor = 0; + + // Spawn thread for socket listener + m_stop_socket_thread = false; + m_socketThread = std::thread(&SlippiSpectateServer::SlippicommSocketThread, this); + } +} + +// CALLED FROM DOLPHIN MAIN THREAD +SlippiSpectateServer::~SlippiSpectateServer() +{ + // The socket thread will be blocked waiting for input + // So to wake it up, let's connect to the socket! + m_stop_socket_thread = true; + if (m_socketThread.joinable()) + { + m_socketThread.join(); + } +} + +// CALLED FROM SERVER THREAD +void SlippiSpectateServer::handleMessage(u8 *buffer, u32 length, u16 peer_id) +{ + // Unpack the message + std::string message((char *)buffer, length); + json json_message = json::parse(message); + if (!json_message.is_discarded() && (json_message.find("type") != json_message.end())) + { + // Check what type of message this is + if (!json_message["type"].is_string()) + { + return; + } + + if (json_message["type"] == "connect_request") + { + // Get the requested cursor + if (json_message.find("cursor") == json_message.end()) + { + return; + } + if (!json_message["cursor"].is_number_integer()) + { + return; + } + u32 requested_cursor = json_message["cursor"]; + u32 sent_cursor = 0; + // Set the user's cursor position + if (requested_cursor >= m_cursor_offset) + { + // If the requested cursor is past what events we even have, then just tell them to start over + if (requested_cursor > m_event_buffer.size() + m_cursor_offset) + { + m_sockets[peer_id]->m_cursor = 0; + } + // Requested cursor is in the middle of a live match, events that we have + else + { + m_sockets[peer_id]->m_cursor = requested_cursor - m_cursor_offset; + } + } + else + { + // The client requested a cursor that was too low. Bring them up to the present + m_sockets[peer_id]->m_cursor = 0; + } + + sent_cursor = (u32)m_sockets[peer_id]->m_cursor + (u32)m_cursor_offset; + + // If someone joins while at the menu, don't catch them up + // set their cursor to the end + if (!m_in_game) + { + m_sockets[peer_id]->m_cursor = m_event_buffer.size(); + } + + json reply; + reply["type"] = "connect_reply"; + reply["nick"] = "Slippi Online"; + reply["version"] = Common::scm_slippi_semver_str; + reply["cursor"] = sent_cursor; + + std::string packet_buffer = reply.dump(); + + ENetPacket *packet = + enet_packet_create(packet_buffer.data(), (u32)packet_buffer.length(), ENET_PACKET_FLAG_RELIABLE); + + // Batch for sending + enet_peer_send(m_sockets[peer_id]->m_peer, 0, packet); + // Put the client in the right in_game state + m_sockets[peer_id]->m_shook_hands = true; + } + } +} + +void SlippiSpectateServer::SlippicommSocketThread(void) +{ + if (enet_initialize() != 0) + { + WARN_LOG(SLIPPI, "An error occurred while initializing spectator server."); + return; + } + + ENetAddress server_address = {0}; + server_address.host = ENET_HOST_ANY; + server_address.port = SConfig::GetInstance().m_spectator_local_port; + + // Create the spectator server + // This call can fail if the system is already listening on the specified port + // or for some period of time after it closes down. You basically have to just + // retry until the OS lets go of the port and we can claim it again + // This typically only takes a few seconds + ENetHost *server = enet_host_create(&server_address, MAX_CLIENTS, 2, 0, 0); + int tries = 0; + while (server == nullptr && tries < 20) + { + server = enet_host_create(&server_address, MAX_CLIENTS, 2, 0, 0); + tries += 1; + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + } + + if (server == nullptr) + { + WARN_LOG(SLIPPI, "Could not create spectator server"); + enet_deinitialize(); + return; + } + + // Main slippicomm server loop + while (1) + { + // If we're told to stop, then quit + if (m_stop_socket_thread) + { + enet_host_destroy(server); + enet_deinitialize(); + return; + } + + // Pop off any events in the queue + popEvents(); + + std::map>::iterator it = m_sockets.begin(); + for (; it != m_sockets.end(); it++) + { + if (it->second->m_shook_hands) + { + writeEvents(it->first); + } + } + + ENetEvent event; + while (enet_host_service(server, &event, 1) > 0) + { + switch (event.type) + { + case ENET_EVENT_TYPE_CONNECT: + { + + INFO_LOG(SLIPPI, "A new spectator connected from %x:%u.\n", event.peer->address.host, + event.peer->address.port); + + std::shared_ptr newSlippiSocket(new SlippiSocket()); + newSlippiSocket->m_peer = event.peer; + m_sockets[event.peer->incomingPeerID] = newSlippiSocket; + break; + } + case ENET_EVENT_TYPE_RECEIVE: + { + handleMessage(event.packet->data, (u32)event.packet->dataLength, event.peer->incomingPeerID); + /* Clean up the packet now that we're done using it. */ + enet_packet_destroy(event.packet); + + break; + } + case ENET_EVENT_TYPE_DISCONNECT: + { + INFO_LOG(SLIPPI, "A spectator disconnected from %x:%u.\n", event.peer->address.host, + event.peer->address.port); + + // Delete the item in the m_sockets map + m_sockets.erase(event.peer->incomingPeerID); + /* Reset the peer's client information. */ + event.peer->data = NULL; + break; + } + default: + { + INFO_LOG(SLIPPI, "Spectator sent an unknown ENet event type"); + break; + } + } + } + } + + enet_host_destroy(server); + enet_deinitialize(); +} diff --git a/Source/Core/Core/Slippi/SlippiSpectate.h b/Source/Core/Core/Slippi/SlippiSpectate.h new file mode 100644 index 0000000000..fc5cbb53d5 --- /dev/null +++ b/Source/Core/Core/Slippi/SlippiSpectate.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include + +#include "Common/SPSCQueue.h" +#include "nlohmann/json.hpp" +#include +using json = nlohmann::json; + +// Sockets in windows are unsigned +#ifdef _WIN32 +#include +#else +#include +#include +typedef int SOCKET; +#endif + +#define MAX_CLIENTS 4 + +#define HANDSHAKE_MSG_BUF_SIZE 128 +#define HANDSHAKE_TYPE 1 +#define PAYLOAD_TYPE 2 +#define KEEPALIVE_TYPE 3 +#define MENU_TYPE 4 + +class SlippiSocket +{ +public: + u64 m_cursor = 0; // Index of the last game event this client sent + u64 m_menu_cursor = 0; // The latest menu event that this socket has sent + bool m_shook_hands = false; // Has this client shaken hands yet? + ENetPeer* m_peer = NULL; // The ENet peer object for the socket +}; + +class SlippiSpectateServer +{ +public: + // Singleton. Get an instance of the class here + // When SConfig::GetInstance().m_slippiNetworkingOutput is false, this + // instance exists and is callable, but does nothing + static SlippiSpectateServer& getInstance(); + + // Write the given game payload data to all listening sockets + void write(u8* payload, u32 length); + + // Should be called each time a new game starts. + // This will clear out the old game event buffer and start a new one + void startGame(); + + // Clear the game event history buffer. Such as when a game ends. + // The slippi server keeps a history of events in a buffer. So that + // when a new client connects to the server mid-match, it can recieve all + // the game events that have happened so far. This buffer needs to be + // cleared when a match ends. + void endGame(); + +private: + // ACCESSED FROM BOTH DOLPHIN AND SERVER THREADS + // This is a lockless queue that bridges the gap between the main + // dolphin thread and the spectator server thread. The purpose here + // is to avoid blocking (even if just for a brief mutex) on the main + // dolphin thread. + Common::SPSCQueue m_event_queue; + // Bool gets flipped by the destrctor to tell the server thread to shut down + // bools are probably atomic by default, but just for safety... + std::atomic m_stop_socket_thread; + + // ONLY ACCESSED FROM SERVER THREAD + bool m_in_game; + std::map> m_sockets; + std::string m_event_concat = ""; + std::vector m_event_buffer; + std::string m_menu_event; + // In order to emulate Wii behavior, the cursor position should be strictly + // increasing. But internally, we need to index arrays by the cursor value. + // To solve this, we keep an "offset" value that is added to all outgoing + // cursor positions to give the appearance like it's going up + u64 m_cursor_offset = 0; + // How many menu events have we sent so far? (Reset between matches) + // Is used to know when a client hasn't been sent a menu event + u64 m_menu_cursor; + + std::thread m_socketThread; + + SlippiSpectateServer() = default; + ~SlippiSpectateServer() = default; + SlippiSpectateServer(SlippiSpectateServer const&) = delete; + SlippiSpectateServer& operator=(const SlippiSpectateServer&) = delete; + SlippiSpectateServer(SlippiSpectateServer&&) = delete; + SlippiSpectateServer& operator=(SlippiSpectateServer&&) = delete; + + // FUNCTIONS CALLED ONLY FROM SERVER THREAD + // Server thread. Accepts new incoming connections and goes back to sleep + void SlippicommSocketThread(void); + // Handle an incoming message on a socket + void handleMessage(u8* buffer, u32 length, u16 peer_id); + // Catch up given socket to the latest events + // Does nothing if they're already caught up. + void writeEvents(u16 peer_id); + // Pop events + void popEvents(); +};