diff --git a/cmake/FindENet.cmake b/cmake/FindENet.cmake new file mode 100644 index 00000000..f9044c30 --- /dev/null +++ b/cmake/FindENet.cmake @@ -0,0 +1,48 @@ +# - Try to find enet +# Once done this will define +# +# ENET_FOUND - system has enet +# ENET_INCLUDE_DIRS - the enet include directory +# ENET_LIBRARIES - the libraries needed to use enet +# +# $ENETDIR is an environment variable used for finding enet. +# +# Borrowed from The Mana World +# http://themanaworld.org/ +# +# Several changes and additions by Fabian 'x3n' Landau +# Lots of simplifications by Adrian Friedli +# > www.orxonox.net < + +FIND_PATH(ENET_INCLUDE_DIRS enet/enet.h + PATHS + $ENV{ENETDIR} + /usr/local + /usr + PATH_SUFFIXES include +) + +FIND_LIBRARY(ENET_LIBRARY + NAMES enet + PATHS + $ENV{ENETDIR} + /usr/local + /usr + PATH_SUFFIXES lib +) + +# handle the QUIETLY and REQUIRED arguments and set ENET_FOUND to TRUE if +# all listed variables are TRUE +INCLUDE(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(ENet DEFAULT_MSG ENET_LIBRARY ENET_INCLUDE_DIRS) + +IF (ENET_FOUND) + IF(WIN32) + SET(WINDOWS_ENET_DEPENDENCIES "ws2_32;winmm") + SET(ENET_LIBRARIES ${ENET_LIBRARY} ${WINDOWS_ENET_DEPENDENCIES}) + ELSE(WIN32) + SET(ENET_LIBRARIES ${ENET_LIBRARY}) + ENDIF(WIN32) +ENDIF (ENET_FOUND) + +MARK_AS_ADVANCED(ENET_LIBRARY ENET_LIBRARIES ENET_INCLUDE_DIRS) diff --git a/src/frontend/qt_sdl/CMakeLists.txt b/src/frontend/qt_sdl/CMakeLists.txt index 7409d056..972539fe 100644 --- a/src/frontend/qt_sdl/CMakeLists.txt +++ b/src/frontend/qt_sdl/CMakeLists.txt @@ -51,6 +51,9 @@ set(SOURCES_QT_SDL CLI.h CLI.cpp + + LAN.cpp + Netplay.cpp ) if (APPLE) @@ -88,6 +91,8 @@ add_compile_definitions(ARCHIVE_SUPPORT_ENABLED) add_executable(melonDS ${SOURCES_QT_SDL}) +find_package(ENet REQUIRED) + add_subdirectory("../../net" "${CMAKE_BINARY_DIR}/net" ) @@ -168,6 +173,7 @@ endif() target_link_libraries(melonDS PRIVATE core) target_link_libraries(melonDS PRIVATE PkgConfig::SDL2 PkgConfig::LibArchive PkgConfig::Zstd) target_link_libraries(melonDS PRIVATE ${QT_LINK_LIBS} ${CMAKE_DL_LIBS}) +target_link_libraries(melonDS PRIVATE ${ENET_LIBRARIES}) if (WIN32) option(PORTABLE "Make a portable build that looks for its configuration in the current directory" ON) diff --git a/src/frontend/qt_sdl/LAN.cpp b/src/frontend/qt_sdl/LAN.cpp new file mode 100644 index 00000000..58d6e972 --- /dev/null +++ b/src/frontend/qt_sdl/LAN.cpp @@ -0,0 +1,1408 @@ +/* + Copyright 2016-2022 melonDS team + + This file is part of melonDS. + + melonDS 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 3 of the License, or (at your option) + any later version. + + melonDS 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 melonDS. If not, see http://www.gnu.org/licenses/. +*/ + +#include +#include +#include +#include +#include + +#ifdef __WIN32__ + #include + #include + + #define socket_t SOCKET + #define sockaddr_t SOCKADDR + #define sockaddr_in_t SOCKADDR_IN +#else + #include + #include + #include + #include + + #define socket_t int + #define sockaddr_t struct sockaddr + #define sockaddr_in_t struct sockaddr_in + #define closesocket close +#endif + +#ifndef INVALID_SOCKET + #define INVALID_SOCKET (socket_t)-1 +#endif + +#include +#include + +#include +#include +#include +#include + +#include "LAN.h" +#include "Config.h" +#include "main.h" + +#include "ui_LANStartHostDialog.h" +#include "ui_LANStartClientDialog.h" +#include "ui_LANDialog.h" + + +extern EmuThread* emuThread; +LANStartClientDialog* lanClientDlg = nullptr; +LANDialog* lanDlg = nullptr; + + +LANStartHostDialog::LANStartHostDialog(QWidget* parent) : QDialog(parent), ui(new Ui::LANStartHostDialog) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + + // TODO: remember the last setting? so this doesn't suck massively + // we could also remember the player name (and auto-init it from the firmware name or whatever) + ui->sbNumPlayers->setRange(2, 16); + ui->sbNumPlayers->setValue(16); +} + +LANStartHostDialog::~LANStartHostDialog() +{ + delete ui; +} + +void LANStartHostDialog::done(int r) +{ + if (r == QDialog::Accepted) + { + std::string player = ui->txtPlayerName->text().toStdString(); + int numplayers = ui->sbNumPlayers->value(); + + // TODO validate input!! + + lanDlg = LANDialog::openDlg(parentWidget()); + + LAN::StartHost(player.c_str(), numplayers); + } + + QDialog::done(r); +} + + +LANStartClientDialog::LANStartClientDialog(QWidget* parent) : QDialog(parent), ui(new Ui::LANStartClientDialog) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + + QStandardItemModel* model = new QStandardItemModel(); + ui->tvAvailableGames->setModel(model); + const QStringList listheader = {"Name", "Players", "Status", "Host IP"}; + model->setHorizontalHeaderLabels(listheader); + + connect(ui->tvAvailableGames->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), + this, SLOT(onGameSelectionChanged(const QItemSelection&, const QItemSelection&))); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setText("Connect"); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + QPushButton* btn = ui->buttonBox->addButton("Direct connect...", QDialogButtonBox::ActionRole); + connect(btn, SIGNAL(clicked()), this, SLOT(onDirectConnect())); + + connect(this, &LANStartClientDialog::sgUpdateDiscoveryList, this, &LANStartClientDialog::doUpdateDiscoveryList); + + lanClientDlg = this; + LAN::StartDiscovery(); +} + +LANStartClientDialog::~LANStartClientDialog() +{ + lanClientDlg = nullptr; + delete ui; +} + +void LANStartClientDialog::onGameSelectionChanged(const QItemSelection& cur, const QItemSelection& prev) +{ + QModelIndexList indlist = cur.indexes(); + if (indlist.count() == 0) + { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + } + else + { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + } +} + +void LANStartClientDialog::on_tvAvailableGames_doubleClicked(QModelIndex index) +{ + done(QDialog::Accepted); +} + +void LANStartClientDialog::onDirectConnect() +{ + if (ui->txtPlayerName->text().trimmed().isEmpty()) + { + QMessageBox::warning(this, "melonDS", "Please enter a player name before connecting."); + return; + } + + QString host = QInputDialog::getText(this, "Direct connect", "Host address:"); + if (host.isEmpty()) return; + + std::string hostname = host.toStdString(); + std::string player = ui->txtPlayerName->text().toStdString(); + + setEnabled(false); + LAN::EndDiscovery(); + if (!LAN::StartClient(player.c_str(), hostname.c_str())) + { + QString msg = QString("Failed to connect to the host %0.").arg(QString::fromStdString(hostname)); + QMessageBox::warning(this, "melonDS", msg); + setEnabled(true); + LAN::StartDiscovery(); + return; + } + + setEnabled(true); + lanDlg = LANDialog::openDlg(parentWidget()); + QDialog::done(QDialog::Accepted); +} + +void LANStartClientDialog::done(int r) +{ + if (r == QDialog::Accepted) + { + if (ui->txtPlayerName->text().trimmed().isEmpty()) + { + QMessageBox::warning(this, "melonDS", "Please enter a player name before connecting."); + return; + } + + QModelIndexList indlist = ui->tvAvailableGames->selectionModel()->selectedRows(); + if (indlist.count() == 0) return; + + QStandardItemModel* model = (QStandardItemModel*)ui->tvAvailableGames->model(); + QStandardItem* item = model->item(indlist[0].row()); + u32 addr = item->data().toUInt(); + char hostname[16]; + snprintf(hostname, 16, "%d.%d.%d.%d", (addr>>24), ((addr>>16)&0xFF), ((addr>>8)&0xFF), (addr&0xFF)); + + std::string player = ui->txtPlayerName->text().toStdString(); + + setEnabled(false); + LAN::EndDiscovery(); + if (!LAN::StartClient(player.c_str(), hostname)) + { + QString msg = QString("Failed to connect to the host %0.").arg(QString(hostname)); + QMessageBox::warning(this, "melonDS", msg); + setEnabled(true); + LAN::StartDiscovery(); + return; + } + + setEnabled(true); + lanDlg = LANDialog::openDlg(parentWidget()); + } + else + { + LAN::EndDiscovery(); + } + + QDialog::done(r); +} + +void LANStartClientDialog::updateDiscoveryList() +{ + emit sgUpdateDiscoveryList(); +} + +void LANStartClientDialog::doUpdateDiscoveryList() +{ + LAN::DiscoveryMutex.lock(); + + QStandardItemModel* model = (QStandardItemModel*)ui->tvAvailableGames->model(); + int curcount = model->rowCount(); + int newcount = LAN::DiscoveryList.size(); + if (curcount > newcount) + { + model->removeRows(newcount, curcount-newcount); + } + else if (curcount < newcount) + { + for (int i = curcount; i < newcount; i++) + { + QList row; + row.append(new QStandardItem()); + row.append(new QStandardItem()); + row.append(new QStandardItem()); + row.append(new QStandardItem()); + model->appendRow(row); + } + } + + int i = 0; + for (const auto& [key, data] : LAN::DiscoveryList) + { + model->item(i, 0)->setText(data.SessionName); + model->item(i, 0)->setData(QVariant(key)); + + QString plcount = QString("%0/%1").arg(data.NumPlayers).arg(data.MaxPlayers); + model->item(i, 1)->setText(plcount); + + QString status; + switch (data.Status) + { + case 0: status = "Idle"; break; + case 1: status = "Playing"; break; + } + model->item(i, 2)->setText(status); + + QString ip = QString("%0.%1.%2.%3").arg(key>>24).arg((key>>16)&0xFF).arg((key>>8)&0xFF).arg(key&0xFF); + model->item(i, 3)->setText(ip); + + i++; + } + + LAN::DiscoveryMutex.unlock(); +} + + +LANDialog::LANDialog(QWidget* parent) : QDialog(parent), ui(new Ui::LANDialog) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + + QStandardItemModel* model = new QStandardItemModel(); + ui->tvPlayerList->setModel(model); + const QStringList header = {"#", "Player", "Status", "Ping", "IP"}; + model->setHorizontalHeaderLabels(header); + + connect(this, &LANDialog::sgUpdatePlayerList, this, &LANDialog::doUpdatePlayerList); +} + +LANDialog::~LANDialog() +{ + delete ui; +} + +void LANDialog::done(int r) +{ + // ??? + + QDialog::done(r); +} + +void LANDialog::updatePlayerList() +{ + playerListMutex.lock(); + memcpy(playerList, LAN::Players, sizeof(playerList)); + memcpy(playerPing, LAN::PlayerPing, sizeof(playerPing)); + numPlayers = LAN::NumPlayers; + maxPlayers = LAN::MaxPlayers; + myPlayerID = LAN::MyPlayer.ID; + hostAddress = LAN::HostAddress; + playerListMutex.unlock(); + + emit sgUpdatePlayerList(); +} + +void LANDialog::doUpdatePlayerList() +{ + playerListMutex.lock(); + + QStandardItemModel* model = (QStandardItemModel*)ui->tvPlayerList->model(); + int curcount = model->rowCount(); + int newcount = numPlayers; + if (curcount > newcount) + { + model->removeRows(newcount, curcount-newcount); + } + else if (curcount < newcount) + { + for (int i = curcount; i < newcount; i++) + { + QList row; + row.append(new QStandardItem()); + row.append(new QStandardItem()); + row.append(new QStandardItem()); + row.append(new QStandardItem()); + row.append(new QStandardItem()); + model->appendRow(row); + } + } + + for (int i = 0; i < 16; i++) + { + LAN::Player* player = &playerList[i]; + if (player->Status == 0) break; + + QString id = QString("%0/%1").arg(player->ID+1).arg(maxPlayers); + model->item(i, 0)->setText(id); + + QString name = player->Name; + model->item(i, 1)->setText(name); + + QString status; + switch (player->Status) + { + case 1: status = "Connected"; break; + case 2: status = "Game host"; break; + case 3: status = "Connecting"; break; + case 4: status = "Connection lost"; break; + } + model->item(i, 2)->setText(status); + + if (i == myPlayerID) + { + model->item(i, 3)->setText("-"); + model->item(i, 4)->setText("(local)"); + } + else + { + if (player->Status == 1 || player->Status == 2) + { + QString ping = QString("%0 ms").arg(playerPing[i]); + model->item(i, 3)->setText(ping); + } + else + { + model->item(i, 3)->setText("-"); + } + + // note on the player IP display + // * we make an exception for the host -- the player list is issued by the host, so the host IP would be 127.0.0.1 + // * for the same reason, the host can't know its own IP, so for the current player we force it to 127.0.0.1 + u32 ip; + if (player->Status == 2) + ip = hostAddress; + else + ip = player->Address; + + QString ips = QString("%0.%1.%2.%3").arg(ip&0xFF).arg((ip>>8)&0xFF).arg((ip>>16)&0xFF).arg(ip>>24); + model->item(i, 4)->setText(ips); + } + } + + playerListMutex.unlock(); +} + + +namespace LAN +{ + +const u32 kDiscoveryMagic = 0x444E414C; // LAND +const u32 kLANMagic = 0x504E414C; // LANP +const u32 kPacketMagic = 0x4946494E; // NIFI + +const u32 kProtocolVersion = 1; + +struct MPPacketHeader +{ + u32 Magic; + u32 SenderID; + u32 Type; // 0=regular 1=CMD 2=reply 3=ack + u32 Length; + u64 Timestamp; +}; + +const int kDiscoveryPort = 7063; +const int kLANPort = 7064; + +socket_t DiscoverySocket; +u32 DiscoveryLastTick; +std::map DiscoveryList; +QMutex DiscoveryMutex; + +bool Active; +bool IsHost; + +ENetHost* Host; +ENetPeer* RemotePeers[16]; + +Player Players[16]; +u32 PlayerPing[16]; +int NumPlayers; +int MaxPlayers; + +u16 ConnectedBitmask; + +Player MyPlayer; +u32 HostAddress; +bool Lag; + +int MPRecvTimeout; +int LastHostID; +ENetPeer* LastHostPeer; +std::queue RXQueue; + +u32 FrameCount; + + +bool Init() +{ + DiscoverySocket = INVALID_SOCKET; + DiscoveryLastTick = 0; + + Active = false; + IsHost = false; + Host = nullptr; + Lag = false; + + memset(RemotePeers, 0, sizeof(RemotePeers)); + memset(Players, 0, sizeof(Players)); + memset(PlayerPing, 0, sizeof(PlayerPing)); + NumPlayers = 0; + MaxPlayers = 0; + + ConnectedBitmask = 0; + + MPRecvTimeout = 25; + LastHostID = -1; + LastHostPeer = nullptr; + + FrameCount = 0; + + // TODO we init enet here but also in Netplay + // that is redundant + if (enet_initialize() != 0) + { + printf("enet shat itself :(\n"); + return false; + } + + printf("enet init OK\n"); + return true; +} + +void DeInit() +{ + if (DiscoverySocket) + { + closesocket(DiscoverySocket); + DiscoverySocket = INVALID_SOCKET; + } + + while (!RXQueue.empty()) + { + ENetPacket* packet = RXQueue.front(); + RXQueue.pop(); + enet_packet_destroy(packet); + } + + for (int i = 0; i < 16; i++) + { + if (i == MyPlayer.ID) continue; + + if (RemotePeers[i]) + enet_peer_disconnect(RemotePeers[i], 0); + + RemotePeers[i] = nullptr; + } + + enet_host_destroy(Host); + Host = nullptr; + + enet_deinitialize(); +} + + +bool StartDiscovery() +{ + int res; + + DiscoverySocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (DiscoverySocket < 0) + { + DiscoverySocket = INVALID_SOCKET; + return false; + } + + sockaddr_in_t saddr; + memset(&saddr, 0, sizeof(saddr)); + saddr.sin_family = AF_INET; + saddr.sin_addr.s_addr = htonl(INADDR_ANY); + saddr.sin_port = htons(kDiscoveryPort); + res = bind(DiscoverySocket, (const sockaddr_t*)&saddr, sizeof(saddr)); + if (res < 0) + { + closesocket(DiscoverySocket); + DiscoverySocket = INVALID_SOCKET; + return false; + } + + int opt_true = 1; + res = setsockopt(DiscoverySocket, SOL_SOCKET, SO_BROADCAST, (const char*)&opt_true, sizeof(int)); + if (res < 0) + { + closesocket(DiscoverySocket); + DiscoverySocket = INVALID_SOCKET; + return false; + } + + DiscoveryLastTick = SDL_GetTicks(); + DiscoveryList.clear(); + + Active = true; + return true; +} + +void EndDiscovery() +{ + if (DiscoverySocket != INVALID_SOCKET) + { + closesocket(DiscoverySocket); + DiscoverySocket = INVALID_SOCKET; + } + + if (!IsHost) + Active = false; +} + +bool StartHost(const char* playername, int numplayers) +{ + ENetAddress addr; + addr.host = ENET_HOST_ANY; + addr.port = kLANPort; + + Host = enet_host_create(&addr, 16, 2, 0, 0); + if (!Host) + { + return false; + } + + Player* player = &Players[0]; + memset(player, 0, sizeof(Player)); + player->ID = 0; + strncpy(player->Name, playername, 31); + player->Status = 2; + player->Address = 0x0100007F; + NumPlayers = 1; + MaxPlayers = numplayers; + memcpy(&MyPlayer, player, sizeof(Player)); + + HostAddress = 0x0100007F; + LastHostID = -1; + LastHostPeer = nullptr; + + Active = true; + IsHost = true; + + if (lanDlg) + lanDlg->updatePlayerList(); + + StartDiscovery(); + return true; +} + +bool StartClient(const char* playername, const char* host) +{ + Host = enet_host_create(nullptr, 16, 2, 0, 0); + if (!Host) + { + return false; + } + + ENetAddress addr; + enet_address_set_host(&addr, host); + addr.port = kLANPort; + ENetPeer* peer = enet_host_connect(Host, &addr, 2, 0); + if (!peer) + { + enet_host_destroy(Host); + Host = nullptr; + return false; + } + + Player* player = &MyPlayer; + memset(player, 0, sizeof(Player)); + player->ID = 0; + strncpy(player->Name, playername, 31); + player->Status = 3; + + ENetEvent event; + int conn = 0; + u32 starttick = SDL_GetTicks(); + const int conntimeout = 5000; + for (;;) + { + u32 curtick = SDL_GetTicks(); + if (curtick < starttick) break; + int timeout = conntimeout - (int)(curtick - starttick); + if (timeout < 0) break; + if (enet_host_service(Host, &event, timeout) > 0) + { + if (conn == 0 && event.type == ENET_EVENT_TYPE_CONNECT) + { + conn = 1; + } + else if (conn == 1 && event.type == ENET_EVENT_TYPE_RECEIVE) + { + u8* data = event.packet->data; + if (event.channelID != 0) continue; + if (data[0] != 0x01) continue; + if (event.packet->dataLength != 11) continue; + + u32 magic = data[1] | (data[2] << 8) | (data[3] << 16) | (data[4] << 24); + u32 version = data[5] | (data[6] << 8) | (data[7] << 16) | (data[8] << 24); + if (magic != kLANMagic) continue; + if (version != kProtocolVersion) continue; + + MaxPlayers = data[10]; + + // send player information + MyPlayer.ID = data[9]; + u8 cmd[9+sizeof(Player)]; + cmd[0] = 0x02; + cmd[1] = (u8)kLANMagic; + cmd[2] = (u8)(kLANMagic >> 8); + cmd[3] = (u8)(kLANMagic >> 16); + cmd[4] = (u8)(kLANMagic >> 24); + cmd[5] = (u8)kProtocolVersion; + cmd[6] = (u8)(kProtocolVersion >> 8); + cmd[7] = (u8)(kProtocolVersion >> 16); + cmd[8] = (u8)(kProtocolVersion >> 24); + memcpy(&cmd[9], &MyPlayer, sizeof(Player)); + ENetPacket* pkt = enet_packet_create(cmd, 9+sizeof(Player), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(event.peer, 0, pkt); + + conn = 2; + break; + } + else if (event.type == ENET_EVENT_TYPE_DISCONNECT) + { + conn = 0; + break; + } + } + else + break; + } + + if (conn != 2) + { + enet_peer_reset(peer); + enet_host_destroy(Host); + Host = nullptr; + return false; + } + + HostAddress = addr.host; + LastHostID = -1; + LastHostPeer = nullptr; + RemotePeers[0] = peer; + peer->data = &Players[0]; + + Active = true; + IsHost = false; + return true; +} + + +void ProcessDiscovery() +{ + if (DiscoverySocket == INVALID_SOCKET) + return; + + u32 tick = SDL_GetTicks(); + if ((tick - DiscoveryLastTick) < 1000) + return; + + DiscoveryLastTick = tick; + + if (IsHost) + { + // advertise this LAN session over the network + + DiscoveryData beacon; + memset(&beacon, 0, sizeof(beacon)); + beacon.Magic = kDiscoveryMagic; + beacon.Version = kProtocolVersion; + beacon.Tick = tick; + snprintf(beacon.SessionName, 64, "%s's game", MyPlayer.Name); + beacon.NumPlayers = NumPlayers; + beacon.MaxPlayers = MaxPlayers; + beacon.Status = 0; // TODO + + sockaddr_in_t saddr; + memset(&saddr, 0, sizeof(saddr)); + saddr.sin_family = AF_INET; + saddr.sin_addr.s_addr = htonl(INADDR_BROADCAST); + saddr.sin_port = htons(kDiscoveryPort); + + sendto(DiscoverySocket, (const char*)&beacon, sizeof(beacon), 0, (const sockaddr_t*)&saddr, sizeof(saddr)); + } + else + { + DiscoveryMutex.lock(); + + // listen for LAN sessions + + fd_set fd; + struct timeval tv; + for (;;) + { + FD_ZERO(&fd); FD_SET(DiscoverySocket, &fd); + tv.tv_sec = 0; tv.tv_usec = 0; + if (!select(DiscoverySocket+1, &fd, nullptr, nullptr, &tv)) + break; + + DiscoveryData beacon; + sockaddr_in_t raddr; + socklen_t ralen = sizeof(raddr); + + int rlen = recvfrom(DiscoverySocket, (char*)&beacon, sizeof(beacon), 0, (sockaddr_t*)&raddr, &ralen); + if (rlen < sizeof(beacon)) continue; + if (beacon.Magic != kDiscoveryMagic) continue; + if (beacon.Version != kProtocolVersion) continue; + if (beacon.MaxPlayers > 16) continue; + if (beacon.NumPlayers > beacon.MaxPlayers) continue; + + u32 key = ntohl(raddr.sin_addr.s_addr); + + if (DiscoveryList.find(key) != DiscoveryList.end()) + { + if (beacon.Tick <= DiscoveryList[key].Tick) + continue; + } + + beacon.Magic = tick; + beacon.SessionName[63] = '\0'; + DiscoveryList[key] = beacon; + } + + // cleanup: remove hosts that haven't given a sign of life in the last 5 seconds + + std::vector deletelist; + + for (const auto& [key, data] : DiscoveryList) + { + u32 age = tick - data.Magic; + if (age < 5000) continue; + + deletelist.push_back(key); + } + + for (const auto& key : deletelist) + { + DiscoveryList.erase(key); + } + + DiscoveryMutex.unlock(); + + // update the list in the connect dialog if needed + + if (lanClientDlg) + lanClientDlg->updateDiscoveryList(); + } +} + +void HostUpdatePlayerList() +{ + u8 cmd[2+sizeof(Players)]; + cmd[0] = 0x03; + cmd[1] = (u8)NumPlayers; + memcpy(&cmd[2], Players, sizeof(Players)); + ENetPacket* pkt = enet_packet_create(cmd, 2+sizeof(Players), ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(Host, 0, pkt); + + if (lanDlg) + lanDlg->updatePlayerList(); +} + +void ClientUpdatePlayerList() +{ + if (lanDlg) + lanDlg->updatePlayerList(); +} + +void ProcessHostEvent(ENetEvent& event) +{ + switch (event.type) + { + case ENET_EVENT_TYPE_CONNECT: + { + if ((NumPlayers >= MaxPlayers) || (NumPlayers >= 16)) + { + // game is full, reject connection + enet_peer_disconnect(event.peer, 0); + break; + } + + // client connected; assign player number + + int id; + for (id = 0; id < 16; id++) + { + if (id >= NumPlayers) break; + if (Players[id].Status == 0) break; + } + + if (id < 16) + { + u8 cmd[11]; + cmd[0] = 0x01; + cmd[1] = (u8)kLANMagic; + cmd[2] = (u8)(kLANMagic >> 8); + cmd[3] = (u8)(kLANMagic >> 16); + cmd[4] = (u8)(kLANMagic >> 24); + cmd[5] = (u8)kProtocolVersion; + cmd[6] = (u8)(kProtocolVersion >> 8); + cmd[7] = (u8)(kProtocolVersion >> 16); + cmd[8] = (u8)(kProtocolVersion >> 24); + cmd[9] = (u8)id; + cmd[10] = MaxPlayers; + ENetPacket* pkt = enet_packet_create(cmd, 11, ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(event.peer, 0, pkt); + + Players[id].ID = id; + Players[id].Status = 3; + Players[id].Address = event.peer->address.host; + event.peer->data = &Players[id]; + NumPlayers++; + + RemotePeers[id] = event.peer; + } + else + { + // ??? + enet_peer_disconnect(event.peer, 0); + } + } + break; + + case ENET_EVENT_TYPE_DISCONNECT: + { + Player* player = (Player*)event.peer->data; + if (!player) break; + + ConnectedBitmask &= ~(1 << player->ID); + + int id = player->ID; + RemotePeers[id] = nullptr; + + player->ID = 0; + player->Status = 0; + NumPlayers--; + + // broadcast updated player list + HostUpdatePlayerList(); + } + break; + + case ENET_EVENT_TYPE_RECEIVE: + { + if (event.packet->dataLength < 1) break; + + u8* data = (u8*)event.packet->data; + switch (data[0]) + { + case 0x02: // client sending player info + { + if (event.packet->dataLength != (9+sizeof(Player))) break; + + u32 magic = data[1] | (data[2] << 8) | (data[3] << 16) | (data[4] << 24); + u32 version = data[5] | (data[6] << 8) | (data[7] << 16) | (data[8] << 24); + if ((magic != kLANMagic) || (version != kProtocolVersion)) + { + enet_peer_disconnect(event.peer, 0); + break; + } + + Player player; + memcpy(&player, &data[9], sizeof(Player)); + player.Name[31] = '\0'; + + Player* hostside = (Player*)event.peer->data; + if (player.ID != hostside->ID) + { + enet_peer_disconnect(event.peer, 0); + break; + } + + player.Status = 1; + player.Address = event.peer->address.host; + memcpy(hostside, &player, sizeof(Player)); + + // broadcast updated player list + HostUpdatePlayerList(); + } + break; + + case 0x04: // player connected + { + if (event.packet->dataLength != 1) break; + Player* player = (Player*)event.peer->data; + //printf("HOST: PLAYER CONNECT %p\n", player); + if (!player) break; + + ConnectedBitmask |= (1 << player->ID); + } + break; + + case 0x05: // player disconnected + { + if (event.packet->dataLength != 1) break; + Player* player = (Player*)event.peer->data; + //printf("HOST: PLAYER DISCONNECT %p\n", player); + if (!player) break; + + ConnectedBitmask &= ~(1 << player->ID); + } + break; + } + + enet_packet_destroy(event.packet); + } + break; + } +} + +void ProcessClientEvent(ENetEvent& event) +{ + switch (event.type) + { + case ENET_EVENT_TYPE_CONNECT: + { + // another client is establishing a direct connection to us + + int playerid = -1; + for (int i = 0; i < 16; i++) + { + Player* player = &Players[i]; + if (i == MyPlayer.ID) continue; + if (player->Status != 1) continue; + + if (player->Address == event.peer->address.host) + { + playerid = i; + break; + } + } + + if (playerid < 0) + { + enet_peer_disconnect(event.peer, 0); + break; + } + + RemotePeers[playerid] = event.peer; + event.peer->data = &Players[playerid]; + } + break; + + case ENET_EVENT_TYPE_DISCONNECT: + { + Player* player = (Player*)event.peer->data; + if (!player) break; + + ConnectedBitmask &= ~(1 << player->ID); + + int id = player->ID; + RemotePeers[id] = nullptr; + + player->Status = 4; + + ClientUpdatePlayerList(); + } + break; + + case ENET_EVENT_TYPE_RECEIVE: + { + if (event.packet->dataLength < 1) break; + + u8* data = (u8*)event.packet->data; + switch (data[0]) + { + case 0x03: // host sending player list + { + if (event.packet->dataLength != (2+sizeof(Players))) break; + if (data[1] > 16) break; + + NumPlayers = data[1]; + memcpy(Players, &data[2], sizeof(Players)); + for (int i = 0; i < 16; i++) + { + Players[i].Name[31] = '\0'; + } + + if (lanDlg) + lanDlg->updatePlayerList(); + + // establish connections to any new clients + for (int i = 0; i < 16; i++) + { + Player* player = &Players[i]; + if (i == MyPlayer.ID) continue; + if (player->Status != 1) continue; + + if (!RemotePeers[i]) + { + ENetAddress peeraddr; + peeraddr.host = player->Address; + peeraddr.port = kLANPort; + ENetPeer* peer = enet_host_connect(Host, &peeraddr, 2, 0); + if (!peer) + { + // TODO deal with this + continue; + } + } + } + } + break; + + case 0x04: // player connected + { + if (event.packet->dataLength != 1) break; + Player* player = (Player*)event.peer->data; + //printf("CLIENT: PLAYER CONNECT %p\n", player); + if (!player) break; + + ConnectedBitmask |= (1 << player->ID); + } + break; + + case 0x05: // player disconnected + { + if (event.packet->dataLength != 1) break; + Player* player = (Player*)event.peer->data; + //printf("CLIENT: PLAYER DISCONNECT %p\n", player); + if (!player) break; + + ConnectedBitmask &= ~(1 << player->ID); + } + break; + } + + enet_packet_destroy(event.packet); + } + break; + } +} + +void ProcessEvent(ENetEvent& event) +{ + if (IsHost) + ProcessHostEvent(event); + else + ProcessClientEvent(event); +} + +// 0 = per-frame processing of events and eventual misc. frame +// 1 = checking if a misc. frame has arrived +// 2 = waiting for a MP frame +void Process(int type) +{ + if (!Host) return; + //printf("Process(%d): %d %d\n", type, RXQueue.empty(), RXQueue.size()); + + u32 time_last = SDL_GetTicks(); + + // see if we have queued packets already, get rid of the stale ones + // any incoming packet should be consumed by the core quickly, so if + // they've been sitting in the queue for more than one frame's time, + // we can assume they're stale + while (!RXQueue.empty()) + { + ENetPacket* enetpacket = RXQueue.front(); + MPPacketHeader* header = (MPPacketHeader*)&enetpacket->data[0]; + u32 packettime = header->Magic; + + if ((packettime > time_last) || (packettime < (time_last - 16))) + { + RXQueue.pop(); + enet_packet_destroy(enetpacket); + } + else + { + // we got a packet, depending on what the caller wants we might be able to return now + if (type == 2) return; + if (type == 1) + { + // if looking for a misc. frame, we shouldn't be receiving a MP frame + if (header->Type == 0) + return; + + RXQueue.pop(); + enet_packet_destroy(enetpacket); + } + + break; + } + } + + int timeout = (type == 2) ? MPRecvTimeout : 0; + time_last = SDL_GetTicks(); + + ENetEvent event; + while (enet_host_service(Host, &event, timeout) > 0) + { + if (event.type == ENET_EVENT_TYPE_RECEIVE && event.channelID == 1) + { + MPPacketHeader* header = (MPPacketHeader*)&event.packet->data[0]; + //printf("- enet_host_service: (%d) got MP frame, len=%d type=%08X fc=%04X\n", type, event.packet->dataLength, header->Type, *(u16*)&event.packet->data[sizeof(MPPacketHeader)+12]); + bool good = true; + if (event.packet->dataLength < sizeof(MPPacketHeader)) + good = false; + else if (header->Magic != 0x4946494E) + good = false; + else if (header->SenderID == MyPlayer.ID) + good = false; + + if (!good) + { + enet_packet_destroy(event.packet); + } + else + { + // mark this packet with the time it was received + header->Magic = SDL_GetTicks(); + + event.packet->userData = event.peer; + RXQueue.push(event.packet); + + // return now -- if we are receiving MP frames, if we keep going + // we'll consume too many even if we have no timeout set + return; + } + } + else + { + //printf("- enet_host_service: got something else, time=%d\n", SDL_GetTicks()-time_last); + ProcessEvent(event); + } + + if (type == 2) + { + u32 time = SDL_GetTicks(); + if (time < time_last) return; + timeout -= (int)(time - time_last); + if (timeout <= 0) return; + time_last = time; + } + } +} + +void ProcessFrame() +{ + ProcessDiscovery(); + Process(0); + + FrameCount++; + if (FrameCount >= 60) + { + FrameCount = 0; + + for (int i = 0; i < 16; i++) + { + if (Players[i].Status == 0) continue; + if (i == MyPlayer.ID) continue; + if (!RemotePeers[i]) continue; + + PlayerPing[i] = RemotePeers[i]->roundTripTime; + } + + if (lanDlg) + lanDlg->updatePlayerList(); + } +} + + +void SetMPRecvTimeout(int timeout) +{ + MPRecvTimeout = timeout; +} + +void MPBegin() +{ + if (!Host) return; + + ConnectedBitmask |= (1 << MyPlayer.ID); + LastHostID = -1; + LastHostPeer = nullptr; + + u8 cmd = 0x04; + ENetPacket* pkt = enet_packet_create(&cmd, 1, ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(Host, 0, pkt); +} + +void MPEnd() +{ + if (!Host) return; + + ConnectedBitmask &= ~(1 << MyPlayer.ID); + + u8 cmd = 0x05; + ENetPacket* pkt = enet_packet_create(&cmd, 1, ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(Host, 0, pkt); +} + + +int SendMPPacketGeneric(u32 type, u8* packet, int len, u64 timestamp) +{ + if (!Host) return 0; + + // TODO make the reliable part optional? + //u32 flags = ENET_PACKET_FLAG_RELIABLE; + u32 flags = ENET_PACKET_FLAG_UNSEQUENCED; + + ENetPacket* enetpacket = enet_packet_create(nullptr, sizeof(MPPacketHeader)+len, flags); + + MPPacketHeader pktheader; + pktheader.Magic = 0x4946494E; + pktheader.SenderID = MyPlayer.ID; + pktheader.Type = type; + pktheader.Length = len; + pktheader.Timestamp = timestamp; + memcpy(&enetpacket->data[0], &pktheader, sizeof(MPPacketHeader)); + if (len) + memcpy(&enetpacket->data[sizeof(MPPacketHeader)], packet, len); + + if (((type & 0xFFFF) == 2) && LastHostPeer) + enet_peer_send(LastHostPeer, 1, enetpacket); + else + enet_host_broadcast(Host, 1, enetpacket); + enet_host_flush(Host); + + return len; +} + +int RecvMPPacketGeneric(u8* packet, bool block, u64* timestamp) +{ + if (!Host) return 0; + + Process(block ? 2 : 1); + if (RXQueue.empty()) return 0; + + ENetPacket* enetpacket = RXQueue.front(); + RXQueue.pop(); + MPPacketHeader* header = (MPPacketHeader*)&enetpacket->data[0]; + + u32 len = header->Length; + if (len) + { + if (len > 2048) len = 2048; + + memcpy(packet, &enetpacket->data[sizeof(MPPacketHeader)], len); + + if (header->Type == 1) + { + LastHostID = header->SenderID; + LastHostPeer = (ENetPeer*)enetpacket->userData; + } + } + + if (timestamp) *timestamp = header->Timestamp; + enet_packet_destroy(enetpacket); + return len; +} + + +int SendMPPacket(u8* packet, int len, u64 timestamp) +{ + return SendMPPacketGeneric(0, packet, len, timestamp); +} + +int RecvMPPacket(u8* packet, u64* timestamp) +{ + return RecvMPPacketGeneric(packet, false, timestamp); +} + + +int SendMPCmd(u8* packet, int len, u64 timestamp) +{ + return SendMPPacketGeneric(1, packet, len, timestamp); +} + +int SendMPReply(u8* packet, int len, u64 timestamp, u16 aid) +{ + return SendMPPacketGeneric(2 | (aid<<16), packet, len, timestamp); +} + +int SendMPAck(u8* packet, int len, u64 timestamp) +{ + return SendMPPacketGeneric(3, packet, len, timestamp); +} + +int RecvMPHostPacket(u8* packet, u64* timestamp) +{ + if (LastHostID != -1) + { + // check if the host is still connected + + if (!(ConnectedBitmask & (1<data[0]; + bool good = true; + if ((header->Type & 0xFFFF) != 2) + good = false; + else if (header->Timestamp < (timestamp - 32)) + good = false; + + if (good) + { + u32 len = header->Length; + if (len) + { + if (len > 1024) len = 1024; + + u32 aid = header->Type >> 16; + memcpy(&packets[(aid-1)*1024], &enetpacket->data[sizeof(MPPacketHeader)], len); + + ret |= (1<SenderID); + if (((myinstmask & ConnectedBitmask) == ConnectedBitmask) || + ((ret & aidmask) == aidmask)) + { + // all the clients have sent their reply + enet_packet_destroy(enetpacket); + return ret; + } + } + + enet_packet_destroy(enetpacket); + } +} + +} diff --git a/src/frontend/qt_sdl/LAN.h b/src/frontend/qt_sdl/LAN.h new file mode 100644 index 00000000..76cc95d1 --- /dev/null +++ b/src/frontend/qt_sdl/LAN.h @@ -0,0 +1,192 @@ +/* + Copyright 2016-2022 melonDS team + + This file is part of melonDS. + + melonDS 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 3 of the License, or (at your option) + any later version. + + melonDS 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 melonDS. If not, see http://www.gnu.org/licenses/. +*/ + +#ifndef LAN_H +#define LAN_H + +#include +#include +#include +#include +#include + +#include "types.h" + +namespace Ui +{ +class LANStartHostDialog; +class LANStartClientDialog; +class LANDialog; +} + +namespace LAN +{ + +struct Player +{ + int ID; + char Name[32]; + int Status; // 0=no player 1=normal 2=host 3=connecting 4=disconnected + u32 Address; +}; + +struct DiscoveryData +{ + u32 Magic; + u32 Version; + u32 Tick; + char SessionName[64]; + u8 NumPlayers; + u8 MaxPlayers; + u8 Status; // 0=idle 1=playing +}; + +} + +class LANStartHostDialog : public QDialog +{ + Q_OBJECT + +public: + explicit LANStartHostDialog(QWidget* parent); + ~LANStartHostDialog(); + + static LANStartHostDialog* openDlg(QWidget* parent) + { + LANStartHostDialog* dlg = new LANStartHostDialog(parent); + dlg->open(); + return dlg; + } + +private slots: + void done(int r); + +private: + Ui::LANStartHostDialog* ui; +}; + +class LANStartClientDialog : public QDialog +{ + Q_OBJECT + +public: + explicit LANStartClientDialog(QWidget* parent); + ~LANStartClientDialog(); + + static LANStartClientDialog* openDlg(QWidget* parent) + { + LANStartClientDialog* dlg = new LANStartClientDialog(parent); + dlg->open(); + return dlg; + } + + void updateDiscoveryList(); + +signals: + void sgUpdateDiscoveryList(); + +private slots: + void onGameSelectionChanged(const QItemSelection& cur, const QItemSelection& prev); + void on_tvAvailableGames_doubleClicked(QModelIndex index); + void onDirectConnect(); + void done(int r); + + void doUpdateDiscoveryList(); + +private: + Ui::LANStartClientDialog* ui; +}; + +class LANDialog : public QDialog +{ + Q_OBJECT + +public: + explicit LANDialog(QWidget* parent); + ~LANDialog(); + + static LANDialog* openDlg(QWidget* parent) + { + LANDialog* dlg = new LANDialog(parent); + dlg->show(); + return dlg; + } + + void updatePlayerList(); + +signals: + void sgUpdatePlayerList(); + +private slots: + void done(int r); + + void doUpdatePlayerList(); + +private: + Ui::LANDialog* ui; + + LAN::Player playerList[16]; + u32 playerPing[16]; + int numPlayers; + int maxPlayers; + int myPlayerID; + u32 hostAddress; + QMutex playerListMutex; +}; + +namespace LAN +{ + +extern bool Active; + +extern std::map DiscoveryList; +extern QMutex DiscoveryMutex; + +extern Player Players[16]; +extern u32 PlayerPing[16]; +extern int NumPlayers; +extern int MaxPlayers; + +extern Player MyPlayer; +extern u32 HostAddress; + +bool Init(); +void DeInit(); + +bool StartDiscovery(); +void EndDiscovery(); +bool StartHost(const char* player, int numplayers); +bool StartClient(const char* player, const char* host); + +void ProcessFrame(); + +void SetMPRecvTimeout(int timeout); +void MPBegin(); +void MPEnd(); + +int SendMPPacket(u8* data, int len, u64 timestamp); +int RecvMPPacket(u8* data, u64* timestamp); +int SendMPCmd(u8* data, int len, u64 timestamp); +int SendMPReply(u8* data, int len, u64 timestamp, u16 aid); +int SendMPAck(u8* data, int len, u64 timestamp); +int RecvMPHostPacket(u8* data, u64* timestamp); +u16 RecvMPReplies(u8* data, u64 timestamp, u16 aidmask); + +} + +#endif // LAN_H diff --git a/src/frontend/qt_sdl/LANDialog.ui b/src/frontend/qt_sdl/LANDialog.ui new file mode 100644 index 00000000..e99000e4 --- /dev/null +++ b/src/frontend/qt_sdl/LANDialog.ui @@ -0,0 +1,31 @@ + + + LANDialog + + + + 0 + 0 + 522 + 391 + + + + LAN SHITO + + + + + + STATUS PLACEHOLDER + + + + + + + + + + + diff --git a/src/frontend/qt_sdl/LANStartClientDialog.ui b/src/frontend/qt_sdl/LANStartClientDialog.ui new file mode 100644 index 00000000..eeea25e9 --- /dev/null +++ b/src/frontend/qt_sdl/LANStartClientDialog.ui @@ -0,0 +1,117 @@ + + + LANStartClientDialog + + + + 0 + 0 + 547 + 409 + + + + + 0 + 0 + + + + Join LAN game - melonDS + + + + + + + + Player name: + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + QAbstractItemView::NoEditTriggers + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + LANStartClientDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + LANStartClientDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/frontend/qt_sdl/LANStartHostDialog.ui b/src/frontend/qt_sdl/LANStartHostDialog.ui new file mode 100644 index 00000000..0d6cd50c --- /dev/null +++ b/src/frontend/qt_sdl/LANStartHostDialog.ui @@ -0,0 +1,97 @@ + + + LANStartHostDialog + + + + 0 + 0 + 389 + 228 + + + + + 0 + 0 + + + + Host LAN game - melonDS + + + + QLayout::SetFixedSize + + + + + + + Player name: + + + + + + + + + + Number of players: + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + LANStartHostDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + LANStartHostDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/frontend/qt_sdl/Netplay.cpp b/src/frontend/qt_sdl/Netplay.cpp new file mode 100644 index 00000000..74fd30b6 --- /dev/null +++ b/src/frontend/qt_sdl/Netplay.cpp @@ -0,0 +1,1222 @@ +/* + Copyright 2016-2022 melonDS team + + This file is part of melonDS. + + melonDS 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 3 of the License, or (at your option) + any later version. + + melonDS 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 melonDS. If not, see http://www.gnu.org/licenses/. +*/ + +#include +#include +#include +#include + +#include + +#include +#include + +#include "NDS.h" +#include "NDSCart.h" +#include "main.h" +#include "IPC.h" +#include "Netplay.h" +#include "Input.h" +#include "ROMManager.h" +#include "Config.h" +#include "Savestate.h" +#include "Platform.h" + +#include "ui_NetplayStartHostDialog.h" +#include "ui_NetplayStartClientDialog.h" +#include "ui_NetplayDialog.h" + + +extern EmuThread* emuThread; +NetplayDialog* netplayDlg; + + +NetplayStartHostDialog::NetplayStartHostDialog(QWidget* parent) : QDialog(parent), ui(new Ui::NetplayStartHostDialog) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + + ui->txtPort->setText("8064"); +} + +NetplayStartHostDialog::~NetplayStartHostDialog() +{ + delete ui; +} + +void NetplayStartHostDialog::done(int r) +{ + if (r == QDialog::Accepted) + { + std::string player = ui->txtPlayerName->text().toStdString(); + int port = ui->txtPort->text().toInt(); + + // TODO validate input!! + + netplayDlg = NetplayDialog::openDlg(parentWidget()); + + Netplay::StartHost(player.c_str(), port); + } + + QDialog::done(r); +} + + +NetplayStartClientDialog::NetplayStartClientDialog(QWidget* parent) : QDialog(parent), ui(new Ui::NetplayStartClientDialog) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + + ui->txtPort->setText("8064"); +} + +NetplayStartClientDialog::~NetplayStartClientDialog() +{ + delete ui; +} + +void NetplayStartClientDialog::done(int r) +{ + if (r == QDialog::Accepted) + { + std::string player = ui->txtPlayerName->text().toStdString(); + std::string host = ui->txtIPAddress->text().toStdString(); + int port = ui->txtPort->text().toInt(); + + // TODO validate input!! + + netplayDlg = NetplayDialog::openDlg(parentWidget()); + + Netplay::StartClient(player.c_str(), host.c_str(), port); + } + + QDialog::done(r); +} + + +NetplayDialog::NetplayDialog(QWidget* parent) : QDialog(parent), ui(new Ui::NetplayDialog) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + + QStandardItemModel* model = new QStandardItemModel(); + ui->tvPlayerList->setModel(model); + + connect(this, &NetplayDialog::sgUpdatePlayerList, this, &NetplayDialog::doUpdatePlayerList); +} + +NetplayDialog::~NetplayDialog() +{ + delete ui; +} + +void NetplayDialog::done(int r) +{ + // ??? + + QDialog::done(r); +} + +void NetplayDialog::updatePlayerList(Netplay::Player* players, int num) +{ + emit sgUpdatePlayerList(players, num); +} + +void NetplayDialog::doUpdatePlayerList(Netplay::Player* players, int num) +{ + QStandardItemModel* model = (QStandardItemModel*)ui->tvPlayerList->model(); + + model->clear(); + model->setRowCount(num); + + // TODO: remove IP column in final product + + const QStringList header = {"#", "Player", "Status", "Ping", "IP"}; + model->setHorizontalHeaderLabels(header); + + for (int i = 0; i < num; i++) + { + Netplay::Player* player = &players[i]; + + QString id = QString("%0").arg(player->ID+1); + model->setItem(i, 0, new QStandardItem(id)); + + QString name = player->Name; + model->setItem(i, 1, new QStandardItem(name)); + + QString status; + switch (player->Status) + { + case 1: status = ""; break; + case 2: status = "Host"; break; + default: status = "ded"; break; + } + model->setItem(i, 2, new QStandardItem(status)); + + // TODO: ping + model->setItem(i, 3, new QStandardItem("x")); + + char ip[32]; + u32 addr = player->Address; + sprintf(ip, "%d.%d.%d.%d", addr&0xFF, (addr>>8)&0xFF, (addr>>16)&0xFF, addr>>24); + model->setItem(i, 4, new QStandardItem(ip)); + } +} + + +namespace Netplay +{ + +bool Active; +bool IsHost; +bool IsMirror; + +ENetHost* Host; +ENetHost* MirrorHost; + +Player Players[16]; +int NumPlayers; + +Player MyPlayer; +u32 HostAddress; +bool Lag; + +int NumMirrorClients; + +struct InputFrame +{ + u32 FrameNum; + u32 KeyMask; + u32 Touching; + u32 TouchX, TouchY; +}; + +std::queue InputQueue; + +enum +{ + Blob_CartROM = 0, + Blob_CartSRAM, + Blob_InitState, + + Blob_MAX +}; + +const u32 kChunkSize = 0x10000; +u8 ChunkBuffer[0x10 + kChunkSize]; +u8* Blobs[Blob_MAX]; +u32 BlobLens[Blob_MAX]; +int CurBlobType; +u32 CurBlobLen; + + +bool Init() +{ + Active = false; + IsHost = false; + IsMirror = false; + Host = nullptr; + MirrorHost = nullptr; + Lag = false; + + memset(Players, 0, sizeof(Players)); + NumPlayers = 0; + + NumMirrorClients = 0; + + for (int i = 0; i < Blob_MAX; i++) + { + Blobs[i] = nullptr; + BlobLens[i] = 0; + } + CurBlobType = -1; + CurBlobLen = 0; + + /*if (enet_initialize() != 0) + { + printf("enet shat itself :(\n"); + return false; + } + + printf("enet init OK\n");*/ + return true; +} + +void DeInit() +{ + // TODO: cleanup resources properly!! + + //enet_deinitialize(); +} + + +void StartHost(const char* playername, int port) +{ + ENetAddress addr; + addr.host = ENET_HOST_ANY; + addr.port = port; + + Host = enet_host_create(&addr, 16, 1, 0, 0); + if (!Host) + { + printf("host shat itself :(\n"); + return; + } + + Player* player = &Players[0]; + memset(player, 0, sizeof(Player)); + player->ID = 0; + strncpy(player->Name, playername, 31); + player->Status = 2; + player->Address = 0x0100007F; + NumPlayers = 1; + memcpy(&MyPlayer, player, sizeof(Player)); + + HostAddress = 0x0100007F; + + NumMirrorClients = 0; + + ENetAddress mirroraddr; + mirroraddr.host = ENET_HOST_ANY; + mirroraddr.port = port + 1; +printf("host mirror host connecting to %08X:%d\n", mirroraddr.host, mirroraddr.port); + MirrorHost = enet_host_create(&mirroraddr, 16, 2, 0, 0); + if (!MirrorHost) + { + printf("mirror host shat itself :(\n"); + return; + } + + Active = true; + IsHost = true; + IsMirror = false; + + netplayDlg->updatePlayerList(Players, NumPlayers); +} + +void StartClient(const char* playername, const char* host, int port) +{ + Host = enet_host_create(nullptr, 1, 1, 0, 0); + if (!Host) + { + printf("client shat itself :(\n"); + return; + } + + printf("client created, connecting (%s, %s:%d)\n", playername, host, port); + + ENetAddress addr; + enet_address_set_host(&addr, host); + addr.port = port; + ENetPeer* peer = enet_host_connect(Host, &addr, 1, 0); + if (!peer) + { + printf("connect shat itself :(\n"); + return; + } + + ENetEvent event; + bool conn = false; + if (enet_host_service(Host, &event, 5000) > 0) + { + if (event.type == ENET_EVENT_TYPE_CONNECT) + { + printf("connected!\n"); + conn = true; + } + } + + if (!conn) + { + printf("connection failed\n"); + enet_peer_reset(peer); + return; + } + + Player* player = &MyPlayer; + memset(player, 0, sizeof(Player)); + player->ID = 0; + strncpy(player->Name, playername, 31); + player->Status = 3; + + HostAddress = addr.host; + + Active = true; + IsHost = false; + IsMirror = false; +} + +void StartMirror(const Player* player) +{ + for (int i = 0; i < Blob_MAX; i++) + { + Blobs[i] = nullptr; + BlobLens[i] = 0; + } + CurBlobType = -1; + CurBlobLen = 0; + + MirrorHost = enet_host_create(nullptr, 1, 2, 0, 0); + if (!MirrorHost) + { + printf("mirror shat itself :(\n"); + return; + } + + printf("mirror created, connecting\n"); + + ENetAddress addr; + addr.host = player->Address; + addr.port = 8064+1 + player->ID; // FIXME!!!!!!!!!! + printf("mirror client connecting to %08X:%d\n", addr.host, addr.port); + ENetPeer* peer = enet_host_connect(MirrorHost, &addr, 2, 0); + if (!peer) + { + printf("connect shat itself :(\n"); + return; + } + + ENetEvent event; + bool conn = false; + if (enet_host_service(MirrorHost, &event, 5000) > 0) + { + if (event.type == ENET_EVENT_TYPE_CONNECT) + { + printf("connected!\n"); + conn = true; + } + } + + if (!conn) + { + printf("connection failed\n"); + enet_peer_reset(peer); + return; + } + + memcpy(&MyPlayer, player, sizeof(Player)); + + HostAddress = addr.host; + + Active = true; + IsHost = false; + IsMirror = true; +} + + +u32 PlayerAddress(int id) +{ + if (id < 0 || id > 16) return 0; + + u32 ret = Players[id].Address; + if (ret == 0x0100007F) ret = HostAddress; + return ret; +} + + +bool SpawnMirrorInstance(Player player) +{ + u16 curmask = IPC::GetInstanceBitmask(); + + QProcess newinst; + newinst.setProgram(QApplication::applicationFilePath()); + newinst.setArguments(QApplication::arguments().mid(1, QApplication::arguments().length()-1)); + +#ifdef __WIN32__ + newinst.setCreateProcessArgumentsModifier([] (QProcess::CreateProcessArguments *args) + { + args->flags |= CREATE_NEW_CONSOLE; + }); +#endif + + if (!newinst.startDetached()) + return false; + + // try to determine the ID of the new instance + + int newid = -1; + for (int tries = 0; tries < 10; tries++) + { + QThread::usleep(100 * 1000); + + u16 newmask = IPC::GetInstanceBitmask(); + if (newmask == curmask) continue; + + newmask &= ~curmask; + for (int id = 0; id < 16; id++) + { + if (newmask & (1 << id)) + { + newid = id; + break; + } + } + } + + if (newid == -1) return false; + + // setup that instance + printf("netplay: spawned mirror instance for player %d with ID %d, configuring\n", player.ID, newid); + + //std::string rompath = ROMManager::FullROMPath.join('|').toStdString(); + //IPC::SendCommandStr(1< 0) + { + buf[0] = 0x02; + *(u32*)&buf[12] = 0; + + for (u32 pos = 0; pos < len; pos += kChunkSize) + { + u32 chunklen = kChunkSize; + if ((pos + chunklen) > len) + chunklen = len - pos; + + *(u32*)&buf[8] = pos; + memcpy(&buf[16], &data[pos], chunklen); + + ENetPacket* pkt = enet_packet_create(buf, 16+chunklen, ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(MirrorHost, 1, pkt); + //enet_host_flush(MirrorHost); + } + } + + buf[0] = 0x03; + + pkt = enet_packet_create(buf, 8, ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(MirrorHost, 1, pkt); + + return true; +} + +void RecvBlobFromMirrorHost(ENetPeer* peer, ENetPacket* pkt) +{ + u8* buf = pkt->data; + if (buf[0] == 0x01) + { + if (CurBlobType != -1) return; + if (pkt->dataLength != 8) return; + + int type = buf[1]; + if (type > Blob_MAX) return; + + u32 len = *(u32*)&buf[4]; + if (len > 0x40000000) return; + + if (Blobs[type] != nullptr) return; + if (BlobLens[type] != 0) return; +printf("[MC] start blob type=%d len=%d\n", type, len); + if (len) Blobs[type] = new u8[len]; + BlobLens[type] = len; + + CurBlobType = type; + CurBlobLen = len; + + ENetEvent evt; + while (enet_host_service(MirrorHost, &evt, 5000) > 0) + { + if (evt.type == ENET_EVENT_TYPE_RECEIVE && evt.channelID == 1) + { + RecvBlobFromMirrorHost(evt.peer, evt.packet); + if (evt.packet->dataLength >= 1 && evt.packet->data[0] == 0x03) + { + printf("[MC] blob done while in fast recv loop\n"); + break; + } + } + else + { + printf("[MC] fast recv loop aborted because evt %d ch %d\n", evt.type, evt.channelID); + break; + } + } + } + else if (buf[0] == 0x02) + { + if (CurBlobType < 0 || CurBlobType > Blob_MAX) return; + if (pkt->dataLength > (16+kChunkSize)) return; + + int type = buf[1]; + if (type != CurBlobType) return; + + u32 len = *(u32*)&buf[4]; + if (len != CurBlobLen) return; + + u32 pos = *(u32*)&buf[8]; + if (pos >= len) return; + if ((pos + (pkt->dataLength-16)) > len) return; + + u8* dst = Blobs[type]; + if (!dst) return; + if (BlobLens[type] != len) return; +printf("[MC] recv blob data, type=%d pos=%08X len=%08X data=%08X\n", type, pos, len, pkt->dataLength-16); + memcpy(&dst[pos], &buf[16], pkt->dataLength-16); + } + else if (buf[0] == 0x03) + { + if (CurBlobType < 0 || CurBlobType > Blob_MAX) return; + if (pkt->dataLength != 8) return; + + int type = buf[1]; + if (type != CurBlobType) return; + + u32 len = *(u32*)&buf[4]; + if (len != CurBlobLen) return; +printf("[MC] finish blob type=%d len=%d\n", type, len); + CurBlobType = -1; + CurBlobLen = 0; + } + else if (buf[0] == 0x04) + { + if (pkt->dataLength != 2) return; + + bool res = false; + + // reset + NDS::SetConsoleType(buf[1]); + NDS::EjectCart(); + NDS::Reset(); + //SetBatteryLevels(); + + if (Blobs[Blob_CartROM]) + { + res = NDS::LoadCart(Blobs[Blob_CartROM], BlobLens[Blob_CartROM], + Blobs[Blob_CartSRAM], BlobLens[Blob_CartSRAM]); + if (!res) + { + printf("!!!! FAIL!!\n"); + return; + } + } + + if (res) + { + ROMManager::CartType = 0; + //ROMManager::NDSSave = new SaveManager(savname); + + //LoadCheats(); + } + + // load initial state + // TODO: terrible hack!! + #if 0 + FILE* f = Platform::OpenFile("netplay2.mln", "wb"); + fwrite(Blobs[Blob_InitState], BlobLens[Blob_InitState], 1, f); + fclose(f); + Savestate* state = new Savestate("netplay2.mln", false); + NDS::DoSavestate(state); + delete state; + + for (int i = 0; i < Blob_MAX; i++) + { + if (Blobs[i]) delete[] Blobs[i]; + Blobs[i] = nullptr; + BlobLens[i] = 0; + } + + /*Savestate* zorp = new Savestate("netplay3.mln", true); + NDS::DoSavestate(zorp); + delete zorp;*/ + +printf("[MC] state loaded, PC=%08X/%08X\n", NDS::GetPC(0), NDS::GetPC(1)); + ENetPacket* resp = enet_packet_create(buf, 1, ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(peer, 1, resp); + #endif + } + else if (buf[0] == 0x05) + { + printf("[MIRROR CLIENT] start\n"); + StartLocal(); + } +} + +void SyncMirrorClients() +{ + printf("[MIRROR HOST] syncing clients\n"); + + //SendBlobToMirrorClients(Blob_CartROM, NDSCart::CartROMSize, NDSCart::CartROM); + SendBlobToMirrorClients(Blob_CartSRAM, NDSCart::GetSaveMemoryLength(), NDSCart::GetSaveMemory()); + + // send initial state + // TODO: this is a terrible hack! + /*printf("[MH] state start\n"); + Savestate* state = new Savestate("netplay.mln", true); + NDS::DoSavestate(state); + delete state; + printf("[MH] state taken: PC=%08X/%08X\n", NDS::GetPC(0), NDS::GetPC(1)); + FILE* f = Platform::OpenLocalFile("netplay.mln", "rb"); + printf("[MH] state=%d\n", f?1:0); + fseek(f, 0, SEEK_END); + u32 flen = ftell(f); + fseek(f, 0, SEEK_SET); + u8* statebuf = new u8[flen]; + fread(statebuf, flen, 1, f); + fclose(f); + printf("[MH] state read, len=%d\n", flen); + SendBlobToMirrorClients(Blob_InitState, flen, statebuf); + printf("[MH] state sent\n"); + delete[] statebuf;*/ + + u8 data[2]; + data[0] = 0x04; + data[1] = (u8)Config::ConsoleType; + ENetPacket* pkt = enet_packet_create(&data, 2, ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(MirrorHost, 1, pkt); + //enet_host_flush(MirrorHost); + + // wait for all clients to have caught up + int ngood = 0; + ENetEvent evt; + while (enet_host_service(MirrorHost, &evt, 300000) > 0) + {printf("EVENT %d CH %d\n", evt.type, evt.channelID); + if (evt.type == ENET_EVENT_TYPE_RECEIVE && evt.channelID == 1) + { + if (evt.packet->dataLength == 1 && evt.packet->data[0] == 0x04) + ngood++; + } + else + break; + + if (ngood >= (NumPlayers-1)) + break; + } + + if (ngood != (NumPlayers-1)) + printf("!!! BAD!! %d %d\n", ngood, NumPlayers); + + printf("[MIRROR HOST] clients synced\n"); + + // start + + data[0] = 0x05; + pkt = enet_packet_create(&data, 1, ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(MirrorHost, 1, pkt); + //enet_host_flush(MirrorHost); + + StartLocal(); +} + +void StartGame() +{ + if (!IsHost) + { + printf("?????\n"); + return; + } + + // spawn mirror instances as needed + for (int i = 1; i < NumPlayers; i++) + { + SpawnMirrorInstance(Players[i]); + } + + //SyncMirrorClients(); + + // tell remote peers to start game + u8 cmd[1] = {0x04}; + ENetPacket* pkt = enet_packet_create(cmd, sizeof(cmd), ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(Host, 0, pkt); + + // tell other mirror instances to start the game + //IPC::SendCommand(0xFFFF, IPC::Cmd_Start, 0, nullptr); + + // TO START MIRROR CLIENT SHITO + // + // 1. NDS::Reset() + // 2. load ROM + // 3. load state + + // start game locally + //StartLocal(); +} + +void StartLocal() +{ + for (int i = 0; i < 4; i++) + { + InputFrame frame; + frame.FrameNum = i; + frame.KeyMask = 0xFFF; + frame.Touching = 0; + frame.TouchX = 0; + frame.TouchY = 0; + InputQueue.push(frame); + } + + NDS::Start(); + emuThread->emuRun(); +} + + +void ProcessHost() +{ + if (!Host) return; + + ENetEvent event; + while (enet_host_service(Host, &event, 0) > 0) + { + switch (event.type) + { + case ENET_EVENT_TYPE_CONNECT: + { + // client connected; assign player number + + int id; + for (id = 0; id < 16; id++) + { + if (id >= NumPlayers) break; + if (Players[id].Status == 0) break; + } + + if (id < 16) + { + u8 cmd[2]; + cmd[0] = 0x01; + cmd[1] = (u8)id; + ENetPacket* pkt = enet_packet_create(cmd, 2, ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(event.peer, 0, pkt); + + Players[id].ID = id; + Players[id].Status = 3; + Players[id].Address = event.peer->address.host; + event.peer->data = &Players[id]; + NumPlayers++; + } + } + break; + + case ENET_EVENT_TYPE_DISCONNECT: + { + // TODO + printf("disco\n"); + } + break; + + case ENET_EVENT_TYPE_RECEIVE: + { + if (event.packet->dataLength < 1) break; + + u8* data = (u8*)event.packet->data; + switch (data[0]) + { + case 0x02: // client sending player info + { + if (event.packet->dataLength != (1+sizeof(Player))) break; + + Player player; + memcpy(&player, &data[1], sizeof(Player)); + player.Name[31] = '\0'; + + Player* hostside = (Player*)event.peer->data; + if (player.ID != hostside->ID) + { + printf("what??? %d =/= %d\n", player.ID, hostside->ID); + // TODO: disconnect + break; + } + + player.Status = 1; + player.Address = event.peer->address.host; + memcpy(hostside, &player, sizeof(Player)); + + // broadcast updated player list + u8 cmd[2+sizeof(Players)]; + cmd[0] = 0x03; + cmd[1] = (u8)NumPlayers; + memcpy(&cmd[2], Players, sizeof(Players)); + ENetPacket* pkt = enet_packet_create(cmd, 2+sizeof(Players), ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(Host, 0, pkt); + + netplayDlg->updatePlayerList(Players, NumPlayers); + } + break; + } + } + break; + } + } +} + +void ProcessClient() +{ + if (!Host) return; + + ENetEvent event; + while (enet_host_service(Host, &event, 0) > 0) + { + switch (event.type) + { + case ENET_EVENT_TYPE_CONNECT: + printf("schmo\n"); + break; + + case ENET_EVENT_TYPE_DISCONNECT: + { + // TODO + printf("shma\n"); + } + break; + + case ENET_EVENT_TYPE_RECEIVE: + { + if (event.packet->dataLength < 1) break; + + u8* data = (u8*)event.packet->data; + switch (data[0]) + { + case 0x01: // host sending player ID + { + if (event.packet->dataLength != 2) break; + + NumMirrorClients = 0; + + // create mirror host + ENetAddress mirroraddr; + mirroraddr.host = ENET_HOST_ANY; + mirroraddr.port = 8064+1 + data[1]; // FIXME!!!! +printf("client mirror host connecting to %08X:%d\n", mirroraddr.host, mirroraddr.port); + MirrorHost = enet_host_create(&mirroraddr, 16, 2, 0, 0); + if (!MirrorHost) + { + printf("mirror host shat itself :(\n"); + break; + } + + // send player information + MyPlayer.ID = data[1]; + u8 cmd[1+sizeof(Player)]; + cmd[0] = 0x02; + memcpy(&cmd[1], &MyPlayer, sizeof(Player)); + ENetPacket* pkt = enet_packet_create(cmd, 1+sizeof(Player), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(event.peer, 0, pkt); + } + break; + + case 0x03: // host sending player list + { + if (event.packet->dataLength != (2+sizeof(Players))) break; + if (data[1] > 16) break; + + NumPlayers = data[1]; + memcpy(Players, &data[2], sizeof(Players)); + for (int i = 0; i < 16; i++) + { + Players[i].Name[31] = '\0'; + } + + netplayDlg->updatePlayerList(Players, NumPlayers); + } + break; + + case 0x04: // start game + { + // spawn mirror instances as needed + for (int i = 0; i < NumPlayers; i++) + { + if (i != MyPlayer.ID) + SpawnMirrorInstance(Players[i]); + } + + //SyncMirrorClients(); +printf("bourf\n"); + // tell other mirror instances to start the game + //IPC::SendCommand(0xFFFF, IPC::Cmd_Start, 0, nullptr); +printf("birf\n"); + // start game locally + //StartLocal(); + } + break; + } + } + break; + } + } +} + +void ProcessMirrorHost() +{ + if (!MirrorHost) return; + + bool block = false; + ENetEvent event; + while (enet_host_service(MirrorHost, &event, block ? 5000 : 0) > 0) + { + switch (event.type) + { + case ENET_EVENT_TYPE_CONNECT: + printf("[MIRROR HOST] mirror client connected\n"); + NumMirrorClients++; + event.peer->data = (void*)0; + + if (NumMirrorClients >= NumPlayers) + { + printf("??????\n"); + } + else if (NumMirrorClients == (NumPlayers-1)) + { + // all mirror clients are connected, we're ready to go + SyncMirrorClients(); + //StartLocal(); + } + break; + + case ENET_EVENT_TYPE_DISCONNECT: + { + // TODO + printf("[MIRROR HOST] mirror client disconnected\n"); + NumMirrorClients--; + } + break; + + case ENET_EVENT_TYPE_RECEIVE: + if (event.channelID == 0) + { + if (event.packet->dataLength != 4) break; + /*u8* data = (u8*)event.packet->data; + + if (data[0]) + { + event.peer->data = (void*)1; + block = true; + } + else + { + event.peer->data = (void*)0; + block = false; + + for (int i = 0; i < MirrorHost->peerCount; i++) + { + ENetPeer* peer = &(MirrorHost->peers[i]); + if (peer->state != ENET_PEER_STATE_CONNECTED) continue; + if (peer->data != (void*)0) + { + block = true; + break; + } + } + }*/ + s32 clientframes = *(s32*)event.packet->data; +//printf("[SYNC] HOST=%d CLIENT=%d\n", NDS::NumFrames, clientframes); + if (clientframes < (((s32)NDS::NumFrames) - 16)) + { + event.peer->data = (void*)1; + block = true; + } + else + { + event.peer->data = (void*)0; + block = false; + + for (int i = 0; i < MirrorHost->peerCount; i++) + { + ENetPeer* peer = &(MirrorHost->peers[i]); + if (peer->state != ENET_PEER_STATE_CONNECTED) continue; + if (peer->data != (void*)0) + { + block = true; + break; + } + } + } + } + break; + } + } +} + +void ProcessMirrorClient() +{ + if (!MirrorHost) return; + + bool block = false; + if (emuThread->emuIsRunning())// && NDS::NumFrames > 4) + { + if (InputQueue.empty()) + block = true; + } + + ENetEvent event; + while (enet_host_service(MirrorHost, &event, block ? 5000 : 0) > 0) + { + switch (event.type) + { + case ENET_EVENT_TYPE_CONNECT: + printf("schmu\n"); + Lag = false; + break; + + case ENET_EVENT_TYPE_DISCONNECT: + { + // TODO + printf("shmz\n"); + } + break; + + case ENET_EVENT_TYPE_RECEIVE://printf("RX %d %d\n", event.channelID, event.packet->dataLength); + if (event.channelID == 0) + { + if (event.packet->dataLength != sizeof(InputFrame)) break; + + u8* data = (u8*)event.packet->data; + InputFrame frame; + memcpy(&frame, data, sizeof(InputFrame)); + InputQueue.push(frame); + + /*bool lag = (InputQueue.size() > 4*2); + if (lag != Lag) + { + // let the mirror host know they are running too fast for us +printf("mirror client lag notify: %d\n", lag); + u8 data = lag ? 1 : 0; + ENetPacket* pkt = enet_packet_create(&data, 1, ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(event.peer, 0, pkt); + + Lag = lag; + }*/ + { + ENetPacket* pkt = enet_packet_create(&NDS::NumFrames, 4, ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(event.peer, 0, pkt); + //enet_host_flush(MirrorHost); + } + } + else if (event.channelID == 1) + { + RecvBlobFromMirrorHost(event.peer, event.packet); + } + break; + } + + if (block) break; + } +} + +void ProcessFrame() +{ + if (IsMirror) + { + ProcessMirrorClient(); + } + else + { + if (IsHost) + { + ProcessHost(); + } + else + { + ProcessClient(); + } + + ProcessMirrorHost(); + } +} + +void ProcessInput() +{ + // netplay input processing + // + // N = current frame # + // L = amount of lag frames + // + // host side: + // we take the current input (normally meant for frame N) + // and delay it to frame N+L + // + // client side: + // we receive input from the host + // apply each input to the frame it's assigned to + // before running a frame, we need to wait to have received input for it + // TODO: alert host if we are running too far behind + + if (!IsMirror) + { + u32 lag = 4; // TODO: make configurable!! + + InputFrame frame; + frame.FrameNum = NDS::NumFrames + lag; + frame.KeyMask = Input::InputMask; + frame.Touching = Input::Touching ? 1:0; + frame.TouchX = Input::TouchX; + frame.TouchY = Input::TouchY; + // TODO: other shit! (some hotkeys for example?) + + InputQueue.push(frame); + + u8 cmd[sizeof(InputFrame)]; + memcpy(cmd, &frame, sizeof(InputFrame)); + ENetPacket* pkt = enet_packet_create(cmd, sizeof(cmd), ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(MirrorHost, 0, pkt); + //enet_host_flush(MirrorHost); + } + + if (InputQueue.empty()) + { + //if (NDS::NumFrames > 4) + printf("Netplay: BAD! INPUT QUEUE EMPTY\n"); + return; + } + + InputFrame& frame = InputQueue.front(); + + if (frame.FrameNum < NDS::NumFrames) + { + // TODO: this situation is a desync + printf("Netplay: BAD! LAGGING BEHIND\n"); + while (frame.FrameNum < NDS::NumFrames) + { + if (InputQueue.size() < 2) break; + InputQueue.pop(); + frame = InputQueue.front(); + } + } + + if (frame.FrameNum > NDS::NumFrames) + { + // frame in the future, ignore + return; + } + + // apply this input frame + if (frame.KeyMask != 0xFFF) printf("[%08d] INPUT=%08X (%08d) (backlog=%d)\n", NDS::NumFrames, frame.KeyMask, frame.FrameNum, InputQueue.size()); + NDS::SetKeyMask(frame.KeyMask); + if (frame.Touching) NDS::TouchScreen(frame.TouchX, frame.TouchY); + else NDS::ReleaseScreen(); + + InputQueue.pop(); +} + +} diff --git a/src/frontend/qt_sdl/Netplay.h b/src/frontend/qt_sdl/Netplay.h new file mode 100644 index 00000000..c18eb635 --- /dev/null +++ b/src/frontend/qt_sdl/Netplay.h @@ -0,0 +1,147 @@ +/* + Copyright 2016-2022 melonDS team + + This file is part of melonDS. + + melonDS 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 3 of the License, or (at your option) + any later version. + + melonDS 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 melonDS. If not, see http://www.gnu.org/licenses/. +*/ + +#ifndef NETPLAY_H +#define NETPLAY_H + +#include + +#include "types.h" + +namespace Ui +{ +class NetplayStartHostDialog; +class NetplayStartClientDialog; +class NetplayDialog; +} + +class NetplayStartHostDialog; +class NetplayStartClientDialog; +class NetplayDialog; + +namespace Netplay +{ + +struct Player +{ + int ID; + char Name[32]; + int Status; // 0=no player 1=normal 2=host 3=connecting + u32 Address; +}; + +} + +class NetplayStartHostDialog : public QDialog +{ + Q_OBJECT + +public: + explicit NetplayStartHostDialog(QWidget* parent); + ~NetplayStartHostDialog(); + + static NetplayStartHostDialog* openDlg(QWidget* parent) + { + NetplayStartHostDialog* dlg = new NetplayStartHostDialog(parent); + dlg->open(); + return dlg; + } + +private slots: + void done(int r); + +private: + Ui::NetplayStartHostDialog* ui; +}; + +class NetplayStartClientDialog : public QDialog +{ + Q_OBJECT + +public: + explicit NetplayStartClientDialog(QWidget* parent); + ~NetplayStartClientDialog(); + + static NetplayStartClientDialog* openDlg(QWidget* parent) + { + NetplayStartClientDialog* dlg = new NetplayStartClientDialog(parent); + dlg->open(); + return dlg; + } + +private slots: + void done(int r); + +private: + Ui::NetplayStartClientDialog* ui; +}; + +class NetplayDialog : public QDialog +{ + Q_OBJECT + +public: + explicit NetplayDialog(QWidget* parent); + ~NetplayDialog(); + + static NetplayDialog* openDlg(QWidget* parent) + { + NetplayDialog* dlg = new NetplayDialog(parent); + dlg->show(); + return dlg; + } + + void updatePlayerList(Netplay::Player* players, int num); + +signals: + void sgUpdatePlayerList(Netplay::Player* players, int num); + +private slots: + void done(int r); + + void doUpdatePlayerList(Netplay::Player* players, int num); + +private: + Ui::NetplayDialog* ui; +}; + +namespace Netplay +{ + +extern bool Active; + +bool Init(); +void DeInit(); + +void StartHost(const char* player, int port); +void StartClient(const char* player, const char* host, int port); +void StartMirror(const Player* player); + +u32 PlayerAddress(int id); + +void StartGame(); +void StartLocal(); + +void StartGame(); + +void ProcessFrame(); +void ProcessInput(); + +} + +#endif // NETPLAY_H diff --git a/src/frontend/qt_sdl/NetplayDialog.ui b/src/frontend/qt_sdl/NetplayDialog.ui new file mode 100644 index 00000000..86b51324 --- /dev/null +++ b/src/frontend/qt_sdl/NetplayDialog.ui @@ -0,0 +1,31 @@ + + + NetplayDialog + + + + 0 + 0 + 522 + 391 + + + + NETPLAY SHITO + + + + + + STATUS PLACEHOLDER + + + + + + + + + + + diff --git a/src/frontend/qt_sdl/NetplayStartClientDialog.ui b/src/frontend/qt_sdl/NetplayStartClientDialog.ui new file mode 100644 index 00000000..df5b4ea7 --- /dev/null +++ b/src/frontend/qt_sdl/NetplayStartClientDialog.ui @@ -0,0 +1,107 @@ + + + NetplayStartClientDialog + + + + 0 + 0 + 400 + 229 + + + + + 0 + 0 + + + + NETPLAY CLIENT + + + + QLayout::SetFixedSize + + + + + + + Player name: + + + + + + + Host port: + + + + + + + + + + + + + Host address: + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + NetplayStartClientDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + NetplayStartClientDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/frontend/qt_sdl/NetplayStartHostDialog.ui b/src/frontend/qt_sdl/NetplayStartHostDialog.ui new file mode 100644 index 00000000..f704e743 --- /dev/null +++ b/src/frontend/qt_sdl/NetplayStartHostDialog.ui @@ -0,0 +1,97 @@ + + + NetplayStartHostDialog + + + + 0 + 0 + 400 + 229 + + + + + 0 + 0 + + + + NETPLAY HOST + + + + QLayout::SetFixedSize + + + + + + + Player name: + + + + + + + Port: + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + NetplayStartHostDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + NetplayStartHostDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +