Backport LAN (#2131)

backport the old LAN feature to the modern melonDS codebase.
This commit is contained in:
Arisotura 2024-08-10 23:20:50 +02:00 committed by GitHub
parent ec71b15505
commit 8d31875902
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 4163 additions and 87 deletions

View File

@ -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

48
cmake/FindENet.cmake Normal file
View File

@ -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)

View File

@ -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": {

View File

@ -37,6 +37,7 @@
libarchive
libGL
libslirp
enet
]) ++ optionals isLinux [
pkgs.wayland
pkgs.kdePackages.qtwayland

View File

@ -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

View File

@ -51,6 +51,9 @@ set(SOURCES_QT_SDL
CLI.h
CLI.cpp
LANDialog.cpp
NetplayDialog.cpp
)
if (APPLE)

View File

@ -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, ...)
{

View File

@ -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<<id); }
bool hotkeyReleased(int id) { return hotkeyRelease & (1<<id); }
bool deleting;
int instanceID;
EmuThread* emuThread;

View File

@ -151,6 +151,7 @@ void EmuThread::run()
while (emuStatus != emuStatus_Exit)
{
MPInterface::Get().Process();
emuInstance->inputProcess();
if (emuInstance->hotkeyPressed(HK_FastForwardToggle)) emit windowLimitFPSChange();

View File

@ -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 <stdio.h>
#include <string.h>
#include <queue>
#include <vector>
#include <QStandardItemModel>
#include <QPushButton>
#include <QInputDialog>
#include <QMessageBox>
#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<QStandardItem*> 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<QStandardItem*> 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++;
}
}

View File

@ -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 <QDialog>
#include <QMutex>
#include <QItemSelection>
#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

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LANDialog</class>
<widget class="QDialog" name="LANDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>522</width>
<height>391</height>
</rect>
</property>
<property name="windowTitle">
<string>LAN game - melonDS</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="btnLeaveGame">
<property name="text">
<string>Leave game</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTreeView" name="tvPlayerList"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LANStartClientDialog</class>
<widget class="QDialog" name="LANStartClientDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>547</width>
<height>409</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Join LAN game - melonDS</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Player name:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="txtPlayerName">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTreeView" name="tvAvailableGames">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>LANStartClientDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>LANStartClientDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LANStartHostDialog</class>
<widget class="QDialog" name="LANStartHostDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>389</width>
<height>228</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Host LAN game - melonDS</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Player name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="txtPlayerName"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Number of players:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="sbNumPlayers"/>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>LANStartHostDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>LANStartHostDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <queue>
#include <enet/enet.h>
#include <QStandardItemModel>
#include <QProcess>
#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));
}
}

View File

@ -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 <QDialog>
#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

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NetplayDialog</class>
<widget class="QDialog" name="NetplayDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>522</width>
<height>391</height>
</rect>
</property>
<property name="windowTitle">
<string>NETPLAY SHITO</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="lblStatus">
<property name="text">
<string>STATUS PLACEHOLDER</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeView" name="tvPlayerList"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NetplayStartClientDialog</class>
<widget class="QDialog" name="NetplayStartClientDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>229</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>NETPLAY CLIENT</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Player name:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Host port:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="txtPlayerName"/>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="txtPort"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Host address:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="txtIPAddress"/>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>NetplayStartClientDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>NetplayStartClientDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NetplayStartHostDialog</class>
<widget class="QDialog" name="NetplayStartHostDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>229</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>NETPLAY HOST</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Player name:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Port:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="txtPlayerName"/>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="txtPort"/>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>NetplayStartHostDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>NetplayStartHostDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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__

View File

@ -22,18 +22,14 @@
#include <string.h>
#include <optional>
#include <vector>
#include <string>
#include <algorithm>
#include <QProcess>
#include <QApplication>
#include <QStyle>
#include <QMessageBox>
#include <QMenuBar>
#include <QMimeDatabase>
#include <QFileDialog>
#include <QInputDialog>
#include <QPaintEvent>
#include <QPainter>
#include <QKeyEvent>
#include <QMimeData>
@ -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<LibPCap> 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();

View File

@ -22,19 +22,14 @@
#include "glad/glad.h"
#include <QApplication>
#include <QWidget>
#include <QWindow>
#include <QMainWindow>
#include <QImage>
#include <QActionGroup>
#include <QTimer>
#include <QScreen>
#include <QCloseEvent>
#include <QEvent>
#include <QElapsedTimer>
#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

View File

@ -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()

1091
src/net/LAN.cpp Normal file

File diff suppressed because it is too large Load Diff

156
src/net/LAN.h Normal file
View File

@ -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 <string>
#include <vector>
#include <map>
#include <queue>
#include <enet/enet.h>
#ifndef socket_t
#ifdef __WIN32__
#include <winsock2.h>
#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<u32, DiscoveryData> GetDiscoveryList();
std::vector<Player> 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<u32, DiscoveryData> 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<ENetPacket*> 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

View File

@ -19,8 +19,6 @@
#include <cstring>
#include "LocalMP.h"
#include "Platform.h"
#include "types.h"
using namespace melonDS;
using namespace melonDS::Platform;

View File

@ -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] {};
};

68
src/net/MPInterface.cpp Normal file
View File

@ -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> MPInterface::Current(std::make_unique<DummyMP>());
MPInterfaceType MPInterface::CurrentType = MPInterface_Dummy;
void MPInterface::Set(MPInterfaceType type)
{
switch (type)
{
case MPInterface_Local:
Current = std::make_unique<LocalMP>();
break;
case MPInterface_LAN:
Current = std::make_unique<LAN>();
break;
default:
Current = std::make_unique<DummyMP>();
break;
}
CurrentType = type;
}
}

82
src/net/MPInterface.h Normal file
View File

@ -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 <memory>
#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<MPInterface> Current;
};
}
#endif // MPINTERFACE_H

1085
src/net/Netplay.cpp Normal file

File diff suppressed because it is too large Load Diff

57
src/net/Netplay.h Normal file
View File

@ -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

View File

@ -3,7 +3,8 @@
"dependencies": [
"sdl2",
"libarchive",
"zstd"
"zstd",
"enet"
],
"features": {
"qt6": {