From 33d3ac28de814b70e07b6b4e6d20de728ae8f354 Mon Sep 17 00:00:00 2001 From: ANR2ME Date: Sat, 18 Jan 2020 02:32:09 +0700 Subject: [PATCH] Added UPnP Support Fix i18n type --- .gitmodules | 3 + CMakeLists.txt | 77 +++++++ Core/Config.cpp | 3 + Core/Config.h | 1 + Core/Core.vcxproj | 13 +- Core/Core.vcxproj.filters | 6 + Core/HLE/proAdhoc.cpp | 17 +- Core/HLE/proAdhocServer.cpp | 9 +- Core/HLE/sceNet.cpp | 15 +- Core/HLE/sceNetAdhoc.cpp | 23 ++- Core/Util/PortManager.cpp | 379 ++++++++++++++++++++++++++++++++++ Core/Util/PortManager.h | 99 +++++++++ UI/GameSettingsScreen.cpp | 1 + Windows/PPSSPP.sln | 14 ++ ext/miniupnp | 1 + ext/miniupnpc.vcxproj | 251 ++++++++++++++++++++++ ext/miniupnpc.vcxproj.filters | 44 ++++ 17 files changed, 938 insertions(+), 18 deletions(-) create mode 100644 Core/Util/PortManager.cpp create mode 100644 Core/Util/PortManager.h create mode 160000 ext/miniupnp create mode 100644 ext/miniupnpc.vcxproj create mode 100644 ext/miniupnpc.vcxproj.filters diff --git a/.gitmodules b/.gitmodules index fe342fd22f..390dd8b41f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "ext/native/tools/prebuilt"] path = ext/native/tools/prebuilt url = https://github.com/hrydgard/ppsspp-freetype.git +[submodule "ext/miniupnp"] + path = ext/miniupnp + url = https://github.com/miniupnp/miniupnp.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 0dc6e2114d..d43cb98324 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,6 +119,7 @@ option(LIBRETRO "Set to ON to generate the libretro target" OFF) # :: Options option(USE_FFMPEG "Build with FFMPEG support" ON) option(USE_DISCORD "Build with Discord support" ON) +option(USE_MINIUPNPC "Build with miniUPnPc support" ON) option(USE_SYSTEM_SNAPPY "Dynamically link against system snappy" ${USE_SYSTEM_SNAPPY}) option(USE_SYSTEM_FFMPEG "Dynamically link against system FFMPEG" ${USE_SYSTEM_FFMPEG}) option(USE_SYSTEM_LIBZIP "Dynamically link against system libzip" ${USE_SYSTEM_LIBZIP}) @@ -1768,6 +1769,8 @@ add_library(${CoreLibName} ${CoreLinkType} Core/Util/AudioFormatNEON.h Core/Util/GameManager.cpp Core/Util/GameManager.h + Core/Util/PortManager.cpp + Core/Util/PortManager.h Core/Util/BlockAllocator.cpp Core/Util/BlockAllocator.h Core/Util/PPGeDraw.cpp @@ -1820,6 +1823,80 @@ if(USE_DISCORD AND NOT IOS AND NOT LIBRETRO) target_link_libraries(${CoreLibName} discord-rpc) endif() +# miniUPnPc integration (MiniUPnPc supposed to works on any POSIX system, not sure if some of these are redundant/not needed tho) +if(USE_MINIUPNPC) + set (MINIUPNPC_VERSION 2.1) # used by miniupnpcstrings.h.cmake + set (MINIUPNPC_API_VERSION 17) + option(UPNPC_BUILD_STATIC "Build static library" TRUE) + option(NO_GETADDRINFO "Define NO_GETADDRINFO" FALSE) + mark_as_advanced(NO_GETADDRINFO) + if (NO_GETADDRINFO) + add_definitions(-DNO_GETADDRINFO) + endif() + + if (NOT WIN32) + add_definitions (-DMINIUPNPC_SET_SOCKET_TIMEOUT) + add_definitions (-D_BSD_SOURCE -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200112L) + else() + add_definitions (-D_WIN32_WINNT=0x0501) # XP or higher for getnameinfo and friends + endif() + if (MACOSX) + add_definitions (-D_DARWIN_C_SOURCE) + endif() + if(WIN32) + add_definitions(-DWIN32 -DMINIUPNP_EXPORTS ) + else() + add_definitions(-fPIC) + endif() + + add_definitions(-DMINIUPNP_STATICLIB) + set(MINIUPNP_DIR "ext/miniupnp/miniupnpc") + include_directories(${CMAKE_CURRENT_BINARY_DIR}) + configure_file(${MINIUPNP_DIR}/miniupnpcstrings.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/miniupnpcstrings.h) # by default miniupnp repo doesn't contains miniupnpcstrings.h and need to be generated + set(MINIUPNPC_SOURCES + # the needed bits of miniupnpc (no python module, no tests, no cli) + ${MINIUPNP_DIR}/connecthostport.c + ${MINIUPNP_DIR}/igd_desc_parse.c + ${MINIUPNP_DIR}/minisoap.c + ${MINIUPNP_DIR}/minissdpc.c + ${MINIUPNP_DIR}/miniupnpc.c + ${MINIUPNP_DIR}/miniwget.c + ${MINIUPNP_DIR}/minixml.c + ${MINIUPNP_DIR}/minixmlvalid.c + ${MINIUPNP_DIR}/portlistingparse.c + ${MINIUPNP_DIR}/receivedata.c + #${MINIUPNP_DIR}/upnpc.c # causing an error due to already existing _main() + ${MINIUPNP_DIR}/upnpcommands.c + ${MINIUPNP_DIR}/upnpdev.c + ${MINIUPNP_DIR}/upnperrors.c + ${MINIUPNP_DIR}/upnpreplyparse.c + ${CMAKE_CURRENT_BINARY_DIR}/miniupnpcstrings.h + ) + if (NOT WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "AmigaOS") + #set(MINIUPNPC_SOURCES ${MINIUPNPC_SOURCES} minissdpc.c) # causing an error due to duplication in MINIUPNPC_SOURCES? + endif() + if (WIN32) + set_source_files_properties(${MINIUPNPC_SOURCES} PROPERTIES COMPILE_DEFINITIONS "MINIUPNP_STATICLIB;MINIUPNP_EXPORTS") + find_library(WINSOCK2_LIBRARY NAMES ws2_32 WS2_32 Ws2_32) + find_library(IPHLPAPI_LIBRARY NAMES iphlpapi) + set(LDLIBS ${WINSOCK2_LIBRARY} ${IPHLPAPI_LIBRARY} ${LDLIBS}) + #elseif (CMAKE_SYSTEM_NAME STREQUAL "Solaris") + # find_library (SOCKET_LIBRARY NAMES socket) + # find_library (NSL_LIBRARY NAMES nsl) + # find_library (RESOLV_LIBRARY NAMES resolv) + # set (LDLIBS ${SOCKET_LIBRARY} ${NSL_LIBRARY} ${RESOLV_LIBRARY} ${LDLIBS}) + endif() + if (MSVC) + # Suppress noise warnings + target_compile_definitions(miniupnpc _CRT_SECURE_NO_WARNINGS _WINSOCK_DEPRECATED_NO_WARNINGS) + endif() + if (UPNPC_BUILD_STATIC) + add_library(miniupnpc STATIC ${MINIUPNPC_SOURCES}) + target_link_libraries(${CoreLibName} miniupnpc ${LDLIBS}) + set(UPNPC_LIBRARY miniupnpc) + endif() +endif() + setup_target_project(${CoreLibName} Core) # Generate git-version.cpp at build time. diff --git a/Core/Config.cpp b/Core/Config.cpp index 606f3321be..5250751855 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -955,6 +955,8 @@ static ConfigSetting controlSettings[] = { static ConfigSetting networkSettings[] = { ConfigSetting("EnableWlan", &g_Config.bEnableWlan, false, true, true), ConfigSetting("EnableAdhocServer", &g_Config.bEnableAdhocServer, false, true, true), + ConfigSetting("EnableUPnP", &g_Config.bEnableUPnP, false, true, true), + ConfigSetting("EnableNetworkChat", &g_Config.bEnableNetworkChat, false, true, true), ConfigSetting("ChatButtonPosition",&g_Config.iChatButtonPosition,BOTTOM_LEFT,true,true), ConfigSetting("ChatScreenPosition",&g_Config.iChatScreenPosition,BOTTOM_LEFT,true,true), @@ -964,6 +966,7 @@ static ConfigSetting networkSettings[] = { ConfigSetting("QuickChat3", &g_Config.sQuickChat2, "Quick Chat 3", true, true), ConfigSetting("QuickChat4", &g_Config.sQuickChat3, "Quick Chat 4", true, true), ConfigSetting("QuickChat5", &g_Config.sQuickChat4, "Quick Chat 5", true, true), + ConfigSetting(false), }; diff --git a/Core/Config.h b/Core/Config.h index d858b85804..077e75ada4 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -412,6 +412,7 @@ public: // Networking bool bEnableWlan; bool bEnableAdhocServer; + bool bEnableUPnP; int iWlanAdhocChannel; bool bWlanPowerSave; bool bEnableNetworkChat; diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index 6ad9a0c3c0..91f0fd5830 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -139,7 +139,7 @@ Level3 ..\ffmpeg\WindowsInclude;..\ffmpeg\Windows\x86\include;../common;..;../ext/native;../ext/glew;../ext/snappy;../ext/native/ext/libpng17;../ext/zlib;../ext/native/ext - _CRTDBG_MAP_ALLOC;USING_WIN_UI;_CRT_SECURE_NO_WARNINGS;USE_FFMPEG;WIN32;_ARCH_32=1;_M_IX86=1;_DEBUG;_LIB;_UNICODE;UNICODE;%(PreprocessorDefinitions) + _CRTDBG_MAP_ALLOC;USING_WIN_UI;_CRT_SECURE_NO_WARNINGS;USE_FFMPEG;WIN32;_ARCH_32=1;_M_IX86=1;_DEBUG;_LIB;_UNICODE;UNICODE;MINIUPNP_STATICLIB;%(PreprocessorDefinitions) StreamingSIMDExtensions2 Precise true @@ -162,7 +162,7 @@ Level3 ..\ffmpeg\WindowsInclude;..\ffmpeg\Windows\x86_64\include;../common;..;../ext/native;../ext/glew;../ext/snappy;../ext/native/ext/libpng17;../ext/zlib;../ext/native/ext - _CRTDBG_MAP_ALLOC;USING_WIN_UI;_CRT_SECURE_NO_WARNINGS;USE_FFMPEG;WIN32;_ARCH_64=1;_M_X64=1;_DEBUG;_LIB;_UNICODE;UNICODE;%(PreprocessorDefinitions) + _CRTDBG_MAP_ALLOC;USING_WIN_UI;_CRT_SECURE_NO_WARNINGS;USE_FFMPEG;WIN32;_ARCH_64=1;_M_X64=1;_DEBUG;_LIB;_UNICODE;UNICODE;MINIUPNP_STATICLIB;%(PreprocessorDefinitions) NotSet Precise false @@ -241,7 +241,7 @@ false StreamingSIMDExtensions2 Precise - USING_WIN_UI;_CRT_SECURE_NO_WARNINGS;USE_FFMPEG;WIN32;_ARCH_32=1;_M_IX86=1;_LIB;NDEBUG;_UNICODE;UNICODE;%(PreprocessorDefinitions) + USING_WIN_UI;_CRT_SECURE_NO_WARNINGS;USE_FFMPEG;WIN32;_ARCH_32=1;_M_IX86=1;_LIB;NDEBUG;_UNICODE;UNICODE;MINIUPNP_STATICLIB;%(PreprocessorDefinitions) Speed MultiThreaded true @@ -276,7 +276,7 @@ Speed false true - USING_WIN_UI;_CRT_SECURE_NO_WARNINGS;USE_FFMPEG;WIN32;_ARCH_64=1;_M_X64=1;_LIB;NDEBUG;_UNICODE;UNICODE;%(PreprocessorDefinitions) + USING_WIN_UI;_CRT_SECURE_NO_WARNINGS;USE_FFMPEG;WIN32;_ARCH_64=1;_M_X64=1;_LIB;NDEBUG;_UNICODE;UNICODE;MINIUPNP_STATICLIB;%(PreprocessorDefinitions) false MultiThreaded true @@ -847,6 +847,7 @@ + MaxSpeed @@ -1181,6 +1182,7 @@ + @@ -1198,6 +1200,9 @@ {129e5e2b-39c1-4d84-96fe-dfd22dbb4a25} + + {d8a71225-178b-424e-96c1-cc3be2c1b047} + {457f45d2-556f-47bc-a31d-aff0d15beaed} diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index b99fc00e07..f3e5bea144 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -745,6 +745,9 @@ Core + + + Util Core @@ -1387,6 +1390,9 @@ Core + + + Util Core diff --git a/Core/HLE/proAdhoc.cpp b/Core/HLE/proAdhoc.cpp index e27b74e80e..855f991300 100644 --- a/Core/HLE/proAdhoc.cpp +++ b/Core/HLE/proAdhoc.cpp @@ -1765,14 +1765,23 @@ int initNetwork(SceNetAdhocctlAdhocId *adhoc_id){ product_code.type = adhoc_id->type; memcpy(product_code.data, adhoc_id->data, ADHOCCTL_ADHOCID_LEN); + // Switch to Nonblocking Behaviour + changeBlockingMode(metasocket, 1); // Connect to Adhoc Server server_addr.sin_addr = serverIp; - iResult = connect(metasocket,(sockaddr *)&server_addr,sizeof(server_addr)); - if (iResult == SOCKET_ERROR) { + int errorcode = 0; + int cnt = 0; + while ((iResult = connect(metasocket, (sockaddr*)&server_addr, sizeof(server_addr))) == SOCKET_ERROR && (errorcode = errno) != EISCONN && cnt < adhocDefaultTimeout) { + sleep_ms(1); + cnt++; + } + // Switch back to Blocking Behaviour + changeBlockingMode(metasocket, 0); + if (iResult == SOCKET_ERROR && errorcode != EISCONN) { char buffer[512]; - snprintf(buffer, sizeof(buffer), "Socket error (%i) when connecting to AdhocServer [%s/%s:%u]", errno, g_Config.proAdhocServer.c_str(), inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port)); + snprintf(buffer, sizeof(buffer), "Socket error (%i) when connecting to AdhocServer [%s/%s:%u]", errorcode, g_Config.proAdhocServer.c_str(), inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port)); ERROR_LOG(SCENET, "%s", buffer); - host->NotifyUserMessage(buffer, 5.0f, 0x0000ff); + host->NotifyUserMessage(n->T("Failed to connect to Adhoc Server"), 6.0f, 0x0000ff); return iResult; } diff --git a/Core/HLE/proAdhocServer.cpp b/Core/HLE/proAdhocServer.cpp index faa78331d3..a385e7d8e5 100644 --- a/Core/HLE/proAdhocServer.cpp +++ b/Core/HLE/proAdhocServer.cpp @@ -44,6 +44,7 @@ //#include #include "thread/threadutil.h" #include "Common/FileUtil.h" +#include "Core/Util/PortManager.h" #include "Core/Core.h" #include "Core/Host.h" #include "Core/HLE/proAdhocServer.h" @@ -1675,14 +1676,20 @@ int proAdhocServerThread(int port) // (int argc, char * argv[]) int server = create_listen_socket(port); //SERVER_PORT // Created Listening Socket - if(server != -1) + if(server != SOCKET_ERROR) { // Notify User INFO_LOG(SCENET, "AdhocServer: Listening for Connections on TCP Port %u", port); //SERVER_PORT + // Port forward + g_PortManager.Add(port, IP_PROTOCOL_TCP); + // Enter Server Loop result = server_loop(server); + // Remove Port mapping + g_PortManager.Remove(port, IP_PROTOCOL_TCP); + // Notify User INFO_LOG(SCENET, "AdhocServer: Shutdown complete"); } diff --git a/Core/HLE/sceNet.cpp b/Core/HLE/sceNet.cpp index 466c4e3a18..e13c234c4b 100644 --- a/Core/HLE/sceNet.cpp +++ b/Core/HLE/sceNet.cpp @@ -32,6 +32,7 @@ #include "Core/MIPS/MIPS.h" #include "Core/Config.h" #include "Core/MemMapHelpers.h" +#include "Core/Util/PortManager.h" #include "sceKernel.h" #include "sceKernelThread.h" @@ -121,7 +122,11 @@ void __NetInit() { char tmpmac[18]; SceNetEtherAddr mac; getLocalMac(&mac); - INFO_LOG(SCENET, "LocalHost IP will be %s [%s]", inet_ntoa(((sockaddr_in*)&LocalhostIP)->sin_addr), mac2str(&mac, tmpmac)); + INFO_LOG(SCENET, "LocalHost IP will be %s [MAC: %s]", inet_ntoa(((sockaddr_in*)&LocalhostIP)->sin_addr), mac2str(&mac, tmpmac)); + // Only initialize when UPnP is enabled since it takes a few seconds to detect UPnP device (may affect people who don't have UPnP device) + if (g_Config.bEnableUPnP) { + g_PortManager.Init(); + } __ResetInitNetLib(); } @@ -137,6 +142,14 @@ void __NetShutdown() { if (netInited) sceNetTerm(); __ResetInitNetLib(); + + if (g_Config.bEnableUPnP) { + g_PortManager.Clear(); + g_PortManager.Restore(); + g_PortManager.Deinit(); + } + + //PPSSPPIDCleanup(); // To make the ID/IP persistent on every reset, we should just let the OS closes all open handles instead of calling PPSSPPIDCleanup() on every reset } static void __UpdateApctlHandlers(int oldState, int newState, int flag, int error) { diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index d1a43a5749..e9ba3a4268 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -30,6 +30,7 @@ #include "Core/MemMapHelpers.h" #include "Common/ChunkFile.h" #include "Core/MIPS/MIPSCodeUtils.h" +#include "Core/Util/PortManager.h" #include "Core/HLE/HLEHelperThread.h" #include "Core/HLE/FunctionWrappers.h" @@ -55,7 +56,7 @@ bool networkInited; static bool netAdhocMatchingInited; int netAdhocMatchingStarted = 0; -int adhocDefaultTimeout = 2000; +int adhocDefaultTimeout = 2000; //5000 int adhocEventPollDelayMS = 10; int adhocMatchingEventDelayMS = 100; int adhocEventDelayMS = 500; // This will affect the duration of "Connecting..." dialog/message box in .Hack//Link and Naruto Ultimate Ninja Heroes 3 @@ -340,7 +341,7 @@ static int sceNetAdhocPdpCreate(const char *mac, int port, int bufferSize, u32 u getLocalIp(&addr); } - addr.sin_port = htons(port + portOffset ); + addr.sin_port = htons(port + portOffset); // The port might be under 1024 (ie. GTA:VCS use port 1, Ford Street Racing use port 0 (UNUSED_PORT), etc) and already used by other application/host OS, should we add 1024 to the port whenever it tried to use an already used port? // Bound Socket to local Port @@ -377,7 +378,8 @@ static int sceNetAdhocPdpCreate(const char *mac, int port, int bufferSize, u32 u pdp[i] = internal; // Forward Port on Router - //sceNetPortOpen("UDP", port); // I need to figure out how to use this in windows/linux + //sceNetPortOpen("UDP", port); + g_PortManager.Add(port + portOffset, IP_PROTOCOL_UDP); // Success return i + 1; @@ -927,9 +929,10 @@ static int sceNetAdhocPdpDelete(int id, int unknown) { // Remove Port Forward from Router //sceNetPortClose("UDP", sock->lport); + //g_PortManager.Remove(sock->lport + portOffset, IP_PROTOCOL_UDP); // Let's not remove mapping in real-time as it could cause lags/disconnection when joining a room with slow routers // Free Memory - // free(sock); + free(sock); // Free Translation Slot pdp[id - 1] = NULL; @@ -1888,7 +1891,8 @@ static int sceNetAdhocPtpOpen(const char *srcmac, int sport, const char *dstmac, ptp[i] = internal; // Add Port Forward to Router - // sceNetPortOpen("TCP", sport); + //sceNetPortOpen("TCP", sport); + g_PortManager.Add(sport + portOffset, IP_PROTOCOL_TCP); // Return PTP Socket Pointer return i + 1; @@ -2074,7 +2078,8 @@ static int sceNetAdhocPtpAccept(int id, u32 peerMacAddrPtr, u32 peerPortPtr, int ptp[i] = internal; // Add Port Forward to Router - // sceNetPortOpen("TCP", internal->lport); + //sceNetPortOpen("TCP", internal->lport); + g_PortManager.Add(internal->lport + portOffset, IP_PROTOCOL_TCP); INFO_LOG(SCENET, "sceNetAdhocPtpAccept[%i->%i:%u]: Established (%s:%u)", id, i+1, internal->lport, inet_ntoa(peeraddr.sin_addr), internal->pport); @@ -2264,7 +2269,8 @@ static int sceNetAdhocPtpClose(int id, int unknown) { closesocket(socket->id); // Remove Port Forward from Router - // sceNetPortClose("TCP", socket->lport); + //sceNetPortClose("TCP", socket->lport); + //g_PortManager.Remove(socket->lport + portOffset, IP_PROTOCOL_TCP); // Let's not remove mapping in real-time as it could cause lags/disconnection when joining a room with slow routers // Free Memory free(socket); @@ -2393,7 +2399,8 @@ static int sceNetAdhocPtpListen(const char *srcmac, int sport, int bufsize, int ptp[i] = internal; // Add Port Forward to Router - // sceNetPortOpen("TCP", sport); + //sceNetPortOpen("TCP", sport); + g_PortManager.Add(sport + portOffset, IP_PROTOCOL_TCP); // Return PTP Socket Pointer return i + 1; diff --git a/Core/Util/PortManager.cpp b/Core/Util/PortManager.cpp new file mode 100644 index 0000000000..99e619d0b6 --- /dev/null +++ b/Core/Util/PortManager.cpp @@ -0,0 +1,379 @@ +// Copyright (c) 2013- PPSSPP Project. + +// This program 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, version 2.0 or later versions. + +// Copyright (c) 2012- PPSSPP Project. + +// This program 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, version 2.0 or later versions. + +// This program 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 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +// Most of the code are based on https://github.com/RJ/libportfwd and updated to the latest miniupnp library +// All credit goes to him and the official miniupnp project! http://miniupnp.free.fr/ + + +#include +#include +#include +#include +#include "Core/HLE/proAdhoc.h" // This import is only used to get product id that was used to connect to adhoc server +#include "Core/Util/PortManager.h" +#include "i18n/i18n.h" + + +PortManager g_PortManager; + +PortManager::PortManager(): + urls(0), + datas(0), + m_InitState(UPNP_INITSTATE_NONE), + m_LocalPort(UPNP_LOCAL_PORT_ANY) { +} + +PortManager::~PortManager() { + Clear(); + Restore(); + Deinit(); +} + +void PortManager::Deinit() { + if (urls) { + FreeUPNPUrls(urls); + free(urls); + urls = NULL; + } + if (datas) { + free(datas); + datas = NULL; + } + m_otherPortList.clear(); m_otherPortList.shrink_to_fit(); + m_portList.clear(); m_portList.shrink_to_fit(); + m_lanip.clear(); + m_defaultDesc.clear(); + m_LocalPort = UPNP_LOCAL_PORT_ANY; + m_InitState = UPNP_INITSTATE_DONE; +} + +bool PortManager::Init(const unsigned int timeout) { + // Windows: Assuming WSAStartup already called beforehand + struct UPNPDev* devlist; + struct UPNPDev* dev; + char* descXML; + int descXMLsize = 0; + int descXMLstatus = 0; + int localport = m_LocalPort; // UPNP_LOCAL_PORT_ANY (0), or UPNP_LOCAL_PORT_SAME (1) as an alias for 1900 for backwards compatability? + int ipv6 = 0; // 0 = IPv4, 1 = IPv6 + unsigned char ttl = 2; // defaulting to 2 + int error = 0; + + INFO_LOG(SCENET, "PortManager::Init(%d)", timeout); + if (!g_Config.bEnableUPnP) { + ERROR_LOG(SCENET, "PortManager::Init - UPnP is Disabled on Networking Settings"); + return false; + } + + if (m_InitState != UPNP_INITSTATE_NONE) { + switch (m_InitState) + { + case UPNP_INITSTATE_BUSY: { + WARN_LOG(SCENET, "PortManager - Initialization already in progress"); + return false; + } + // We should redetect UPnP just in case the player switched to a different network in the middle + /*case UPNP_INITSTATE_DONE: { + WARN_LOG(SCENET, "PortManager - Already Initialized"); + return false; + } + */ + default: + break; + } + } + m_InitState = UPNP_INITSTATE_BUSY; + urls = (UPNPUrls*)malloc(sizeof(struct UPNPUrls)); + datas = (IGDdatas*)malloc(sizeof(struct IGDdatas)); + memset(urls, 0, sizeof(struct UPNPUrls)); + memset(datas, 0, sizeof(struct IGDdatas)); + + devlist = upnpDiscover(timeout, NULL, NULL, localport, ipv6, ttl, &error); + if (devlist) + { + dev = devlist; + while (dev) + { + if (strstr(dev->st, "InternetGatewayDevice")) + break; + dev = dev->pNext; + } + if (!dev) + dev = devlist; // defaulting to first device + + INFO_LOG(SCENET, "PortManager - UPnP device: [desc: %s] [st: %s]", dev->descURL, dev->st); + + descXML = (char*)miniwget(dev->descURL, &descXMLsize, dev->scope_id, &descXMLstatus); + if (descXML) + { + parserootdesc(descXML, descXMLsize, datas); + free(descXML); descXML = 0; + GetUPNPUrls(urls, datas, dev->descURL, dev->scope_id); + } + + // Get LAN IP address + char lanaddr[64] = "unset"; + int status = UPNP_GetValidIGD(devlist, urls, datas, lanaddr, sizeof(lanaddr)); //possible "status" values, 0 = NO IGD found, 1 = A valid connected IGD has been found, 2 = A valid IGD has been found but it reported as not connected, 3 = an UPnP device has been found but was not recognized as an IGD + m_lanip = std::string(lanaddr); + INFO_LOG(SCENET, "PortManager - Detected LAN IP: %s", m_lanip.c_str()); + + // Additional Info + char connectionType[64] = ""; + if (UPNP_GetConnectionTypeInfo(urls->controlURL, datas->first.servicetype, connectionType) != UPNPCOMMAND_SUCCESS) { + WARN_LOG(SCENET, "PortManager - GetConnectionTypeInfo failed"); + } + else { + INFO_LOG(SCENET, "PortManager - Connection Type: %s", connectionType); + } + + // Using Game ID & Player Name as default description for mapping (prioritizing the ID sent by the game to Adhoc server) + char productid[10] = { 0 }; + memcpy(productid, product_code.data, sizeof(product_code.data)); + std::string gameID = std::string(productid); + if (productid[0] == '\0') { + gameID = g_paramSFO.GetDiscID(); + } + m_defaultDesc = "PPSSPP:" + gameID + ":" + g_Config.sNickName; + + freeUPNPDevlist(devlist); + + //m_LocalPort = localport; // We shouldn't keep the right port for the next game reset if we wanted to redetect UPnP + m_InitState = UPNP_INITSTATE_DONE; + RefreshPortList(); + return true; + } + ERROR_LOG(SCENET, "PortManager - upnpDiscover failed (error: %d) or No UPnP device detected", error); + auto n = GetI18NCategory("Networking"); + host->NotifyUserMessage(n->T("Unable to find UPnP device"), 6.0f, 0x0000ff); + m_InitState = UPNP_INITSTATE_NONE; + return false; +} + +int PortManager::GetInitState() { + return m_InitState; +} + +bool PortManager::Add(unsigned short port, const char* protocol) { + char port_str[16]; + std::string leaseDuration = "43200"; // range(0-604800) in seconds (0 = Indefinite/permanent). Some routers doesn't support non-zero value + int r; + + INFO_LOG(SCENET, "PortManager::Add(%d, %s)", port, protocol); + if (urls == NULL || urls->controlURL == NULL || urls->controlURL[0] == '\0') + { + if (g_Config.bEnableUPnP) WARN_LOG(SCENET, "PortManager::Add - the init was not done !"); + return false; + } + sprintf(port_str, "%d", port); + // Only add new port map if it's not previously created by PPSSPP for current IP + auto el_it = std::find_if(m_portList.begin(), m_portList.end(), + [port_str, protocol](const std::pair &el) { return el.first == port_str && el.second == protocol; }); + if (el_it == m_portList.end()) { + auto el_it = std::find_if(m_otherPortList.begin(), m_otherPortList.end(), + [port_str, protocol](const PortMap& el) { return el.extPort_str == port_str && el.protocol == protocol; }); + if (el_it != m_otherPortList.end()) { + // Try to delete the port mapping before we create it, just in case we have dangling port mapping from the daemon not being shut down correctly or the port was taken by other + r = UPNP_DeletePortMapping(urls->controlURL, datas->first.servicetype, port_str, protocol, NULL); + } + r = UPNP_AddPortMapping(urls->controlURL, datas->first.servicetype, + port_str, port_str, m_lanip.c_str(), m_defaultDesc.c_str(), protocol, NULL, leaseDuration.c_str()); + if (r != 0) + { + ERROR_LOG(SCENET, "PortManager - AddPortMapping failed (error: %d)", r); + auto n = GetI18NCategory("Networking"); + host->NotifyUserMessage(n->T("UPnP need to be reinitialized"), 6.0f, 0x0000ff); + Deinit(); // Most of the time errors occurred because the router is no longer reachable (ie. changed networks) so we should invalidate the state to prevent further lags due to timeouts + return false; + } + m_portList.push_front({ port_str, protocol }); + // Keep tracks of it to be restored later if it belongs to others + if (el_it != m_otherPortList.end()) el_it->taken = true; + } + return true; +} + +bool PortManager::Remove(unsigned short port, const char* protocol) { + char port_str[16]; + + INFO_LOG(SCENET, "PortManager::Remove(%d, %s)", port, protocol); + if (urls == NULL || urls->controlURL == NULL || urls->controlURL[0] == '\0') + { + if (g_Config.bEnableUPnP) WARN_LOG(SCENET, "PortManager::Remove - the init was not done !"); + return false; + } + sprintf(port_str, "%d", port); + int r = UPNP_DeletePortMapping(urls->controlURL, datas->first.servicetype, port_str, protocol, NULL); + if (r != 0) + { + ERROR_LOG(SCENET, "PortManager - DeletePortMapping failed (error: %d)", r); + auto n = GetI18NCategory("Networking"); + host->NotifyUserMessage(n->T("UPnP need to be reinitialized"), 6.0f, 0x0000ff); + Deinit(); // Most of the time errors occurred because the router is no longer reachable (ie. changed networks) so we should invalidate the state to prevent further lags due to timeouts + return false; + } + for (auto it = m_portList.begin(); it != m_portList.end(); ) { + (it->first == port_str && it->second == protocol) ? it = m_portList.erase(it) : ++it; + } + return true; +} + +bool PortManager::Restore() { + int r; + INFO_LOG(SCENET, "PortManager::Restore()"); + if (urls == NULL || urls->controlURL == NULL || urls->controlURL[0] == '\0') + { + if (g_Config.bEnableUPnP) WARN_LOG(SCENET, "PortManager::Remove - the init was not done !"); + return false; + } + for (auto it = m_otherPortList.begin(); it != m_otherPortList.end(); ++it) { + if (it->taken) { + auto port_str = it->extPort_str; + auto protocol = it->protocol; + // Remove it first if it's still being taken by PPSSPP + auto el_it = std::find_if(m_portList.begin(), m_portList.end(), + [port_str, protocol](const std::pair& el) { return el.first == port_str && el.second == protocol; }); + if (el_it != m_portList.end()) { + r = UPNP_DeletePortMapping(urls->controlURL, datas->first.servicetype, port_str.c_str(), protocol.c_str(), NULL); + if (r == 0) { + m_portList.erase(el_it); + } + else { + ERROR_LOG(SCENET, "PortManager::Restore - DeletePortMapping failed (error: %d)", r); + return false; // Might be better not to exit here, but exiting a loop will avoid long timeouts in the case the router is no longer reachable + } + } + // Add the original owner back + r = UPNP_AddPortMapping(urls->controlURL, datas->first.servicetype, + it->extPort_str.c_str(), it->intPort_str.c_str(), it->lanip.c_str(), it->desc.c_str(), it->protocol.c_str(), it->remoteHost.c_str(), it->duration.c_str()); + if (r == 0) { + it->taken = false; + } + else { + ERROR_LOG(SCENET, "PortManager::Restore - AddPortMapping failed (error: %d)", r); + return false; // Might be better not to exit here, but exiting a loop will avoid long timeouts in the case the router is no longer reachable + } + } + } + return true; +} + +bool PortManager::Clear() { + int r; + int i = 0; + char index[6]; + char intAddr[40]; + char intPort[6]; + char extPort[6]; + char protocol[4]; + char desc[80]; + char enabled[6]; + char rHost[64]; + char duration[16]; + + INFO_LOG(SCENET, "PortManager::Clear()"); + if (urls == NULL || urls->controlURL == NULL || urls->controlURL[0] == '\0') + { + if (g_Config.bEnableUPnP) WARN_LOG(SCENET, "PortManager::Clear - the init was not done !"); + return false; + } + //unsigned int num = 0; + //UPNP_GetPortMappingNumberOfEntries(urls->controlURL, datas->first.servicetype, &num); // Not supported by many routers + do { + snprintf(index, 6, "%d", i); + rHost[0] = '\0'; enabled[0] = '\0'; + duration[0] = '\0'; desc[0] = '\0'; + extPort[0] = '\0'; intPort[0] = '\0'; intAddr[0] = '\0'; + r = UPNP_GetGenericPortMappingEntry(urls->controlURL, + datas->first.servicetype, + index, + extPort, intAddr, intPort, + protocol, desc, enabled, + rHost, duration); + // Only removes port mappings created by PPSSPP for current LAN IP + if (r == 0 && intAddr == m_lanip && std::strncmp(desc, "PPSSPP", 6) == 0) { + int r2 = UPNP_DeletePortMapping(urls->controlURL, datas->first.servicetype, extPort, protocol, rHost); + if (r2 != 0) + { + ERROR_LOG(SCENET, "PortManager::Clear - DeletePortMapping(%s, %s) failed (error: %d)", extPort, protocol, r2); + return false; + } + else { + i--; + for (auto it = m_portList.begin(); it != m_portList.end(); ) { + (it->first == extPort && it->second == protocol) ? it = m_portList.erase(it) : ++it; + } + } + } + i++; + } while (r == 0); + return true; +} + +bool PortManager::RefreshPortList() { + int r; + int i = 0; + char index[6]; + char intAddr[40]; + char intPort[6]; + char extPort[6]; + char protocol[4]; + char desc[80]; + char enabled[6]; + char rHost[64]; + char duration[16]; + + INFO_LOG(SCENET, "PortManager::RefreshPortList()"); + if (urls == NULL || urls->controlURL == NULL || urls->controlURL[0] == '\0') + { + if (g_Config.bEnableUPnP) WARN_LOG(SCENET, "PortManager::RefreshPortList - the init was not done !"); + return false; + } + m_portList.clear(); + m_otherPortList.clear(); + //unsigned int num = 0; + //UPNP_GetPortMappingNumberOfEntries(urls->controlURL, datas->first.servicetype, &num); // Not supported by many routers + do { + snprintf(index, 6, "%d", i); + rHost[0] = '\0'; enabled[0] = '\0'; + duration[0] = '\0'; desc[0] = '\0'; + extPort[0] = '\0'; intPort[0] = '\0'; intAddr[0] = '\0'; + r = UPNP_GetGenericPortMappingEntry(urls->controlURL, + datas->first.servicetype, + index, + extPort, intAddr, intPort, + protocol, desc, enabled, + rHost, duration); + if (r == 0) { + // Only include port mappings created by PPSSPP for current LAN IP + if (intAddr == m_lanip && std::strncmp(desc, "PPSSPP", 6) == 0) { + m_portList.push_back({ extPort, protocol }); + } + // Port mappings belong to others that might be taken by PPSSPP later + else { + m_otherPortList.push_back({ false, protocol, extPort, intPort, intAddr, rHost, desc, duration, enabled }); + } + } + i++; + } while (r == 0); + return true; +} diff --git a/Core/Util/PortManager.h b/Core/Util/PortManager.h new file mode 100644 index 0000000000..176200c53e --- /dev/null +++ b/Core/Util/PortManager.h @@ -0,0 +1,99 @@ +// Copyright (c) 2013- PPSSPP Project. + +// This program 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, version 2.0 or later versions. + +// This program 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 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +// Most of the code are based on https://github.com/RJ/libportfwd and updated to the latest miniupnp library +// All credit goes to him and the official miniupnp project! http://miniupnp.free.fr/ + + +#pragma once + + +#ifndef MINIUPNP_STATICLIB +#define MINIUPNP_STATICLIB +#endif + +#include "ext/miniupnp/miniupnpc/miniwget.h" +#include "ext/miniupnp/miniupnpc/miniupnpc.h" +#include "ext/miniupnp/miniupnpc/upnpcommands.h" + +#include +#include + +#define IP_PROTOCOL_TCP "TCP" +#define IP_PROTOCOL_UDP "UDP" +#define UPNP_INITSTATE_NONE 0 +#define UPNP_INITSTATE_BUSY 1 +#define UPNP_INITSTATE_DONE 2 + +struct UPNPUrls; +struct IGDdatas; + +struct PortMap { + bool taken; + std::string protocol; + std::string extPort_str; + std::string intPort_str; + std::string lanip; + std::string remoteHost; + std::string desc; + std::string duration; + std::string enabled; +}; + +class PortManager { +public: + PortManager(); + ~PortManager(); + + // Initialize UPnP + // timeout: milliseconds to wait for a router to respond (default = 2000 ms) + bool Init(const unsigned int timeout = 2000); + + // Uninitialize/Reset the state + void Deinit(); + + // Get UPnP Initialization status + int GetInitState(); + + // Add a port & protocol (TCP, UDP or vendor-defined) to map for forwarding + bool Add(unsigned short port, const char* protocol); + + // Remove a port mapping + bool Remove(unsigned short port, const char* protocol); + + // Removes any lingering mapped ports from previous crashes + bool Clear(); + + // Restore ports mapped by others that were taken by PPSSPP, better used after Clear() + bool Restore(); + + // Get port list mapped by PPSSPP for current LAN IP & others + bool RefreshPortList(); + +protected: + struct UPNPUrls* urls = NULL; + struct IGDdatas* datas = NULL; + + int m_InitState = UPNP_INITSTATE_NONE; + int m_LocalPort = UPNP_LOCAL_PORT_ANY; + std::string m_lanip; + std::string m_defaultDesc; + std::deque> m_portList; + std::deque m_otherPortList; +}; + +extern PortManager g_PortManager; diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 44d6f52151..78d860f234 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -690,6 +690,7 @@ void GameSettingsScreen::CreateViews() { networkingSettings->Add(new ChoiceWithValueDisplay(&g_Config.proAdhocServer, n->T("Change proAdhocServer Address"), (const char *)nullptr))->OnClick.Handle(this, &GameSettingsScreen::OnChangeproAdhocServerAddress); networkingSettings->Add(new CheckBox(&g_Config.bEnableAdhocServer, n->T("Enable built-in PRO Adhoc Server", "Enable built-in PRO Adhoc Server"))); + networkingSettings->Add(new CheckBox(&g_Config.bEnableUPnP, n->T("Enable UPnP", "Enable UPnP (need a few seconds to detect)"))); networkingSettings->Add(new ChoiceWithValueDisplay(&g_Config.sMACAddress, n->T("Change Mac Address"), (const char *)nullptr))->OnClick.Handle(this, &GameSettingsScreen::OnChangeMacAddress); networkingSettings->Add(new PopupSliderChoice(&g_Config.iPortOffset, 0, 60000, n->T("Port offset", "Port offset(0 = PSP compatibility)"), 100, screenManager())); diff --git a/Windows/PPSSPP.sln b/Windows/PPSSPP.sln index daf8a56189..a0bacf19db 100644 --- a/Windows/PPSSPP.sln +++ b/Windows/PPSSPP.sln @@ -83,6 +83,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AtlasTool", "..\ext\native\ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZimTool", "..\ext\native\tools\ZimTool\ZimTool.vcxproj", "{B7DED405-40A2-48F8-9382-538F10D442F1}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "miniupnpc", "..\ext\miniupnpc.vcxproj", "{D8A71225-178B-424E-96C1-CC3BE2C1B047}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM = Debug|ARM @@ -337,6 +339,18 @@ Global {B7DED405-40A2-48F8-9382-538F10D442F1}.Release|Win32.ActiveCfg = Release|x64 {B7DED405-40A2-48F8-9382-538F10D442F1}.Release|x64.ActiveCfg = Release|x64 {B7DED405-40A2-48F8-9382-538F10D442F1}.Release|x64.Build.0 = Release|x64 + {D8A71225-178B-424E-96C1-CC3BE2C1B047}.Debug|ARM.ActiveCfg = Debug|Win32 + {D8A71225-178B-424E-96C1-CC3BE2C1B047}.Debug|ARM64.ActiveCfg = Debug|Win32 + {D8A71225-178B-424E-96C1-CC3BE2C1B047}.Debug|Win32.ActiveCfg = Debug|Win32 + {D8A71225-178B-424E-96C1-CC3BE2C1B047}.Debug|Win32.Build.0 = Debug|Win32 + {D8A71225-178B-424E-96C1-CC3BE2C1B047}.Debug|x64.ActiveCfg = Debug|x64 + {D8A71225-178B-424E-96C1-CC3BE2C1B047}.Debug|x64.Build.0 = Debug|x64 + {D8A71225-178B-424E-96C1-CC3BE2C1B047}.Release|ARM.ActiveCfg = Release|Win32 + {D8A71225-178B-424E-96C1-CC3BE2C1B047}.Release|ARM64.ActiveCfg = Release|Win32 + {D8A71225-178B-424E-96C1-CC3BE2C1B047}.Release|Win32.ActiveCfg = Release|Win32 + {D8A71225-178B-424E-96C1-CC3BE2C1B047}.Release|Win32.Build.0 = Release|Win32 + {D8A71225-178B-424E-96C1-CC3BE2C1B047}.Release|x64.ActiveCfg = Release|x64 + {D8A71225-178B-424E-96C1-CC3BE2C1B047}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ext/miniupnp b/ext/miniupnp new file mode 160000 index 0000000000..927e2f3666 --- /dev/null +++ b/ext/miniupnp @@ -0,0 +1 @@ +Subproject commit 927e2f36666f543d59a8080686921069832a3393 diff --git a/ext/miniupnpc.vcxproj b/ext/miniupnpc.vcxproj new file mode 100644 index 0000000000..ef4015103d --- /dev/null +++ b/ext/miniupnpc.vcxproj @@ -0,0 +1,251 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + 16.0 + {D8A71225-178B-424E-96C1-CC3BE2C1B047} + Win32Proj + miniupnpc + 10.0 + + + + StaticLibrary + true + $(DefaultPlatformToolset) + Unicode + + + StaticLibrary + false + $(DefaultPlatformToolset) + false + Unicode + + + StaticLibrary + true + $(DefaultPlatformToolset) + Unicode + + + StaticLibrary + false + $(DefaultPlatformToolset) + false + Unicode + + + + + + + + + + + + + + + + + + + + + true + $(Platform)\$(Configuration)\$(ProjectName)\ + $(SolutionDir)$(Platform)\$(Configuration)\ + + + true + $(Platform)\$(Configuration)\$(ProjectName)\ + + + false + $(Configuration)\$(ProjectName)\ + + + false + $(Platform)\$(Configuration)\$(ProjectName)\ + + + + NotUsing + Level3 + + + false + stdafx.h + DEBUG;_DEBUG;WIN32;_LIB;MINIUPNP_STATICLIB;_CRT_SECURE_NO_WARNINGS;_WINSOCK_DEPRECATED_NO_WARNINGS;%(PreprocessorDefinitions) + Default + false + ProgramDatabase + true + MultiThreadedDebug + + + Windows + true + + + cd miniupnp\miniupnpc\msvc +genminiupnpcstrings.vbs +cd..\..\.. + + + $(OutDir)$(TargetName)$(TargetExt) + iphlpapi.lib + + + + + NotUsing + Level3 + + + DEBUG;_DEBUG;WIN32;_LIB;MINIUPNP_STATICLIB;_CRT_SECURE_NO_WARNINGS;_WINSOCK_DEPRECATED_NO_WARNINGS;%(PreprocessorDefinitions) + false + stdafx.h + ProgramDatabase + true + Default + MultiThreadedDebug + false + + + Windows + true + + + cd miniupnp\miniupnpc\msvc +genminiupnpcstrings.vbs +cd..\..\.. + + + iphlpapi.lib + + + + + NotUsing + Level3 + true + true + + + false + stdafx.h + NDEBUG;WIN32;_LIB;MINIUPNP_STATICLIB;_CRT_SECURE_NO_WARNINGS;_WINSOCK_DEPRECATED_NO_WARNINGS;%(PreprocessorDefinitions) + Speed + false + true + false + MultiThreaded + + + Windows + true + true + true + + + cd miniupnp\miniupnpc\msvc +genminiupnpcstrings.vbs +cd..\..\.. + + + $(OutDir)$(TargetName)$(TargetExt) + iphlpapi.lib + + + + + NotUsing + Level3 + true + true + + + NDEBUG;WIN32;_LIB;MINIUPNP_STATICLIB;_CRT_SECURE_NO_WARNINGS;_WINSOCK_DEPRECATED_NO_WARNINGS;%(PreprocessorDefinitions) + false + stdafx.h + true + Speed + MultiThreaded + false + false + + + Windows + true + true + true + + + iphlpapi.lib + + + cd miniupnp\miniupnpc\msvc +genminiupnpcstrings.vbs +cd..\..\.. + + + + + + \ No newline at end of file diff --git a/ext/miniupnpc.vcxproj.filters b/ext/miniupnpc.vcxproj.filters new file mode 100644 index 0000000000..bda1ac6d7d --- /dev/null +++ b/ext/miniupnpc.vcxproj.filters @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {c82edf03-bf88-478d-aaf5-2854df756cab} + + + + + scripts + + + \ No newline at end of file