From 8423dae6ff24173d435f743eab2fe609ab86cab3 Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Wed, 7 Aug 2024 15:53:58 +0200 Subject: [PATCH 01/18] Add "Open melonDS directory" menu option --- src/frontend/qt_sdl/Window.cpp | 7 +++++++ src/frontend/qt_sdl/Window.h | 1 + 2 files changed, 8 insertions(+) diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index f39bbb37..440f3e20 100644 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #ifndef _WIN32 #include #include @@ -366,6 +367,12 @@ MainWindow::MainWindow(int id, EmuInstance* inst, QWidget* parent) : actUndoStateLoad->setShortcut(QKeySequence(Qt::Key_F12)); connect(actUndoStateLoad, &QAction::triggered, this, &MainWindow::onUndoStateLoad); + menu->addSeparator(); + actOpenConfig = menu->addAction("Open melonDS directory"); + connect(actOpenConfig, &QAction::triggered, this, [&]() { + QDesktopServices::openUrl(QUrl::fromLocalFile(emuDirectory)); + }); + menu->addSeparator(); actQuit = menu->addAction("Quit"); diff --git a/src/frontend/qt_sdl/Window.h b/src/frontend/qt_sdl/Window.h index aff8507f..c3ed166c 100644 --- a/src/frontend/qt_sdl/Window.h +++ b/src/frontend/qt_sdl/Window.h @@ -264,6 +264,7 @@ public: QAction* actSaveState[9]; QAction* actLoadState[9]; QAction* actUndoStateLoad; + QAction* actOpenConfig; QAction* actQuit; QAction* actPause; From a17490141299099c8d836d7230ebfe09138c3ca7 Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Wed, 7 Aug 2024 17:23:48 +0200 Subject: [PATCH 02/18] fix building with system libslirp --- src/frontend/qt_sdl/CMakeLists.txt | 3 -- src/net/Net_Slirp.cpp | 65 +----------------------------- src/net/Net_Slirp.h | 2 +- 3 files changed, 2 insertions(+), 68 deletions(-) diff --git a/src/frontend/qt_sdl/CMakeLists.txt b/src/frontend/qt_sdl/CMakeLists.txt index 559b7a2d..c857bbbc 100644 --- a/src/frontend/qt_sdl/CMakeLists.txt +++ b/src/frontend/qt_sdl/CMakeLists.txt @@ -164,9 +164,6 @@ target_include_directories(melonDS PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") target_include_directories(melonDS PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..") target_include_directories(melonDS PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../..") target_include_directories(melonDS PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../../net") -target_include_directories(melonDS PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../../net/libslirp/src") -get_target_property(SLIRP_BINARY_DIR slirp BINARY_DIR) -target_include_directories(melonDS PUBLIC "${SLIRP_BINARY_DIR}") # for libslirp-version.h if (USE_QT6) target_include_directories(melonDS PUBLIC ${Qt6Gui_PRIVATE_INCLUDE_DIRS}) else() diff --git a/src/net/Net_Slirp.cpp b/src/net/Net_Slirp.cpp index fef4d19c..2200b074 100644 --- a/src/net/Net_Slirp.cpp +++ b/src/net/Net_Slirp.cpp @@ -23,12 +23,7 @@ #include "FIFO.h" #include "Platform.h" -#include - -// "register" is indirectly used by slirp.h but isn't allowed in C++17, this is a workaround -#define register -// Needed for Slirp's definition so we can adjust the opaque pointer in the move constructor -#include +#include #ifdef __WIN32__ #include @@ -163,63 +158,6 @@ Net_Slirp::Net_Slirp(const Platform::SendPacketCallback& callback) noexcept : Ca Ctx = slirp_new(&cfg, &cb, this); } - -Net_Slirp::Net_Slirp(Net_Slirp&& other) noexcept -{ - RXBuffer = other.RXBuffer; - IPv4ID = other.IPv4ID; - Ctx = other.Ctx; - PollListSize = other.PollListSize; - Callback = std::move(other.Callback); - memcpy(PollList, other.PollList, sizeof(PollList)); - - other.RXBuffer = {}; - other.IPv4ID = 0; - other.Ctx = nullptr; - other.PollListSize = 0; - other.Callback = nullptr; - memset(other.PollList, 0, sizeof(other.PollList)); - - if (Ctx) - { - Ctx->opaque = this; - // Gotta ensure that the context doesn't try to pass around a dead object - } -} - -Net_Slirp& Net_Slirp::operator=(Net_Slirp&& other) noexcept -{ - if (this != &other) - { - if (Ctx) - { - slirp_cleanup(Ctx); - } - - RXBuffer = other.RXBuffer; - IPv4ID = other.IPv4ID; - Ctx = other.Ctx; - PollListSize = other.PollListSize; - Callback = std::move(other.Callback); - memcpy(PollList, other.PollList, sizeof(PollList)); - - other.RXBuffer = {}; - other.IPv4ID = 0; - other.Ctx = nullptr; - other.PollListSize = 0; - other.Callback = nullptr; - memset(other.PollList, 0, sizeof(other.PollList)); - - if (Ctx) - { - Ctx->opaque = this; - // Gotta ensure that the context doesn't try to pass around a dead object - } - } - - return *this; -} - Net_Slirp::~Net_Slirp() noexcept { if (Ctx) @@ -229,7 +167,6 @@ Net_Slirp::~Net_Slirp() noexcept } } - void FinishUDPFrame(u8* data, int len) { u8* ipheader = &data[0xE]; diff --git a/src/net/Net_Slirp.h b/src/net/Net_Slirp.h index 5f9b6587..256b3058 100644 --- a/src/net/Net_Slirp.h +++ b/src/net/Net_Slirp.h @@ -24,7 +24,7 @@ #include "Platform.h" #include "NetDriver.h" -#include +#include #ifdef __WIN32__ #include From b47563e888630ae7c95992d60e311f37c9f47466 Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Wed, 7 Aug 2024 17:28:25 +0200 Subject: [PATCH 03/18] Apply FixInterfaceIncludes to the slirp package again to work around package inconsistencies --- src/net/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/net/CMakeLists.txt b/src/net/CMakeLists.txt index 082c00fb..0d4698df 100644 --- a/src/net/CMakeLists.txt +++ b/src/net/CMakeLists.txt @@ -1,3 +1,5 @@ +include(FixInterfaceIncludes) + add_library(net-utils STATIC Net.cpp Net_PCap.cpp @@ -13,6 +15,7 @@ target_include_directories(net-utils PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..") option(USE_SYSTEM_LIBSLIRP "Use system libslirp instead of the bundled version" OFF) if (USE_SYSTEM_LIBSLIRP) pkg_check_modules(Slirp REQUIRED IMPORTED_TARGET slirp) + fix_interface_includes(PkgConfig::Slirp) target_link_libraries(net-utils PRIVATE PkgConfig::Slirp) else() add_subdirectory(libslirp EXCLUDE_FROM_ALL) From 4359bccfcbec2b149d4b01870897bf849e9a69ce Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Wed, 7 Aug 2024 17:45:56 +0200 Subject: [PATCH 04/18] fix the slirp shit some more --- src/net/CMakeLists.txt | 5 ++--- src/net/Net_Slirp.cpp | 2 +- src/net/Net_Slirp.h | 2 +- src/net/libslirp/CMakeLists.txt | 6 +++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/net/CMakeLists.txt b/src/net/CMakeLists.txt index 0d4698df..62bb557c 100644 --- a/src/net/CMakeLists.txt +++ b/src/net/CMakeLists.txt @@ -16,9 +16,8 @@ option(USE_SYSTEM_LIBSLIRP "Use system libslirp instead of the bundled version" if (USE_SYSTEM_LIBSLIRP) pkg_check_modules(Slirp REQUIRED IMPORTED_TARGET slirp) fix_interface_includes(PkgConfig::Slirp) - target_link_libraries(net-utils PRIVATE PkgConfig::Slirp) + target_link_libraries(net-utils PUBLIC PkgConfig::Slirp) else() add_subdirectory(libslirp EXCLUDE_FROM_ALL) - target_include_directories(net-utils SYSTEM PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/libslirp/glib") - target_link_libraries(net-utils PRIVATE slirp) + target_link_libraries(net-utils PUBLIC slirp) endif() diff --git a/src/net/Net_Slirp.cpp b/src/net/Net_Slirp.cpp index 2200b074..0386c586 100644 --- a/src/net/Net_Slirp.cpp +++ b/src/net/Net_Slirp.cpp @@ -23,7 +23,7 @@ #include "FIFO.h" #include "Platform.h" -#include +#include #ifdef __WIN32__ #include diff --git a/src/net/Net_Slirp.h b/src/net/Net_Slirp.h index 256b3058..5f9b6587 100644 --- a/src/net/Net_Slirp.h +++ b/src/net/Net_Slirp.h @@ -24,7 +24,7 @@ #include "Platform.h" #include "NetDriver.h" -#include +#include #ifdef __WIN32__ #include diff --git a/src/net/libslirp/CMakeLists.txt b/src/net/libslirp/CMakeLists.txt index 67b8ac78..99d0b3f7 100644 --- a/src/net/libslirp/CMakeLists.txt +++ b/src/net/libslirp/CMakeLists.txt @@ -49,9 +49,9 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/libslirp-version.h.in" "${CMAKE_ add_library(slirp STATIC ${SOURCES}) target_compile_definitions(slirp PUBLIC LIBSLIRP_STATIC_BUILD) -target_include_directories(slirp PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/glib") -target_include_directories(slirp PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") -target_include_directories(slirp PUBLIC "${CMAKE_CURRENT_BINARY_DIR}") +target_include_directories(slirp SYSTEM PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/glib") +target_include_directories(slirp SYSTEM PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") +target_include_directories(slirp SYSTEM PUBLIC "${CMAKE_CURRENT_BINARY_DIR}") target_compile_definitions(slirp PRIVATE BUILDING_LIBSLIRP) target_compile_definitions(slirp PRIVATE "G_LOG_DOMAIN=\"Slirp\"") From 53c58bd7773541f93cde45e0b2fcc0b7b40afbc2 Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Wed, 7 Aug 2024 17:49:29 +0200 Subject: [PATCH 05/18] fix potential issue with glib shim min/max defines --- src/net/libslirp/glib/glib.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/net/libslirp/glib/glib.h b/src/net/libslirp/glib/glib.h index e979b12a..3563f7f5 100644 --- a/src/net/libslirp/glib/glib.h +++ b/src/net/libslirp/glib/glib.h @@ -68,10 +68,10 @@ #define GLIB_SIZEOF_VOID_P 8 #ifndef MAX -#define MAX(a, b) (a > b ? a : b) +#define MAX(a, b) ((a) > (b) ? (a) : (b)) #endif #ifndef MIN -#define MIN(a, b) (a < b ? a : b) +#define MIN(a, b) ((a) < (b) ? (a) : (b)) #endif #ifndef TRUE From ec71b15505e8e6f66c22402ba929bea5545c31fc Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Thu, 8 Aug 2024 05:36:06 +0200 Subject: [PATCH 06/18] Add a Nix flake (#2097) Adds a Nix flake, allowing easy building and running of melonDS using the Nix package manager, as well as potentially very stable and reproducible CI in the future. --- flake.lock | 61 +++++++++++++++++++++++++ flake.nix | 72 ++++++++++++++++++++++++++++++ src/frontend/qt_sdl/CMakeLists.txt | 2 + 3 files changed, 135 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..bd799223 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1722813957, + "narHash": "sha256-IAoYyYnED7P8zrBFMnmp7ydaJfwTnwcnqxUElC1I26Y=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "cb9a96f23c491c081b38eab96d22fa958043c9fa", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..5074a7fb --- /dev/null +++ b/flake.nix @@ -0,0 +1,72 @@ +{ + description = "Nintendo DS emulator"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + inherit (pkgs.lib) cmakeBool optionals makeLibraryPath; + inherit (pkgs.stdenv) isLinux isDarwin; + + versionSuffix = with self; if sourceInfo?dirtyShortRev + then sourceInfo.dirtyShortRev + else sourceInfo.shortRev; + + melonDS = pkgs.stdenv.mkDerivation { + pname = "melonDS"; + version = "0.9.5-${versionSuffix}"; + src = ./.; + + nativeBuildInputs = with pkgs; [ + cmake + ninja + pkg-config + kdePackages.wrapQtAppsHook + ]; + + buildInputs = (with pkgs; [ + kdePackages.qtbase + kdePackages.qtmultimedia + extra-cmake-modules + SDL2 + zstd + libarchive + libGL + libslirp + ]) ++ optionals isLinux [ + pkgs.wayland + pkgs.kdePackages.qtwayland + ]; + + cmakeFlags = [ + (cmakeBool "USE_QT6" true) + (cmakeBool "USE_SYSTEM_LIBSLIRP" true) + ]; + + qtWrapperArgs = optionals isLinux [ + "--prefix LD_LIBRARY_PATH : ${makeLibraryPath [ pkgs.libpcap ]}" + ] ++ optionals isDarwin [ + "--prefix DYLD_LIBRARY_PATH : ${makeLibraryPath [ pkgs.libpcap ]}" + ]; + + passthru = { + exePath = if isDarwin then + "/Applications/melonDS.app/Contents/MacOS/melonDS" + else "/bin/melonDS"; + }; + }; + in { + packages.default = melonDS; + apps.default = flake-utils.lib.mkApp { + drv = self.packages.${system}.default; + }; + devShells.default = pkgs.mkShell { + inputsFrom = [ self.packages.${system}.default ]; + }; + } + ); +} diff --git a/src/frontend/qt_sdl/CMakeLists.txt b/src/frontend/qt_sdl/CMakeLists.txt index c857bbbc..fc7819a1 100644 --- a/src/frontend/qt_sdl/CMakeLists.txt +++ b/src/frontend/qt_sdl/CMakeLists.txt @@ -240,6 +240,8 @@ if (UNIX AND NOT APPLE) INTERPROCEDURAL_OPTIMIZATION OFF INTERPROCEDURAL_OPTIMIZATION_RELEASE OFF) endif() +elseif(APPLE) + install(TARGETS melonDS BUNDLE DESTINATION "${CMAKE_INSTALL_PREFIX}/Applications") endif() if (ENABLE_OGLRENDERER) From 8d3187590210f789de8774edfe782bb0624cacc3 Mon Sep 17 00:00:00 2001 From: Arisotura Date: Sat, 10 Aug 2024 23:20:50 +0200 Subject: [PATCH 07/18] Backport LAN (#2131) backport the old LAN feature to the modern melonDS codebase. --- .github/workflows/build-ubuntu.yml | 4 +- cmake/FindENet.cmake | 48 + flake.lock | 6 +- flake.nix | 1 + src/Platform.h | 3 + src/frontend/qt_sdl/CMakeLists.txt | 3 + src/frontend/qt_sdl/EmuInstance.cpp | 50 +- src/frontend/qt_sdl/EmuInstance.h | 4 + src/frontend/qt_sdl/EmuThread.cpp | 1 + src/frontend/qt_sdl/LANDialog.cpp | 405 ++++++ src/frontend/qt_sdl/LANDialog.h | 117 ++ src/frontend/qt_sdl/LANDialog.ui | 51 + src/frontend/qt_sdl/LANStartClientDialog.ui | 117 ++ src/frontend/qt_sdl/LANStartHostDialog.ui | 97 ++ src/frontend/qt_sdl/NetplayDialog.cpp | 181 +++ src/frontend/qt_sdl/NetplayDialog.h | 111 ++ src/frontend/qt_sdl/NetplayDialog.ui | 31 + .../qt_sdl/NetplayStartClientDialog.ui | 107 ++ src/frontend/qt_sdl/NetplayStartHostDialog.ui | 97 ++ src/frontend/qt_sdl/Platform.cpp | 32 +- src/frontend/qt_sdl/Window.cpp | 105 +- src/frontend/qt_sdl/Window.h | 16 + src/frontend/qt_sdl/main.cpp | 70 +- src/frontend/qt_sdl/main.h | 18 +- src/net/CMakeLists.txt | 12 + src/net/LAN.cpp | 1091 +++++++++++++++++ src/net/LAN.h | 156 +++ src/net/LocalMP.cpp | 2 - src/net/LocalMP.h | 19 +- src/net/MPInterface.cpp | 68 + src/net/MPInterface.h | 82 ++ src/net/Netplay.cpp | 1085 ++++++++++++++++ src/net/Netplay.h | 57 + vcpkg.json | 3 +- 34 files changed, 4163 insertions(+), 87 deletions(-) create mode 100644 cmake/FindENet.cmake create mode 100644 src/frontend/qt_sdl/LANDialog.cpp create mode 100644 src/frontend/qt_sdl/LANDialog.h create mode 100644 src/frontend/qt_sdl/LANDialog.ui create mode 100644 src/frontend/qt_sdl/LANStartClientDialog.ui create mode 100644 src/frontend/qt_sdl/LANStartHostDialog.ui create mode 100644 src/frontend/qt_sdl/NetplayDialog.cpp create mode 100644 src/frontend/qt_sdl/NetplayDialog.h create mode 100644 src/frontend/qt_sdl/NetplayDialog.ui create mode 100644 src/frontend/qt_sdl/NetplayStartClientDialog.ui create mode 100644 src/frontend/qt_sdl/NetplayStartHostDialog.ui create mode 100644 src/net/LAN.cpp create mode 100644 src/net/LAN.h create mode 100644 src/net/MPInterface.cpp create mode 100644 src/net/MPInterface.h create mode 100644 src/net/Netplay.cpp create mode 100644 src/net/Netplay.h 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": { From 55980659853185266b8c2ba566479fb6226dbbcc Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Sun, 11 Aug 2024 03:14:14 +0200 Subject: [PATCH 08/18] Netplay.cpp should not include main.h, also fix a format warning --- src/net/Netplay.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/net/Netplay.cpp b/src/net/Netplay.cpp index 68caa43d..1dc04282 100644 --- a/src/net/Netplay.cpp +++ b/src/net/Netplay.cpp @@ -25,7 +25,6 @@ #include "NDS.h" #include "NDSCart.h" -#include "main.h" //#include "IPC.h" #include "Netplay.h" //#include "Input.h" @@ -442,7 +441,7 @@ printf("[MC] start blob type=%d len=%d\n", type, len); 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); + printf("[MC] recv blob data, type=%d pos=%08X len=%08X data=%08lX\n", type, pos, len, pkt->dataLength-16); memcpy(&dst[pos], &buf[16], pkt->dataLength-16); } else if (buf[0] == 0x03) From e290c42360f5f2ae7182c5430234545f3b066876 Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Sun, 11 Aug 2024 04:54:05 +0200 Subject: [PATCH 09/18] flake: add wayland to library path like nixpkgs does --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 961ef245..487bf197 100644 --- a/flake.nix +++ b/flake.nix @@ -49,7 +49,7 @@ ]; qtWrapperArgs = optionals isLinux [ - "--prefix LD_LIBRARY_PATH : ${makeLibraryPath [ pkgs.libpcap ]}" + "--prefix LD_LIBRARY_PATH : ${makeLibraryPath [ pkgs.libpcap pkgs.wayland ]}" ] ++ optionals isDarwin [ "--prefix DYLD_LIBRARY_PATH : ${makeLibraryPath [ pkgs.libpcap ]}" ]; From c6cab9ed41b51694c72b9b817042292df65fefe4 Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Tue, 13 Aug 2024 07:53:09 +0200 Subject: [PATCH 10/18] mac-libs.rb: Make bundling a bit smarter * Resolve symlinks to avoid including the same thing twice (like version-numered dylib symlinks) * Look in all Qt prefix paths for plugins - the package may not necessarily have the same path * reduce install_name_tool invocations to make it a bit faster * change dylib IDs to remove original source path --- src/frontend/qt_sdl/CMakeLists.txt | 4 +- tools/mac-libs.rb | 120 +++++++++++++++++++---------- 2 files changed, 83 insertions(+), 41 deletions(-) diff --git a/src/frontend/qt_sdl/CMakeLists.txt b/src/frontend/qt_sdl/CMakeLists.txt index 656b6535..9cd784aa 100644 --- a/src/frontend/qt_sdl/CMakeLists.txt +++ b/src/frontend/qt_sdl/CMakeLists.txt @@ -63,10 +63,10 @@ else() endif() if (USE_QT6) - find_package(Qt6 COMPONENTS Core Gui Widgets Network Multimedia OpenGL OpenGLWidgets REQUIRED) + find_package(Qt6 COMPONENTS Core Gui Widgets Network Multimedia OpenGL OpenGLWidgets Svg REQUIRED) set(QT_LINK_LIBS Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Network Qt6::Multimedia Qt6::OpenGL Qt6::OpenGLWidgets) else() - find_package(Qt5 COMPONENTS Core Gui Widgets Network Multimedia REQUIRED) + find_package(Qt5 COMPONENTS Core Gui Widgets Network Multimedia Svg REQUIRED) set(QT_LINK_LIBS Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Network Qt5::Multimedia) endif() diff --git a/tools/mac-libs.rb b/tools/mac-libs.rb index e5d4dd57..e609bd8e 100755 --- a/tools/mac-libs.rb +++ b/tools/mac-libs.rb @@ -77,6 +77,19 @@ def expand_load_path(lib, path) return nil end +def detect_framework(lib) + framework = lib.match(/(.*).framework/) + framework = framework.to_s if framework + + if framework + fwname = File.basename(framework) + fwlib = lib.sub(framework + "/", "") + return true, framework, fwname, fwlib + else + return false + end +end + def system_path?(path) path.match(/^\/usr\/lib|^\/System/) != nil end @@ -85,9 +98,10 @@ def system_lib?(lib) system_path? File.dirname(lib) end -def install_name_tool(exec, action, path1, path2 = nil) - args = ["-#{action.to_s}", path1] - args << path2 if path2 != nil +def install_name_tool(exec, *options) + args = options.map do |it| + if it.is_a? Symbol then "-#{it.to_s}" else it end + end Open3.popen3("install_name_tool", *args, exec) do |stdin, stdout, stderr, thread| print stdout.read @@ -99,58 +113,68 @@ def install_name_tool(exec, action, path1, path2 = nil) end def strip(lib) - out, _ = Open3.capture2("strip", "-no_code_signature_warning", "-Sx", lib) + out, _ = Open3.capture2("xcrun", "strip", "-no_code_signature_warning", "-Sx", lib) print out end def fixup_libs(prog, orig_path) throw "fixup_libs: #{prog} doesn't exist" unless File.exist? prog - libs = get_load_libs(prog).map { |it| expand_load_path(orig_path, it) }.select { |it| not system_lib? it[0] } + libs = get_load_libs(prog) + .map { |it| expand_load_path(orig_path, it) } + .select { |it| not system_lib? it[0] } FileUtils.chmod("u+w", prog) strip prog + changes = [] + + isfw, _, fwname, fwlib = detect_framework(prog) + if isfw then + changes += [:id, File.join("@rpath", fwname, fwlib)] + else + changes += [:id, File.join("@rpath", File.basename(prog))] + end + libs.each do |lib| libpath, libtype = lib if File.basename(libpath) == File.basename(prog) if libtype == :absolute - install_name_tool prog, :change, libpath, File.join("@rpath", File.basename(libpath)) + changes += [:change, libpath, File.join("@rpath", File.basename(libpath))] end next end - framework = libpath.match(/(.*).framework/) - framework = framework.to_s if framework - - if framework - fwlib = libpath.sub(framework + "/", "") - fwname = File.basename(framework) + is_framework, fwpath, fwname, fwlib = detect_framework(libpath) + if is_framework unless libtype == :rpath - install_name_tool prog, :change, libpath, File.join("@rpath", fwname, fwlib) + changes += [:change, libpath, File.join("@rpath", fwname, fwlib)] end next if File.exist? File.join(frameworks_dir, fwname) - expath, _ = expand_load_path(orig_path, framework) + expath, _ = expand_load_path(orig_path, fwpath) FileUtils.cp_r(expath, frameworks_dir, preserve: true) FileUtils.chmod_R("u+w", File.join(frameworks_dir, fwname)) fixup_libs File.join(frameworks_dir, fwname, fwlib), libpath else - libname = File.basename(libpath) + reallibpath = File.realpath(libpath) + libname = File.basename(reallibpath) dest = File.join(frameworks_dir, libname) if libtype == :absolute - install_name_tool prog, :change, libpath, File.join("@rpath", libname) + changes += [:change, libpath, File.join("@rpath", libname)] end next if File.exist? dest - expath, _ = expand_load_path(orig_path, libpath) + expath, _ = expand_load_path(orig_path, reallibpath) FileUtils.copy expath, frameworks_dir FileUtils.chmod("u+w", dest) - fixup_libs dest, libpath + fixup_libs dest, reallibpath end end + + install_name_tool(prog, *changes) end if ARGV[0] == "--dmg" @@ -176,14 +200,6 @@ unless File.exist? $bundle and File.exist? File.join($build_dir, "CMakeCache.txt exit 1 end -File.read(File.join($build_dir, "CMakeCache.txt")) - .split("\n") - .find { |it| it.match /Qt(.)_DIR:PATH=(.*)/ } - -qt_major = $1 -qt_dir = $2 -qt_dir = File.absolute_path("#{qt_dir}/../../..") - for lib in get_load_libs(executable) do next if system_lib? lib @@ -196,19 +212,38 @@ for lib in get_load_libs(executable) do $fallback_rpaths << path unless $fallback_rpaths.include? path end -$fallback_rpaths << File.join(qt_dir, "lib") +$qt_major = nil -plugin_paths = [ - File.join(qt_dir, "libexec", "qt#{qt_major}", "plugins"), - File.join(qt_dir, "plugins"), - File.join(qt_dir, "share", "qt", "plugins") -] +qt_dirs = File.read(File.join($build_dir, "CMakeCache.txt")) + .split("\n") + .select { |it| it.match /^Qt([\w]+)_DIR:PATH=.*/ } + .map { |dir| + dir.match /^Qt(5|6).*\=(.*)/ + throw "Inconsistent Qt versions found." if $qt_major != nil && $qt_major != $1 + $qt_major = $1 + File.absolute_path("#{$2}/../../..") + }.uniq -qt_plugins = plugin_paths.find { |file| File.exist? file } -if qt_plugins == nil - puts "Couldn't find Qt plugins, tried looking for:" - plugin_paths.each { |path| puts " - #{path}" } +def locate_plugin(dirs, plugin) + plugin_paths = [ + File.join("plugins", plugin), + File.join("lib", "qt-#{$qt_major}", "plugins", plugin), + File.join("libexec", "qt-#{$qt_major}", "plugins", plugin), + File.join("share", "qt", "plugins", plugin) + ] + + dirs.each do |dir| + plugin_paths.each do |plug| + path = File.join(dir, plug) + return path if File.exists? path + end + end + puts "Couldn't find the required Qt plugin: #{plugin}" + puts "Tried the following prefixes: " + puts dirs.map { |dir| "- #{dir}"}.join("\n") + puts "With the following plugin paths:" + puts plugin_paths.map { |path| "- #{path}"}.join("\n") exit 1 end @@ -217,12 +252,19 @@ fixup_libs(executable, executable) bundle_plugins = File.join($bundle, "Contents", "PlugIns") -want_plugins = ["styles/libqmacstyle.dylib", "platforms/libqcocoa.dylib", "imageformats/libqsvg.dylib"] +want_plugins = [ + "styles/libqmacstyle.dylib", + "platforms/libqcocoa.dylib", + "imageformats/libqsvg.dylib" +] + want_plugins.each do |plug| + pluginpath = locate_plugin(qt_dirs, plug) + destdir = File.join(bundle_plugins, File.dirname(plug)) FileUtils.mkdir_p(destdir) - FileUtils.copy(File.join(qt_plugins, plug), destdir) - fixup_libs File.join(bundle_plugins, plug), File.join(qt_plugins, plug) + FileUtils.copy(pluginpath, destdir) + fixup_libs File.join(bundle_plugins, plug), pluginpath end want_rpath = "@executable_path/../Frameworks" From cb6f60c383a52214e75d9d3971bc8b1799730b76 Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Tue, 13 Aug 2024 07:59:38 +0200 Subject: [PATCH 11/18] ci: Linux runners should install qtsvg for proper bundling --- .github/workflows/build-ubuntu.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml index fc0857e4..c117a768 100644 --- a/.github/workflows/build-ubuntu.yml +++ b/.github/workflows/build-ubuntu.yml @@ -21,7 +21,7 @@ jobs: 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 libenet-dev \ - qt6-{base,base-private,multimedia}-dev libarchive-dev libzstd-dev libfuse2 + qt6-{base,base-private,multimedia,svg}-dev libarchive-dev libzstd-dev libfuse2 - name: Configure run: cmake -B build -G Ninja -DUSE_QT6=ON -DCMAKE_INSTALL_PREFIX=/usr - name: Build @@ -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,libenet}-dev:arm64 \ + {libsdl2,qt6-{base,base-private,multimedia,svg},libarchive,libzstd,libenet}-dev:arm64 \ pkg-config dpkg-dev - name: Check out source uses: actions/checkout@v4 From f54b6311c13b251ded2617e04a98d89e379ddc8c Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Tue, 13 Aug 2024 08:05:36 +0200 Subject: [PATCH 12/18] ubuntu... --- .github/workflows/build-ubuntu.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml index c117a768..1bcf2f66 100644 --- a/.github/workflows/build-ubuntu.yml +++ b/.github/workflows/build-ubuntu.yml @@ -21,7 +21,7 @@ jobs: 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 libenet-dev \ - qt6-{base,base-private,multimedia,svg}-dev libarchive-dev libzstd-dev libfuse2 + qt6-{base,base-private,multimedia}-dev libqt6svg6-dev libarchive-dev libzstd-dev libfuse2 - name: Configure run: cmake -B build -G Ninja -DUSE_QT6=ON -DCMAKE_INSTALL_PREFIX=/usr - name: Build @@ -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,svg},libarchive,libzstd,libenet}-dev:arm64 \ + {libsdl2,qt6-{base,base-private,multimedia},libqt6svg6,libarchive,libzstd,libenet}-dev:arm64 \ pkg-config dpkg-dev - name: Check out source uses: actions/checkout@v4 From deb1ba2bb2bc6e4bf8fef7ca944d56cae23e3f6a Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Tue, 13 Aug 2024 08:40:10 +0200 Subject: [PATCH 13/18] README: Update build instructions to include enet and qtsvg (input and battery dialogs don't render correctly without qtsvg, we should have been installing it) --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3d7c2561..eb8b1358 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ As for the rest, the interface should be pretty straightforward. If you have a q ### Linux 1. Install dependencies: - * Ubuntu 22.04: `sudo apt install cmake extra-cmake-modules libcurl4-gnutls-dev libpcap0.8-dev libsdl2-dev qtbase5-dev qtbase5-private-dev qtmultimedia5-dev libarchive-dev libzstd-dev` - * Older Ubuntu: `sudo apt install cmake extra-cmake-modules libcurl4-gnutls-dev libpcap0.8-dev libsdl2-dev qt5-default qtbase5-private-dev qtmultimedia5-dev libarchive-dev libzstd-dev` - * Arch Linux: `sudo pacman -S base-devel cmake extra-cmake-modules git libpcap sdl2 qt5-base qt5-multimedia libarchive zstd` + * Ubuntu 22.04: `sudo apt install cmake extra-cmake-modules libcurl4-gnutls-dev libpcap0.8-dev libsdl2-dev qtbase5-dev qtbase5-private-dev qtmultimedia5-dev libqt5svg5-dev libarchive-dev libenet-dev libzstd-dev` + * Older Ubuntu: `sudo apt install cmake extra-cmake-modules libcurl4-gnutls-dev libpcap0.8-dev libsdl2-dev qt5-default qtbase5-private-dev qtmultimedia5-dev libqt5svg5-dev libarchive-dev libenet-dev libzstd-dev` + * Arch Linux: `sudo pacman -S base-devel cmake extra-cmake-modules git libpcap sdl2 qt5-base qt5-multimedia qt5-svg libarchive enet zstd` 3. Download the melonDS repository and prepare: ```bash git clone https://github.com/melonDS-emu/melonDS @@ -64,7 +64,7 @@ As for the rest, the interface should be pretty straightforward. If you have a q cd melonDS ``` #### Dynamic builds (with DLLs) -5. Install dependencies: `pacman -S mingw-w64-x86_64-{cmake,SDL2,toolchain,qt5-base,qt5-svg,qt5-multimedia,qt5-tools,libarchive,zstd}` +5. Install dependencies: `pacman -S mingw-w64-x86_64-{cmake,SDL2,toolchain,qt5-base,qt5-svg,qt5-multimedia,qt5-svg,qt5-tools,libarchive,enet,zstd}` 6. Compile: ```bash cmake -B build @@ -75,7 +75,7 @@ As for the rest, the interface should be pretty straightforward. If you have a q If everything went well, melonDS and the libraries it needs should now be in the `dist` folder. #### Static builds (without DLLs, standalone executable) -5. Install dependencies: `pacman -S mingw-w64-x86_64-{cmake,SDL2,toolchain,qt5-static,libarchive,zstd}` +5. Install dependencies: `pacman -S mingw-w64-x86_64-{cmake,SDL2,toolchain,qt5-static,libarchive,enet,zstd}` 6. Compile: ```bash cmake -B build -DBUILD_STATIC=ON -DCMAKE_PREFIX_PATH=/mingw64/qt5-static @@ -85,7 +85,7 @@ If everything went well, melonDS should now be in the `build` folder. ### macOS 1. Install the [Homebrew Package Manager](https://brew.sh) -2. Install dependencies: `brew install git pkg-config cmake sdl2 qt@6 libarchive zstd` +2. Install dependencies: `brew install git pkg-config cmake sdl2 qt@6 libarchive enet zstd` 3. Download the melonDS repository and prepare: ```zsh git clone https://github.com/melonDS-emu/melonDS @@ -93,14 +93,14 @@ If everything went well, melonDS should now be in the `build` folder. ``` 4. Compile: ```zsh - cmake -B build -DCMAKE_PREFIX_PATH="$(brew --prefix qt@6);$(brew --prefix libarchive)" -DUSE_QT6=ON + cmake -B build -DCMAKE_PREFIX_PATH="$(brew --prefix qt@6);$(brew --prefix libarchive)" cmake --build build -j$(sysctl -n hw.logicalcpu) ``` If everything went well, melonDS.app should now be in the `build` directory. #### Self-contained app bundle If you want an app bundle that can be distributed to other computers without needing to install dependencies through Homebrew, you can additionally run ` -../tools/mac-bundle.rb melonDS.app` after the build is completed, or add `-DMACOS_BUNDLE_LIBS=ON` to the first CMake command. +../tools/mac-libs.rb .` after the build is completed, or add `-DMACOS_BUNDLE_LIBS=ON` to the first CMake command. ## TODO LIST From 2fff4c0b5aaa2d1883c4fbfe29f5700d775a3dd2 Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Wed, 14 Aug 2024 06:42:06 +0200 Subject: [PATCH 14/18] vcpkg: build qtbase with harfbuzz enabled, fixes windows widget drawing --- vcpkg.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcpkg.json b/vcpkg.json index 445f3139..eb8790c8 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -13,7 +13,7 @@ { "name": "qtbase", "default-features": false, - "features": ["gui", "png", "thread", "widgets", "opengl", "zstd"] + "features": ["gui", "png", "thread", "widgets", "opengl", "zstd", "harfbuzz"] }, { "name": "qtbase", From 5b986d31113955a17bb5c8c335ec5b86d9089fc1 Mon Sep 17 00:00:00 2001 From: Arisotura Date: Wed, 14 Aug 2024 16:47:08 +0200 Subject: [PATCH 15/18] wifi: add hack to facilitate multiplayer connections (extend post-beacon interval when connection is being initiated) --- src/Wifi.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Wifi.cpp b/src/Wifi.cpp index b097da1e..3eaa9fd3 100644 --- a/src/Wifi.cpp +++ b/src/Wifi.cpp @@ -1645,9 +1645,21 @@ bool Wifi::CheckRX(int type) // 0=regular 1=MP replies 2=MP host frames *(u16*)&RXBuffer[6] = txrate; *(u16*)&RXBuffer[8] = framelen; + u16 frametype = (framectl & 0x00FF); bool macgood = (RXBuffer[12 + 4] & 0x01) || MACEqual(&RXBuffer[12 + 4], (u8*)&IOPORT(W_MACAddr0)); - if (((framectl & 0x00FF) == 0x0010) && timestamp && macgood) + // HACK: when receiving auth/assoc frames, extend the post-beacon interval + // during MP comm, the host will periodically wake up to send a beacon, and stay awake during the + // post-beacon interval to see if any clients are trying to connect + // the auth/assoc procedure would normally fit during that window, but when we are emulating wifi + // and not yet synced, these frames may lag behind, preventing a successful connection + if ((frametype == 0x00B0 || frametype == 0x0010 || frametype == 0x0000) && timestamp && macgood) + { + if (IOPORT(W_BeaconCount2)) + IOPORT(W_BeaconCount2) += 10; + } + + if ((frametype == 0x0010) && timestamp && macgood) { // if receiving an association response: get the sync value from the host @@ -1666,7 +1678,7 @@ bool Wifi::CheckRX(int type) // 0=regular 1=MP replies 2=MP host frames RXTimestamp = 0; StartRX(); } - else if (((framectl & 0x00FF) == 0x00C0) && timestamp && macgood && IsMPClient) + else if ((frametype == 0x00C0) && timestamp && macgood && IsMPClient) { IsMP = false; IsMPClient = false; From 0e6235a7c4d3e69940a6deae158a5a91dfbfa612 Mon Sep 17 00:00:00 2001 From: Arisotura Date: Thu, 15 Aug 2024 13:34:27 +0200 Subject: [PATCH 16/18] LAN: remember player name and max players setting --- src/frontend/qt_sdl/Config.cpp | 4 +++- src/frontend/qt_sdl/LANDialog.cpp | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/frontend/qt_sdl/Config.cpp b/src/frontend/qt_sdl/Config.cpp index 998a03ec..5997d543 100644 --- a/src/frontend/qt_sdl/Config.cpp +++ b/src/frontend/qt_sdl/Config.cpp @@ -71,7 +71,8 @@ DefaultList DefaultInts = #ifdef GDBSTUB_ENABLED {"Instance*.Gdb.ARM7.Port", 3334}, {"Instance*.Gdb.ARM9.Port", 3333}, -#endif +#endif, + {"LAN.HostNumPlayers", 16}, }; RangeList IntRanges = @@ -90,6 +91,7 @@ RangeList IntRanges = {"Instance*.Window*.ScreenAspectTop", {0, AspectRatiosNum-1}}, {"Instance*.Window*.ScreenAspectBot", {0, AspectRatiosNum-1}}, {"MP.AudioMode", {0, 2}}, + {"LAN.HostNumPlayers", {2, 16}}, }; DefaultList DefaultBools = diff --git a/src/frontend/qt_sdl/LANDialog.cpp b/src/frontend/qt_sdl/LANDialog.cpp index 58baf908..32539e3f 100644 --- a/src/frontend/qt_sdl/LANDialog.cpp +++ b/src/frontend/qt_sdl/LANDialog.cpp @@ -51,10 +51,11 @@ LANStartHostDialog::LANStartHostDialog(QWidget* parent) : QDialog(parent), ui(ne 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) + auto cfg = Config::GetGlobalTable(); + ui->txtPlayerName->setText(cfg.GetQString("LAN.PlayerName")); + ui->sbNumPlayers->setRange(2, 16); - ui->sbNumPlayers->setValue(16); + ui->sbNumPlayers->setValue(cfg.GetInt("LAN.HostNumPlayers")); } LANStartHostDialog::~LANStartHostDialog() @@ -82,6 +83,11 @@ void LANStartHostDialog::done(int r) } lanDlg = LANDialog::openDlg(parentWidget()); + + auto cfg = Config::GetGlobalTable(); + cfg.SetString("LAN.PlayerName", player); + cfg.SetInt("LAN.HostNumPlayers", numplayers); + Config::Save(); } else { @@ -99,6 +105,9 @@ LANStartClientDialog::LANStartClientDialog(QWidget* parent) : QDialog(parent), u setMPInterface(MPInterface_LAN); + auto cfg = Config::GetGlobalTable(); + ui->txtPlayerName->setText(cfg.GetQString("LAN.PlayerName")); + QStandardItemModel* model = new QStandardItemModel(); ui->tvAvailableGames->setModel(model); const QStringList listheader = {"Name", "Players", "Status", "Host IP"}; @@ -209,6 +218,10 @@ void LANStartClientDialog::done(int r) setEnabled(true); lanDlg = LANDialog::openDlg(parentWidget()); + + auto cfg = Config::GetGlobalTable(); + cfg.SetString("LAN.PlayerName", player); + Config::Save(); } else { From 824eb370e4bbf8f4b90279b0488b65bff2bda683 Mon Sep 17 00:00:00 2001 From: Jesse Talavera Date: Mon, 19 Aug 2024 09:21:34 -0400 Subject: [PATCH 17/18] Fix the build when the JIT is disabled (#2139) --- src/ARM.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ARM.cpp b/src/ARM.cpp index 3452d361..4cf70749 100644 --- a/src/ARM.cpp +++ b/src/ARM.cpp @@ -609,6 +609,7 @@ void ARMv5::Execute() while (NDS.ARM9Timestamp < NDS.ARM9Target) { +#ifdef JIT_ENABLED if constexpr (mode == CPUExecuteMode::JIT) { u32 instrAddr = R[15] - ((CPSR&0x20)?2:4); @@ -647,6 +648,7 @@ void ARMv5::Execute() } } else +#endif { if (CPSR & 0x20) // THUMB { @@ -747,6 +749,7 @@ void ARMv4::Execute() while (NDS.ARM7Timestamp < NDS.ARM7Target) { +#ifdef JIT_ENABLED if constexpr (mode == CPUExecuteMode::JIT) { u32 instrAddr = R[15] - ((CPSR&0x20)?2:4); @@ -784,6 +787,7 @@ void ARMv4::Execute() } } else +#endif { if (CPSR & 0x20) // THUMB { From 4f6498c99c5dcdb780371fe936d49e32df148e6e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 21 Aug 2024 13:39:34 +0200 Subject: [PATCH 18/18] Fix "Ejected GBA cart" not adding a newline (#2140) --- src/GBACart.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GBACart.cpp b/src/GBACart.cpp index 1518b6cf..4fd42894 100644 --- a/src/GBACart.cpp +++ b/src/GBACart.cpp @@ -833,7 +833,7 @@ void GBACartSlot::SetCart(std::unique_ptr&& cart) noexcept if (!Cart) { - Log(LogLevel::Info, "Ejected GBA cart"); + Log(LogLevel::Info, "Ejected GBA cart\n"); return; } @@ -917,4 +917,4 @@ void GBACartSlot::SRAMWrite(u32 addr, u8 val) noexcept } -} \ No newline at end of file +}