diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml index 0102d912..fc0857e4 100644 --- a/.github/workflows/build-ubuntu.yml +++ b/.github/workflows/build-ubuntu.yml @@ -20,7 +20,7 @@ jobs: run: | sudo rm -f /etc/apt/sources.list.d/dotnetdev.list /etc/apt/sources.list.d/microsoft-prod.list sudo apt update - sudo apt install --allow-downgrades cmake ninja-build extra-cmake-modules libpcap0.8-dev libsdl2-dev \ + sudo apt install --allow-downgrades cmake ninja-build extra-cmake-modules libpcap0.8-dev libsdl2-dev libenet-dev \ qt6-{base,base-private,multimedia}-dev libarchive-dev libzstd-dev libfuse2 - name: Configure run: cmake -B build -G Ninja -DUSE_QT6=ON -DCMAKE_INSTALL_PREFIX=/usr @@ -63,7 +63,7 @@ jobs: apt update apt -y full-upgrade apt -y install git {gcc-12,g++-12}-aarch64-linux-gnu cmake ninja-build extra-cmake-modules \ - {libsdl2,qt6-{base,base-private,multimedia},libarchive,libzstd}-dev:arm64 \ + {libsdl2,qt6-{base,base-private,multimedia},libarchive,libzstd,libenet}-dev:arm64 \ pkg-config dpkg-dev - name: Check out source uses: actions/checkout@v4 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/flake.lock b/flake.lock index bd799223..6e99d27f 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1722813957, - "narHash": "sha256-IAoYyYnED7P8zrBFMnmp7ydaJfwTnwcnqxUElC1I26Y=", + "lastModified": 1723175592, + "narHash": "sha256-M0xJ3FbDUc4fRZ84dPGx5VvgFsOzds77KiBMW/mMTnI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cb9a96f23c491c081b38eab96d22fa958043c9fa", + "rev": "5e0ca22929f3342b19569b21b2f3462f053e497b", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 5074a7fb..961ef245 100644 --- a/flake.nix +++ b/flake.nix @@ -37,6 +37,7 @@ libarchive libGL libslirp + enet ]) ++ optionals isLinux [ pkgs.wayland pkgs.kdePackages.qtwayland diff --git a/src/Platform.h b/src/Platform.h index 90791100..bef66593 100644 --- a/src/Platform.h +++ b/src/Platform.h @@ -270,6 +270,9 @@ bool Mutex_TryLock(Mutex* mutex); void Sleep(u64 usecs); +u64 GetMSCount(); +u64 GetUSCount(); + // functions called when the NDS or GBA save files need to be written back to storage // savedata and savelen are always the entire save memory buffer and its full length diff --git a/src/frontend/qt_sdl/CMakeLists.txt b/src/frontend/qt_sdl/CMakeLists.txt index fc7819a1..656b6535 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 + + LANDialog.cpp + NetplayDialog.cpp ) if (APPLE) diff --git a/src/frontend/qt_sdl/EmuInstance.cpp b/src/frontend/qt_sdl/EmuInstance.cpp index 188d42f7..b29d2b09 100644 --- a/src/frontend/qt_sdl/EmuInstance.cpp +++ b/src/frontend/qt_sdl/EmuInstance.cpp @@ -39,7 +39,7 @@ #include "Config.h" #include "Platform.h" #include "Net.h" -#include "LocalMP.h" +#include "MPInterface.h" #include "NDS.h" #include "DSi.h" @@ -62,11 +62,11 @@ using namespace melonDS::Platform; MainWindow* topWindow = nullptr; const string kWifiSettingsPath = "wfcsettings.bin"; -extern LocalMP localMp; extern Net net; -EmuInstance::EmuInstance(int inst) : instanceID(inst), +EmuInstance::EmuInstance(int inst) : deleting(false), + instanceID(inst), globalCfg(Config::GetGlobalTable()), localCfg(Config::GetLocalTable(inst)) { @@ -117,8 +117,10 @@ EmuInstance::EmuInstance(int inst) : instanceID(inst), EmuInstance::~EmuInstance() { - // TODO window cleanup and shit? - localMp.End(instanceID); + deleting = true; + deleteAllWindows(); + + MPInterface::Get().End(instanceID); emuThread->emuExit(); emuThread->wait(); @@ -168,6 +170,44 @@ void EmuInstance::createWindow() emuThread->attachWindow(win); } +void EmuInstance::deleteWindow(int id, bool close) +{ + if (id >= kMaxWindows) return; + + MainWindow* win = windowList[id]; + if (!win) return; + + if (win->hasOpenGL() && win == mainWindow) + { + // we intentionally don't unpause here + emuThread->emuPause(); + emuThread->deinitContext(); + } + + emuThread->detachWindow(win); + + windowList[id] = nullptr; + numWindows--; + + if (topWindow == win) topWindow = nullptr; + if (mainWindow == win) mainWindow = nullptr; + + if (close) + win->close(); + + if ((!mainWindow) && (!deleting)) + { + // if we closed this instance's main window, delete the instance + deleteEmuInstance(instanceID); + } +} + +void EmuInstance::deleteAllWindows() +{ + for (int i = kMaxWindows-1; i >= 0; i--) + deleteWindow(i, true); +} + void EmuInstance::osdAddMessage(unsigned int color, const char* fmt, ...) { diff --git a/src/frontend/qt_sdl/EmuInstance.h b/src/frontend/qt_sdl/EmuInstance.h index 34290795..39c187c2 100644 --- a/src/frontend/qt_sdl/EmuInstance.h +++ b/src/frontend/qt_sdl/EmuInstance.h @@ -91,6 +91,8 @@ public: std::string instanceFileSuffix(); void createWindow(); + void deleteWindow(int id, bool close); + void deleteAllWindows(); void osdAddMessage(unsigned int color, const char* fmt, ...); @@ -217,6 +219,8 @@ private: bool hotkeyPressed(int id) { return hotkeyPress & (1<inputProcess(); if (emuInstance->hotkeyPressed(HK_FastForwardToggle)) emit windowLimitFPSChange(); diff --git a/src/frontend/qt_sdl/LANDialog.cpp b/src/frontend/qt_sdl/LANDialog.cpp new file mode 100644 index 00000000..58baf908 --- /dev/null +++ b/src/frontend/qt_sdl/LANDialog.cpp @@ -0,0 +1,405 @@ +/* + Copyright 2016-2024 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 + +#include "LANDialog.h" +#include "Config.h" +#include "main.h" +#include "LAN.h" + +#include "ui_LANStartHostDialog.h" +#include "ui_LANStartClientDialog.h" +#include "ui_LANDialog.h" + +using namespace melonDS; + + +LANStartClientDialog* lanClientDlg = nullptr; +LANDialog* lanDlg = nullptr; + +#define lan() ((LAN&)MPInterface::Get()) + + +LANStartHostDialog::LANStartHostDialog(QWidget* parent) : QDialog(parent), ui(new Ui::LANStartHostDialog) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + + setMPInterface(MPInterface_LAN); + + // 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) + { + if (ui->txtPlayerName->text().trimmed().isEmpty()) + { + QMessageBox::warning(this, "melonDS", "Please enter a player name."); + return; + } + + std::string player = ui->txtPlayerName->text().toStdString(); + int numplayers = ui->sbNumPlayers->value(); + + if (!lan().StartHost(player.c_str(), numplayers)) + { + QMessageBox::warning(this, "melonDS", "Failed to start LAN game."); + return; + } + + lanDlg = LANDialog::openDlg(parentWidget()); + } + else + { + setMPInterface(MPInterface_Local); + } + + QDialog::done(r); +} + + +LANStartClientDialog::LANStartClientDialog(QWidget* parent) : QDialog(parent), ui(new Ui::LANStartClientDialog) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + + setMPInterface(MPInterface_LAN); + + 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())); + + lanClientDlg = this; + lan().StartDiscovery(); + + timerID = startTimer(1000); +} + +LANStartClientDialog::~LANStartClientDialog() +{ + killTimer(timerID); + + 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(); + setMPInterface(MPInterface_Local); + } + + QDialog::done(r); +} + +void LANStartClientDialog::timerEvent(QTimerEvent *event) +{ + doUpdateDiscoveryList(); +} + +void LANStartClientDialog::doUpdateDiscoveryList() +{ + auto disclist = lan().GetDiscoveryList(); + + QStandardItemModel* model = (QStandardItemModel*)ui->tvAvailableGames->model(); + int curcount = model->rowCount(); + int newcount = disclist.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] : disclist) + { + 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++; + } +} + + +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); + + timerID = startTimer(1000); +} + +LANDialog::~LANDialog() +{ + killTimer(timerID); + + delete ui; +} + +void LANDialog::on_btnLeaveGame_clicked() +{ + done(QDialog::Accepted); +} + +void LANDialog::done(int r) +{ + bool showwarning = true; + if (lan().GetNumPlayers() < 2) + showwarning = false; + + if (showwarning) + { + if (QMessageBox::warning(this, "melonDS", "Really leave this LAN game?", + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) + return; + } + + lan().EndSession(); + setMPInterface(MPInterface_Local); + + QDialog::done(r); +} + +void LANDialog::timerEvent(QTimerEvent *event) +{ + doUpdatePlayerList(); +} + +void LANDialog::doUpdatePlayerList() +{ + auto playerlist = lan().GetPlayerList(); + auto maxplayers = lan().GetMaxPlayers(); + + QStandardItemModel* model = (QStandardItemModel*)ui->tvPlayerList->model(); + int curcount = model->rowCount(); + int newcount = playerlist.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()); + row.append(new QStandardItem()); + model->appendRow(row); + } + } + + int i = 0; + for (const auto& player : playerlist) + { + 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 LAN::Player_Client: + status = "Connected"; + break; + case LAN::Player_Host: + status = "Game host"; + break; + case LAN::Player_Connecting: + status = "Connecting"; + break; + case LAN::Player_Disconnected: + status = "Connection lost"; + break; + } + model->item(i, 2)->setText(status); + + if (player.IsLocalPlayer) + { + model->item(i, 3)->setText("-"); + model->item(i, 4)->setText("(local)"); + } + else + { + if (player.Status == LAN::Player_Client || + player.Status == LAN::Player_Host) + { + QString ping = QString("%0 ms").arg(player.Ping); + model->item(i, 3)->setText(ping); + } + else + { + model->item(i, 3)->setText("-"); + } + + + u32 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); + } + + i++; + } +} diff --git a/src/frontend/qt_sdl/LANDialog.h b/src/frontend/qt_sdl/LANDialog.h new file mode 100644 index 00000000..03857d79 --- /dev/null +++ b/src/frontend/qt_sdl/LANDialog.h @@ -0,0 +1,117 @@ +/* + Copyright 2016-2024 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 LANDIALOG_H +#define LANDIALOG_H + +#include +#include +#include + +#include "types.h" + +namespace Ui +{ + class LANStartHostDialog; + class LANStartClientDialog; + class LANDialog; +} + +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; + } + +protected: + void timerEvent(QTimerEvent* event) override; + +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; + int timerID; +}; + +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; + } + +protected: + void timerEvent(QTimerEvent* event) override; + +private slots: + void on_btnLeaveGame_clicked(); + void done(int r); + + void doUpdatePlayerList(); + +private: + Ui::LANDialog* ui; + int timerID; +}; + +#endif // LANDIALOG_H diff --git a/src/frontend/qt_sdl/LANDialog.ui b/src/frontend/qt_sdl/LANDialog.ui new file mode 100644 index 00000000..88e9718f --- /dev/null +++ b/src/frontend/qt_sdl/LANDialog.ui @@ -0,0 +1,51 @@ + + + LANDialog + + + + 0 + 0 + 522 + 391 + + + + LAN game - melonDS + + + + + + 0 + + + + + Leave game + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + 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/NetplayDialog.cpp b/src/frontend/qt_sdl/NetplayDialog.cpp new file mode 100644 index 00000000..e9ed6022 --- /dev/null +++ b/src/frontend/qt_sdl/NetplayDialog.cpp @@ -0,0 +1,181 @@ +/* + Copyright 2016-2024 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 "NetplayDialog.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" + +using namespace melonDS; + + +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)); + } +} diff --git a/src/frontend/qt_sdl/NetplayDialog.h b/src/frontend/qt_sdl/NetplayDialog.h new file mode 100644 index 00000000..1fa0dcf2 --- /dev/null +++ b/src/frontend/qt_sdl/NetplayDialog.h @@ -0,0 +1,111 @@ +/* + Copyright 2016-2024 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 NETPLAYDIALOG_H +#define NETPLAYDIALOG_H + +#include + +#include "types.h" +#include "Netplay.h" + +namespace Ui +{ + class NetplayStartHostDialog; + class NetplayStartClientDialog; + class NetplayDialog; +} + +class NetplayStartHostDialog; +class NetplayStartClientDialog; +class NetplayDialog; + +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; +}; + +#endif // NETPLAYDIALOG_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 + + + + + diff --git a/src/frontend/qt_sdl/Platform.cpp b/src/frontend/qt_sdl/Platform.cpp index 217f1b45..541b51f2 100644 --- a/src/frontend/qt_sdl/Platform.cpp +++ b/src/frontend/qt_sdl/Platform.cpp @@ -38,7 +38,7 @@ #include "main.h" #include "CameraManager.h" #include "Net.h" -#include "LocalMP.h" +#include "MPInterface.h" #include "SPI_Firmware.h" #ifdef __WIN32__ @@ -47,7 +47,7 @@ #endif // __WIN32__ extern CameraManager* camManager[2]; -extern melonDS::LocalMP localMp; + extern melonDS::Net net; namespace melonDS::Platform @@ -395,6 +395,16 @@ void Sleep(u64 usecs) QThread::usleep(usecs); } +u64 GetMSCount() +{ + return sysTimer.elapsed(); +} + +u64 GetUSCount() +{ + return sysTimer.nsecsElapsed() / 1000; +} + void WriteNDSSave(const u8* savedata, u32 savelen, u32 writeoffset, u32 writelen, void* userdata) { @@ -458,55 +468,55 @@ void WriteDateTime(int year, int month, int day, int hour, int minute, int secon void MP_Begin(void* userdata) { int inst = ((EmuInstance*)userdata)->getInstanceID(); - localMp.Begin(inst); + MPInterface::Get().Begin(inst); } void MP_End(void* userdata) { int inst = ((EmuInstance*)userdata)->getInstanceID(); - localMp.End(inst); + MPInterface::Get().End(inst); } int MP_SendPacket(u8* data, int len, u64 timestamp, void* userdata) { int inst = ((EmuInstance*)userdata)->getInstanceID(); - return localMp.SendPacket(inst, data, len, timestamp); + return MPInterface::Get().SendPacket(inst, data, len, timestamp); } int MP_RecvPacket(u8* data, u64* timestamp, void* userdata) { int inst = ((EmuInstance*)userdata)->getInstanceID(); - return localMp.RecvPacket(inst, data, timestamp); + return MPInterface::Get().RecvPacket(inst, data, timestamp); } int MP_SendCmd(u8* data, int len, u64 timestamp, void* userdata) { int inst = ((EmuInstance*)userdata)->getInstanceID(); - return localMp.SendCmd(inst, data, len, timestamp); + return MPInterface::Get().SendCmd(inst, data, len, timestamp); } int MP_SendReply(u8* data, int len, u64 timestamp, u16 aid, void* userdata) { int inst = ((EmuInstance*)userdata)->getInstanceID(); - return localMp.SendReply(inst, data, len, timestamp, aid); + return MPInterface::Get().SendReply(inst, data, len, timestamp, aid); } int MP_SendAck(u8* data, int len, u64 timestamp, void* userdata) { int inst = ((EmuInstance*)userdata)->getInstanceID(); - return localMp.SendAck(inst, data, len, timestamp); + return MPInterface::Get().SendAck(inst, data, len, timestamp); } int MP_RecvHostPacket(u8* data, u64* timestamp, void* userdata) { int inst = ((EmuInstance*)userdata)->getInstanceID(); - return localMp.RecvHostPacket(inst, data, timestamp); + return MPInterface::Get().RecvHostPacket(inst, data, timestamp); } u16 MP_RecvReplies(u8* data, u64 timestamp, u16 aidmask, void* userdata) { int inst = ((EmuInstance*)userdata)->getInstanceID(); - return localMp.RecvReplies(inst, data, timestamp, aidmask); + return MPInterface::Get().RecvReplies(inst, data, timestamp, aidmask); } diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index 440f3e20..79b37e31 100644 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -73,7 +73,8 @@ #include "Config.h" #include "version.h" #include "Savestate.h" -#include "LocalMP.h" +#include "MPInterface.h" +#include "LANDialog.h" //#include "main_shaders.h" @@ -88,7 +89,6 @@ using namespace melonDS; extern CameraManager* camManager[2]; extern bool camStarted[2]; -extern LocalMP localMp; QString NdsRomMimeType = "application/x-nintendo-ds-rom"; @@ -432,6 +432,25 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : actMPNewInstance = submenu->addAction("Launch new instance"); connect(actMPNewInstance, &QAction::triggered, this, &MainWindow::onMPNewInstance); + + submenu->addSeparator(); + + actLANStartHost = submenu->addAction("Host LAN game"); + connect(actLANStartHost, &QAction::triggered, this, &MainWindow::onLANStartHost); + + actLANStartClient = submenu->addAction("Join LAN game"); + connect(actLANStartClient, &QAction::triggered, this, &MainWindow::onLANStartClient); + + /*submenu->addSeparator(); + + actNPStartHost = submenu->addAction("NETPLAY HOST"); + connect(actNPStartHost, &QAction::triggered, this, &MainWindow::onNPStartHost); + + actNPStartClient = submenu->addAction("NETPLAY CLIENT"); + connect(actNPStartClient, &QAction::triggered, this, &MainWindow::onNPStartClient); + + actNPTest = submenu->addAction("NETPLAY GO"); + connect(actNPTest, &QAction::triggered, this, &MainWindow::onNPTest);*/ } } { @@ -737,6 +756,8 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : QObject::connect(qApp, &QApplication::applicationStateChanged, this, &MainWindow::onAppStateChanged); onUpdateInterfaceSettings(); + + updateMPInterface(MPInterface::GetType()); } MainWindow::~MainWindow() @@ -756,24 +777,9 @@ void MainWindow::closeEvent(QCloseEvent* event) QByteArray geom = saveGeometry(); QByteArray enc = geom.toBase64(QByteArray::Base64Encoding); windowCfg.SetString("Geometry", enc.toStdString()); - Config::Save(); - if (hasOGL && (windowID == 0)) - { - // we intentionally don't unpause here - emuThread->emuPause(); - emuThread->deinitContext(); - } - - emuThread->detachWindow(this); - - if (windowID == 0) - { - int inst = emuInstance->instanceID; - deleteEmuInstance(inst); - } - + emuInstance->deleteWindow(windowID, false); QMainWindow::closeEvent(event); } @@ -1685,6 +1691,67 @@ void MainWindow::onMPNewInstance() createEmuInstance(); } +void MainWindow::onLANStartHost() +{ + if (!lanWarning(true)) return; + LANStartHostDialog::openDlg(this); +} + +void MainWindow::onLANStartClient() +{ + if (!lanWarning(false)) return; + LANStartClientDialog::openDlg(this); +} + +void MainWindow::onNPStartHost() +{ + //Netplay::StartHost(); + //NetplayStartHostDialog::openDlg(this); +} + +void MainWindow::onNPStartClient() +{ + //Netplay::StartClient(); + //NetplayStartClientDialog::openDlg(this); +} + +void MainWindow::onNPTest() +{ + // HAX + //Netplay::StartGame(); +} + +void MainWindow::updateMPInterface(MPInterfaceType type) +{ + // MP interface was changed, reflect it in the UI + + bool enable = (type == MPInterface_Local); + actMPNewInstance->setEnabled(enable); + actLANStartHost->setEnabled(enable); + actLANStartClient->setEnabled(enable); + /*actNPStartHost->setEnabled(enable); + actNPStartClient->setEnabled(enable); + actNPTest->setEnabled(enable);*/ +} + +bool MainWindow::lanWarning(bool host) +{ + if (numEmuInstances() < 2) + return true; + + QString verb = host ? "host" : "join"; + QString msg = "Multiple emulator instances are currently open.\n" + "If you "+verb+" a LAN game now, all secondary instances will be closed.\n\n" + "Do you wish to continue?"; + + auto res = QMessageBox::warning(this, "melonDS", msg, QMessageBox::Yes|QMessageBox::No, QMessageBox::No); + if (res == QMessageBox::No) + return false; + + deleteAllEmuInstances(1); + return true; +} + void MainWindow::onOpenEmuSettings() { emuThread->emuPause(); @@ -1840,7 +1907,7 @@ void MainWindow::onMPSettingsFinished(int res) { emuInstance->mpAudioMode = globalCfg.GetInt("MP.AudioMode"); emuInstance->audioMute(); - localMp.SetRecvTimeout(globalCfg.GetInt("MP.RecvTimeout")); + MPInterface::Get().SetRecvTimeout(globalCfg.GetInt("MP.RecvTimeout")); emuThread->emuUnpause(); } diff --git a/src/frontend/qt_sdl/Window.h b/src/frontend/qt_sdl/Window.h index c3ed166c..30d97b17 100644 --- a/src/frontend/qt_sdl/Window.h +++ b/src/frontend/qt_sdl/Window.h @@ -35,6 +35,7 @@ #include "Screen.h" #include "Config.h" +#include "MPInterface.h" class EmuInstance; @@ -125,6 +126,9 @@ public: void osdAddMessage(unsigned int color, const char* msg); + // called when the MP interface is changed + void updateMPInterface(melonDS::MPInterfaceType type); + protected: void keyPressEvent(QKeyEvent* event) override; void keyReleaseEvent(QKeyEvent* event) override; @@ -167,6 +171,11 @@ private slots: void onRAMInfo(); void onOpenTitleManager(); void onMPNewInstance(); + void onLANStartHost(); + void onLANStartClient(); + void onNPStartHost(); + void onNPStartClient(); + void onNPTest(); void onOpenEmuSettings(); void onEmuSettingsDialogFinished(int res); @@ -232,6 +241,8 @@ private: void createScreenPanel(); + bool lanWarning(bool host); + bool showOSD; bool hasOGL; @@ -279,6 +290,11 @@ public: QAction* actRAMInfo; QAction* actTitleManager; QAction* actMPNewInstance; + QAction* actLANStartHost; + QAction* actLANStartClient; + QAction* actNPStartHost; + QAction* actNPStartClient; + QAction* actNPTest; QAction* actEmuSettings; #ifdef __APPLE__ diff --git a/src/frontend/qt_sdl/main.cpp b/src/frontend/qt_sdl/main.cpp index 7d92bf7b..9a9c93cb 100644 --- a/src/frontend/qt_sdl/main.cpp +++ b/src/frontend/qt_sdl/main.cpp @@ -22,18 +22,14 @@ #include #include -#include #include -#include -#include #include +#include #include #include -#include #include #include -#include #include #include #include @@ -57,24 +53,14 @@ #include "duckstation/gl/context.h" #include "main.h" -#include "CheatsDialog.h" -#include "DateTimeDialog.h" -#include "EmuSettingsDialog.h" -#include "InputConfig/InputConfigDialog.h" -#include "VideoSettingsDialog.h" -#include "ROMInfoDialog.h" -#include "RAMInfoDialog.h" -#include "PowerManagement/PowerManagementDialog.h" - #include "version.h" #include "Config.h" -#include "DSi.h" #include "EmuInstance.h" #include "ArchiveUtil.h" #include "CameraManager.h" -#include "LocalMP.h" +#include "MPInterface.h" #include "Net.h" #include "CLI.h" @@ -87,7 +73,6 @@ using namespace melonDS; QString* systemThemeName; - QString emuDirectory; const int kMaxEmuInstances = 16; @@ -95,10 +80,14 @@ EmuInstance* emuInstances[kMaxEmuInstances]; CameraManager* camManager[2]; bool camStarted[2]; -LocalMP localMp; + std::optional pcap; Net net; + +QElapsedTimer sysTimer; + + void NetInit() { Config::Table cfg = Config::GetGlobalTable(); @@ -159,12 +148,25 @@ void deleteEmuInstance(int id) emuInstances[id] = nullptr; } -void deleteAllEmuInstances() +void deleteAllEmuInstances(int first) { - for (int i = 0; i < kMaxEmuInstances; i++) + for (int i = first; i < kMaxEmuInstances; i++) deleteEmuInstance(i); } +int numEmuInstances() +{ + int ret = 0; + + for (int i = 0; i < kMaxEmuInstances; i++) + { + if (emuInstances[i]) + ret++; + } + + return ret; +} + void pathInit() { @@ -203,6 +205,28 @@ void pathInit() } +void setMPInterface(MPInterfaceType type) +{ + // switch to the requested MP interface + MPInterface::Set(type); + + // set receive timeout + // TODO: different settings per interface? + MPInterface::Get().SetRecvTimeout(Config::GetGlobalTable().GetInt("MP.RecvTimeout")); + + // update UI appropriately + // TODO: decide how to deal with multi-window when it becomes a thing + for (int i = 0; i < kMaxEmuInstances; i++) + { + EmuInstance* inst = emuInstances[i]; + if (!inst) continue; + + MainWindow* win = inst->getMainWindow(); + if (win) win->updateMPInterface(type); + } +} + + MelonApplication::MelonApplication(int& argc, char** argv) : QApplication(argc, argv) @@ -237,6 +261,7 @@ bool MelonApplication::event(QEvent *event) int main(int argc, char** argv) { + sysTimer.start(); srand(time(nullptr)); for (int i = 0; i < kMaxEmuInstances; i++) @@ -308,7 +333,10 @@ int main(int argc, char** argv) } } - // localMp is initialized at this point + // default MP interface type is local MP + // this will be changed if a LAN or netplay session is initiated + setMPInterface(MPInterface_Local); + NetInit(); createEmuInstance(); diff --git a/src/frontend/qt_sdl/main.h b/src/frontend/qt_sdl/main.h index 5f2d7bb5..77cdf4ee 100644 --- a/src/frontend/qt_sdl/main.h +++ b/src/frontend/qt_sdl/main.h @@ -22,19 +22,14 @@ #include "glad/glad.h" #include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include #include "EmuInstance.h" #include "Window.h" #include "EmuThread.h" #include "ScreenLayout.h" +#include "MPInterface.h" class MelonApplication : public QApplication { @@ -48,8 +43,13 @@ public: extern QString* systemThemeName; extern QString emuDirectory; +extern QElapsedTimer sysTimer; + bool createEmuInstance(); void deleteEmuInstance(int id); -void deleteAllEmuInstances(); +void deleteAllEmuInstances(int first = 0); +int numEmuInstances(); + +void setMPInterface(melonDS::MPInterfaceType type); #endif // MAIN_H diff --git a/src/net/CMakeLists.txt b/src/net/CMakeLists.txt index 62bb557c..6ca24de6 100644 --- a/src/net/CMakeLists.txt +++ b/src/net/CMakeLists.txt @@ -6,6 +6,9 @@ add_library(net-utils STATIC Net_Slirp.cpp PacketDispatcher.cpp LocalMP.cpp + LAN.cpp + Netplay.cpp + MPInterface.cpp ) target_include_directories(net-utils PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") @@ -21,3 +24,12 @@ else() add_subdirectory(libslirp EXCLUDE_FROM_ALL) target_link_libraries(net-utils PUBLIC slirp) endif() + +if (USE_VCPKG) + find_package(unofficial-enet CONFIG REQUIRED) + target_link_libraries(net-utils PRIVATE unofficial::enet::enet) +else() + pkg_check_modules(ENet REQUIRED IMPORTED_TARGET libenet) + fix_interface_includes(PkgConfig::ENet) + target_link_libraries(net-utils PUBLIC PkgConfig::ENet) +endif() diff --git a/src/net/LAN.cpp b/src/net/LAN.cpp new file mode 100644 index 00000000..ebc66fd8 --- /dev/null +++ b/src/net/LAN.cpp @@ -0,0 +1,1091 @@ +/* + Copyright 2016-2024 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 + +#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 "LAN.h" + + +namespace melonDS +{ + +const u32 kDiscoveryMagic = 0x444E414C; // LAND +const u32 kLANMagic = 0x504E414C; // LANP +const u32 kPacketMagic = 0x4946494E; // NIFI + +const u32 kProtocolVersion = 1; + +const u32 kLocalhost = 0x0100007F; + +enum +{ + Chan_Cmd = 0, // channel 0 -- control commands + Chan_MP, // channel 1 -- MP data exchange +}; + +enum +{ + Cmd_ClientInit = 1, // 01 -- host->client -- init new client and assign ID + Cmd_PlayerInfo, // 02 -- client->host -- send client player info to host + Cmd_PlayerList, // 03 -- host->client -- broadcast updated player list + Cmd_PlayerConnect, // 04 -- both -- signal connected state (ready to receive MP frames) + Cmd_PlayerDisconnect, // 05 -- both -- signal disconnected state (not receiving MP frames) +}; + +const int kDiscoveryPort = 7063; +const int kLANPort = 7064; + + +LAN::LAN() noexcept : Inited(false) +{ + DiscoveryMutex = Platform::Mutex_Create(); + PlayersMutex = Platform::Mutex_Create(); + + DiscoverySocket = INVALID_SOCKET; + DiscoveryLastTick = 0; + + Active = false; + IsHost = false; + Host = nullptr; + //Lag = false; + + memset(RemotePeers, 0, sizeof(RemotePeers)); + memset(Players, 0, sizeof(Players)); + NumPlayers = 0; + MaxPlayers = 0; + + ConnectedBitmask = 0; + + MPRecvTimeout = 25; + LastHostID = -1; + LastHostPeer = nullptr; + + FrameCount = 0; + + // TODO make this somewhat nicer + if (enet_initialize() != 0) + { + Platform::Log(Platform::LogLevel::Error, "LAN: failed to initialize enet\n"); + return; + } + + Platform::Log(Platform::LogLevel::Info, "LAN: enet initialized\n"); + Inited = true; +} + +LAN::~LAN() noexcept +{ + EndSession(); + + Inited = false; + enet_deinitialize(); + + Platform::Mutex_Free(DiscoveryMutex); + Platform::Mutex_Free(PlayersMutex); + + Platform::Log(Platform::LogLevel::Info, "LAN: enet deinitialized\n"); +} + + +std::map LAN::GetDiscoveryList() +{ + Platform::Mutex_Lock(DiscoveryMutex); + auto ret = DiscoveryList; + Platform::Mutex_Unlock(DiscoveryMutex); + return ret; +} + +std::vector LAN::GetPlayerList() +{ + Platform::Mutex_Lock(PlayersMutex); + + std::vector ret; + for (int i = 0; i < 16; i++) + { + if (Players[i].Status == Player_None) continue; + + // make a copy of the player entry, fix up the address field + Player newp = Players[i]; + if (newp.ID == MyPlayer.ID) + { + newp.IsLocalPlayer = true; + newp.Address = kLocalhost; + } + else + { + newp.IsLocalPlayer = false; + if (newp.Status == Player_Host) + newp.Address = HostAddress; + } + + ret.push_back(newp); + } + + Platform::Mutex_Unlock(PlayersMutex); + return ret; +} + + +bool LAN::StartDiscovery() +{ + if (!Inited) return false; + + 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 = (u32)Platform::GetMSCount(); + DiscoveryList.clear(); + + Active = true; + return true; +} + +void LAN::EndDiscovery() +{ + if (!Inited) return; + + if (DiscoverySocket != INVALID_SOCKET) + { + closesocket(DiscoverySocket); + DiscoverySocket = INVALID_SOCKET; + } + + if (!IsHost) + Active = false; +} + +bool LAN::StartHost(const char* playername, int numplayers) +{ + if (!Inited) return false; + if (numplayers > 16) return false; + + ENetAddress addr; + addr.host = ENET_HOST_ANY; + addr.port = kLANPort; + + Host = enet_host_create(&addr, 16, 2, 0, 0); + if (!Host) + { + return false; + } + + Platform::Mutex_Lock(PlayersMutex); + + Player* player = &Players[0]; + memset(player, 0, sizeof(Player)); + player->ID = 0; + strncpy(player->Name, playername, 31); + player->Status = Player_Host; + player->Address = kLocalhost; + NumPlayers = 1; + MaxPlayers = numplayers; + memcpy(&MyPlayer, player, sizeof(Player)); + + Platform::Mutex_Unlock(PlayersMutex); + + HostAddress = kLocalhost; + LastHostID = -1; + LastHostPeer = nullptr; + + Active = true; + IsHost = true; + + StartDiscovery(); + return true; +} + +bool LAN::StartClient(const char* playername, const char* host) +{ + if (!Inited) return false; + + 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; + } + + Platform::Mutex_Lock(PlayersMutex); + + Player* player = &MyPlayer; + memset(player, 0, sizeof(Player)); + player->ID = 0; + strncpy(player->Name, playername, 31); + player->Status = Player_Connecting; + + Platform::Mutex_Unlock(PlayersMutex); + + ENetEvent event; + int conn = 0; + u32 starttick = (u32)Platform::GetMSCount(); + const int conntimeout = 5000; + for (;;) + { + u32 curtick = (u32)Platform::GetMSCount(); + 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 != Chan_Cmd) continue; + if (data[0] != Cmd_ClientInit) 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; + if (data[10] > 16) continue; + + MaxPlayers = data[10]; + + // send player information + MyPlayer.ID = data[9]; + u8 cmd[9+sizeof(Player)]; + cmd[0] = Cmd_PlayerInfo; + 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, Chan_Cmd, 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 LAN::EndSession() +{ + if (!Active) return; + if (IsHost) EndDiscovery(); + + Active = false; + + 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; + IsHost = false; +} + + +void LAN::ProcessDiscovery() +{ + if (DiscoverySocket == INVALID_SOCKET) + return; + + u32 tick = (u32)Platform::GetMSCount(); + 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 + { + Platform::Mutex_Lock(DiscoveryMutex); + + // 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); + } + + Platform::Mutex_Unlock(DiscoveryMutex); + } +} + +void LAN::HostUpdatePlayerList() +{ + u8 cmd[2+sizeof(Players)]; + cmd[0] = Cmd_PlayerList; + 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, Chan_Cmd, pkt); +} + +void LAN::ClientUpdatePlayerList() +{ +} + +void LAN::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 == Player_None) break; + } + + if (id < 16) + { + u8 cmd[11]; + cmd[0] = Cmd_ClientInit; + 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, Chan_Cmd, pkt); + + Platform::Mutex_Lock(PlayersMutex); + + Players[id].ID = id; + Players[id].Status = Player_Connecting; + Players[id].Address = event.peer->address.host; + event.peer->data = &Players[id]; + NumPlayers++; + + Platform::Mutex_Unlock(PlayersMutex); + + 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 = Player_None; + 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 Cmd_PlayerInfo: // 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; + } + + Platform::Mutex_Lock(PlayersMutex); + + player.Status = Player_Client; + player.Address = event.peer->address.host; + memcpy(hostside, &player, sizeof(Player)); + + Platform::Mutex_Unlock(PlayersMutex); + + // broadcast updated player list + HostUpdatePlayerList(); + } + break; + + case Cmd_PlayerConnect: // player connected + { + if (event.packet->dataLength != 1) break; + Player* player = (Player*)event.peer->data; + if (!player) break; + + ConnectedBitmask |= (1 << player->ID); + } + break; + + case Cmd_PlayerDisconnect: // player disconnected + { + if (event.packet->dataLength != 1) break; + Player* player = (Player*)event.peer->data; + if (!player) break; + + ConnectedBitmask &= ~(1 << player->ID); + } + break; + } + + enet_packet_destroy(event.packet); + } + break; + } +} + +void LAN::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 != Player_Client) 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; + + Platform::Mutex_Lock(PlayersMutex); + player->Status = Player_Disconnected; + Platform::Mutex_Unlock(PlayersMutex); + + ClientUpdatePlayerList(); + } + break; + + case ENET_EVENT_TYPE_RECEIVE: + { + if (event.packet->dataLength < 1) break; + + u8* data = (u8*)event.packet->data; + switch (data[0]) + { + case Cmd_PlayerList: // host sending player list + { + if (event.packet->dataLength != (2+sizeof(Players))) break; + if (data[1] > 16) break; + + Platform::Mutex_Lock(PlayersMutex); + + NumPlayers = data[1]; + memcpy(Players, &data[2], sizeof(Players)); + for (int i = 0; i < 16; i++) + { + Players[i].Name[31] = '\0'; + } + + Platform::Mutex_Unlock(PlayersMutex); + + // 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 != Player_Client) 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 Cmd_PlayerConnect: // player connected + { + if (event.packet->dataLength != 1) break; + Player* player = (Player*)event.peer->data; + if (!player) break; + + ConnectedBitmask |= (1 << player->ID); + } + break; + + case Cmd_PlayerDisconnect: // player disconnected + { + if (event.packet->dataLength != 1) break; + Player* player = (Player*)event.peer->data; + if (!player) break; + + ConnectedBitmask &= ~(1 << player->ID); + } + break; + } + + enet_packet_destroy(event.packet); + } + break; + } +} + +void LAN::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 LAN::ProcessLAN(int type) +{ + if (!Host) return; + + u32 time_last = (u32)Platform::GetMSCount(); + + // 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 = (u32)Platform::GetMSCount(); + + ENetEvent event; + while (enet_host_service(Host, &event, timeout) > 0) + { + if (event.type == ENET_EVENT_TYPE_RECEIVE && event.channelID == Chan_MP) + { + MPPacketHeader* header = (MPPacketHeader*)&event.packet->data[0]; + + 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 = (u32)Platform::GetMSCount(); + + 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 + { + ProcessEvent(event); + } + + if (type == 2) + { + u32 time = (u32)Platform::GetMSCount(); + if (time < time_last) return; + timeout -= (int)(time - time_last); + if (timeout <= 0) return; + time_last = time; + } + } +} + +void LAN::Process() +{ + if (!Active) return; + + ProcessDiscovery(); + ProcessLAN(0); + + FrameCount++; + if (FrameCount >= 60) + { + FrameCount = 0; + + Platform::Mutex_Lock(PlayersMutex); + + for (int i = 0; i < 16; i++) + { + if (Players[i].Status == Player_None) continue; + if (i == MyPlayer.ID) continue; + if (!RemotePeers[i]) continue; + + Players[i].Ping = RemotePeers[i]->roundTripTime; + } + + Platform::Mutex_Unlock(PlayersMutex); + } +} + + +void LAN::Begin(int inst) +{ + if (!Host) return; + + ConnectedBitmask |= (1 << MyPlayer.ID); + LastHostID = -1; + LastHostPeer = nullptr; + + u8 cmd = Cmd_PlayerConnect; + ENetPacket* pkt = enet_packet_create(&cmd, 1, ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(Host, Chan_Cmd, pkt); +} + +void LAN::End(int inst) +{ + if (!Host) return; + + ConnectedBitmask &= ~(1 << MyPlayer.ID); + + u8 cmd = Cmd_PlayerDisconnect; + ENetPacket* pkt = enet_packet_create(&cmd, 1, ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(Host, Chan_Cmd, pkt); +} + + +int LAN::SendPacketGeneric(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, Chan_MP, enetpacket); + else + enet_host_broadcast(Host, Chan_MP, enetpacket); + enet_host_flush(Host); + + return len; +} + +int LAN::RecvPacketGeneric(u8* packet, bool block, u64* timestamp) +{ + if (!Host) return 0; + + ProcessLAN(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 LAN::SendPacket(int inst, u8* packet, int len, u64 timestamp) +{ + return SendPacketGeneric(0, packet, len, timestamp); +} + +int LAN::RecvPacket(int inst, u8* packet, u64* timestamp) +{ + return RecvPacketGeneric(packet, false, timestamp); +} + + +int LAN::SendCmd(int inst, u8* packet, int len, u64 timestamp) +{ + return SendPacketGeneric(1, packet, len, timestamp); +} + +int LAN::SendReply(int inst, u8* packet, int len, u64 timestamp, u16 aid) +{ + return SendPacketGeneric(2 | (aid<<16), packet, len, timestamp); +} + +int LAN::SendAck(int inst, u8* packet, int len, u64 timestamp) +{ + return SendPacketGeneric(3, packet, len, timestamp); +} + +int LAN::RecvHostPacket(int inst, 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/net/LAN.h b/src/net/LAN.h new file mode 100644 index 00000000..87282539 --- /dev/null +++ b/src/net/LAN.h @@ -0,0 +1,156 @@ +/* + Copyright 2016-2024 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 + +#ifndef socket_t + #ifdef __WIN32__ + #include + #define socket_t SOCKET + #else + #define socket_t int + #endif +#endif + +#include "types.h" +#include "Platform.h" +#include "MPInterface.h" + +namespace melonDS +{ + +class LAN : public MPInterface +{ +public: + LAN() noexcept; + LAN(const LAN&) = delete; + LAN& operator=(const LAN&) = delete; + LAN(LAN&& other) = delete; + LAN& operator=(LAN&& other) = delete; + ~LAN() noexcept; + + enum PlayerStatus + { + Player_None = 0, // no player in this entry + Player_Client, // game client + Player_Host, // game host + Player_Connecting, // player still connecting + Player_Disconnected, // player disconnected + }; + + struct Player + { + int ID; + char Name[32]; + PlayerStatus Status; + u32 Address; + + bool IsLocalPlayer; + u32 Ping; + }; + + struct DiscoveryData + { + u32 Magic; + u32 Version; + u32 Tick; + char SessionName[64]; + u8 NumPlayers; + u8 MaxPlayers; + u8 Status; // 0=idle 1=playing + }; + + bool StartDiscovery(); + void EndDiscovery(); + bool StartHost(const char* player, int numplayers); + bool StartClient(const char* player, const char* host); + void EndSession(); + + std::map GetDiscoveryList(); + std::vector GetPlayerList(); + int GetNumPlayers() { return NumPlayers; } + int GetMaxPlayers() { return MaxPlayers; } + + void Process() override; + + void Begin(int inst) override; + void End(int inst) override; + + int SendPacket(int inst, u8* data, int len, u64 timestamp) override; + int RecvPacket(int inst, u8* data, u64* timestamp) override; + int SendCmd(int inst, u8* data, int len, u64 timestamp) override; + int SendReply(int inst, u8* data, int len, u64 timestamp, u16 aid) override; + int SendAck(int inst, u8* data, int len, u64 timestamp) override; + int RecvHostPacket(int inst, u8* data, u64* timestamp) override; + u16 RecvReplies(int inst, u8* data, u64 timestamp, u16 aidmask) override; + +private: + bool Inited; + bool Active; + bool IsHost; + + ENetHost* Host; + ENetPeer* RemotePeers[16]; + + socket_t DiscoverySocket; + u32 DiscoveryLastTick; + std::map DiscoveryList; + Platform::Mutex* DiscoveryMutex; + + Player Players[16]; + int NumPlayers; + int MaxPlayers; + Platform::Mutex* PlayersMutex; + + Player MyPlayer; + u32 HostAddress; + + u16 ConnectedBitmask; + + int MPRecvTimeout; + int LastHostID; + ENetPeer* LastHostPeer; + std::queue RXQueue; + + u32 FrameCount; + + void ProcessDiscovery(); + + void HostUpdatePlayerList(); + void ClientUpdatePlayerList(); + + void ProcessHostEvent(ENetEvent& event); + void ProcessClientEvent(ENetEvent& event); + void ProcessEvent(ENetEvent& event); + void ProcessLAN(int type); + + int SendPacketGeneric(u32 type, u8* packet, int len, u64 timestamp); + int RecvPacketGeneric(u8* packet, bool block, u64* timestamp); +}; + +} + +#endif // LAN_H diff --git a/src/net/LocalMP.cpp b/src/net/LocalMP.cpp index 0f6889ac..a789964e 100644 --- a/src/net/LocalMP.cpp +++ b/src/net/LocalMP.cpp @@ -19,8 +19,6 @@ #include #include "LocalMP.h" -#include "Platform.h" -#include "types.h" using namespace melonDS; using namespace melonDS::Platform; diff --git a/src/net/LocalMP.h b/src/net/LocalMP.h index 4de6f30f..8688d8e1 100644 --- a/src/net/LocalMP.h +++ b/src/net/LocalMP.h @@ -21,6 +21,7 @@ #include "types.h" #include "Platform.h" +#include "MPInterface.h" namespace melonDS { @@ -33,20 +34,11 @@ struct MPStatusData u16 MPReplyBitmask; // bitmask of which clients replied in time }; -struct MPPacketHeader -{ - u32 Magic; - u32 SenderID; - u32 Type; // 0=regular 1=CMD 2=reply 3=ack - u32 Length; - u64 Timestamp; -}; - constexpr u32 kPacketQueueSize = 0x10000; constexpr u32 kReplyQueueSize = 0x10000; constexpr u32 kMaxFrameSize = 0x948; -class LocalMP +class LocalMP : public MPInterface { public: LocalMP() noexcept; @@ -56,8 +48,7 @@ public: LocalMP& operator=(LocalMP&& other) = delete; ~LocalMP() noexcept; - [[nodiscard]] int GetRecvTimeout() const noexcept { return RecvTimeout; } - void SetRecvTimeout(int timeout) noexcept { RecvTimeout = timeout; } + void Process() {} void Begin(int inst); void End(int inst); @@ -69,11 +60,13 @@ public: int SendAck(int inst, u8* data, int len, u64 timestamp); int RecvHostPacket(int inst, u8* data, u64* timestamp); u16 RecvReplies(int inst, u8* data, u64 timestamp, u16 aidmask); + private: void FIFORead(int inst, int fifo, void* buf, int len) noexcept; void FIFOWrite(int inst, int fifo, void* buf, int len) noexcept; int SendPacketGeneric(int inst, u32 type, u8* packet, int len, u64 timestamp) noexcept; int RecvPacketGeneric(int inst, u8* packet, bool block, u64* timestamp) noexcept; + Platform::Mutex* MPQueueLock; MPStatusData MPStatus {}; u8 MPPacketQueue[kPacketQueueSize] {}; @@ -81,8 +74,6 @@ private: u32 PacketReadOffset[16] {}; u32 ReplyReadOffset[16] {}; - int RecvTimeout = 25; - int LastHostID = -1; Platform::Semaphore* SemPool[32] {}; }; diff --git a/src/net/MPInterface.cpp b/src/net/MPInterface.cpp new file mode 100644 index 00000000..39d1915d --- /dev/null +++ b/src/net/MPInterface.cpp @@ -0,0 +1,68 @@ +/* + Copyright 2016-2024 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 "MPInterface.h" +#include "LocalMP.h" +#include "LAN.h" + +namespace melonDS +{ + +class DummyMP : public MPInterface +{ +public: + void Process() override {} + + void Begin(int inst) override {} + void End(int inst) override {} + + int SendPacket(int inst, u8* data, int len, u64 timestamp) override { return 0; } + int RecvPacket(int inst, u8* data, u64* timestamp) override { return 0; } + int SendCmd(int inst, u8* data, int len, u64 timestamp) override { return 0; } + int SendReply(int inst, u8* data, int len, u64 timestamp, u16 aid) override { return 0; } + int SendAck(int inst, u8* data, int len, u64 timestamp) override { return 0; } + int RecvHostPacket(int inst, u8* data, u64* timestamp) override { return 0; } + u16 RecvReplies(int inst, u8* data, u64 timestamp, u16 aidmask) override { return 0; } +}; + + +std::unique_ptr MPInterface::Current(std::make_unique()); +MPInterfaceType MPInterface::CurrentType = MPInterface_Dummy; + + +void MPInterface::Set(MPInterfaceType type) +{ + switch (type) + { + case MPInterface_Local: + Current = std::make_unique(); + break; + + case MPInterface_LAN: + Current = std::make_unique(); + break; + + default: + Current = std::make_unique(); + break; + } + + CurrentType = type; +} + +} diff --git a/src/net/MPInterface.h b/src/net/MPInterface.h new file mode 100644 index 00000000..eb5bef88 --- /dev/null +++ b/src/net/MPInterface.h @@ -0,0 +1,82 @@ +/* + Copyright 2016-2024 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 MPINTERFACE_H +#define MPINTERFACE_H + +#include +#include "types.h" + +namespace melonDS +{ + +// TODO: provision for excluding unwanted interfaces at compile time +enum MPInterfaceType +{ + MPInterface_Dummy = -1, + MPInterface_Local, + MPInterface_LAN, + MPInterface_Netplay, +}; + +struct MPPacketHeader +{ + u32 Magic; + u32 SenderID; + u32 Type; // 0=regular 1=CMD 2=reply 3=ack + u32 Length; + u64 Timestamp; +}; + +class MPInterface +{ +public: + virtual ~MPInterface() = default; + + static MPInterface& Get() { return *Current; } + static MPInterfaceType GetType() { return CurrentType; } + static void Set(MPInterfaceType type); + + [[nodiscard]] int GetRecvTimeout() const noexcept { return RecvTimeout; } + void SetRecvTimeout(int timeout) noexcept { RecvTimeout = timeout; } + + // function called every video frame + virtual void Process() = 0; + + virtual void Begin(int inst) = 0; + virtual void End(int inst) = 0; + + virtual int SendPacket(int inst, u8* data, int len, u64 timestamp) = 0; + virtual int RecvPacket(int inst, u8* data, u64* timestamp) = 0; + virtual int SendCmd(int inst, u8* data, int len, u64 timestamp) = 0; + virtual int SendReply(int inst, u8* data, int len, u64 timestamp, u16 aid) = 0; + virtual int SendAck(int inst, u8* data, int len, u64 timestamp) = 0; + virtual int RecvHostPacket(int inst, u8* data, u64* timestamp) = 0; + virtual u16 RecvReplies(int inst, u8* data, u64 timestamp, u16 aidmask) = 0; + +protected: + int RecvTimeout = 25; + +private: + static MPInterfaceType CurrentType; + static std::unique_ptr Current; +}; + +} + +#endif // MPINTERFACE_H diff --git a/src/net/Netplay.cpp b/src/net/Netplay.cpp new file mode 100644 index 00000000..68caa43d --- /dev/null +++ b/src/net/Netplay.cpp @@ -0,0 +1,1085 @@ +/* + Copyright 2016-2024 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 "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" + +using namespace melonDS; + +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) +{ +#if 0 + 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; +#if 0 + // 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(); + } +#endif + // 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"); + +#if 0 + 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(); +#endif +} + +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; +#if 0 + 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; + } + } +#endif +} + +void ProcessMirrorClient() +{ + if (!MirrorHost) return; +#if 0 + 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; + } +#endif +} + +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 0 + 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(); +#endif +} + +} diff --git a/src/net/Netplay.h b/src/net/Netplay.h new file mode 100644 index 00000000..1eff54b0 --- /dev/null +++ b/src/net/Netplay.h @@ -0,0 +1,57 @@ +/* + Copyright 2016-2024 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 "types.h" + +namespace Netplay +{ + +struct Player +{ + int ID; + char Name[32]; + int Status; // 0=no player 1=normal 2=host 3=connecting + melonDS::u32 Address; +}; + + +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); + +melonDS::u32 PlayerAddress(int id); + +void StartGame(); +void StartLocal(); + +void StartGame(); + +void ProcessFrame(); +void ProcessInput(); + +} + +#endif // NETPLAY_H diff --git a/vcpkg.json b/vcpkg.json index ab89176e..445f3139 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -3,7 +3,8 @@ "dependencies": [ "sdl2", "libarchive", - "zstd" + "zstd", + "enet" ], "features": { "qt6": {