From 9ad2826e5600c510b80f4e5b45b9aa83abb16df3 Mon Sep 17 00:00:00 2001 From: comex Date: Sat, 5 Oct 2013 22:55:01 -0400 Subject: [PATCH 001/202] Fix build issues on OS X i386. --- Externals/soundtouch/cpu_detect_x86.cpp | 3 +++ Source/Core/Common/Src/x64FPURoundMode.cpp | 2 +- Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_hid.cpp | 3 +-- Source/Core/Core/Src/PowerPC/JitCommon/JitBackpatch.h | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Externals/soundtouch/cpu_detect_x86.cpp b/Externals/soundtouch/cpu_detect_x86.cpp index dff64d0b1404..184949756086 100644 --- a/Externals/soundtouch/cpu_detect_x86.cpp +++ b/Externals/soundtouch/cpu_detect_x86.cpp @@ -50,6 +50,9 @@ #elif defined(_M_IX86) // windows non-gcc #include + #endif + #ifndef bit_MMX + #define bit_MMX (1 << 23) #define bit_MMX (1 << 23) #define bit_SSE (1 << 25) #define bit_SSE2 (1 << 26) diff --git a/Source/Core/Common/Src/x64FPURoundMode.cpp b/Source/Core/Common/Src/x64FPURoundMode.cpp index 903d58e49979..0a2dc3acb502 100644 --- a/Source/Core/Common/Src/x64FPURoundMode.cpp +++ b/Source/Core/Common/Src/x64FPURoundMode.cpp @@ -70,7 +70,7 @@ namespace FPURoundMode 3 << 8, // FPU_PREC_MASK }; unsigned short _mode; - asm ("fstcw %0" : : "m" (_mode)); + asm ("fstcw %0" : "=m" (_mode)); _mode = (_mode & ~table[3]) | table[mode]; asm ("fldcw %0" : : "m" (_mode)); #endif diff --git a/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_hid.cpp b/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_hid.cpp index f5e6aa6d982c..f2699b989f4e 100644 --- a/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_hid.cpp +++ b/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_hid.cpp @@ -493,7 +493,6 @@ int CWII_IPC_HLE_Device_hid::Align(int num, int alignment) libusb_device_handle * CWII_IPC_HLE_Device_hid::GetDeviceByDevNum(u32 devNum) { - u32 i; libusb_device **list; libusb_device_handle *handle = NULL; ssize_t cnt; @@ -527,7 +526,7 @@ libusb_device_handle * CWII_IPC_HLE_Device_hid::GetDeviceByDevNum(u32 devNum) static bool has_warned_about_drivers = false; #endif - for (i = 0; i < cnt; i++) { + for (ssize_t i = 0; i < cnt; i++) { libusb_device *device = list[i]; struct libusb_device_descriptor desc; int dRet = libusb_get_device_descriptor (device, &desc); diff --git a/Source/Core/Core/Src/PowerPC/JitCommon/JitBackpatch.h b/Source/Core/Core/Src/PowerPC/JitCommon/JitBackpatch.h index 3dc48dd05db6..5859962853ac 100644 --- a/Source/Core/Core/Src/PowerPC/JitCommon/JitBackpatch.h +++ b/Source/Core/Core/Src/PowerPC/JitCommon/JitBackpatch.h @@ -67,7 +67,7 @@ #define CTX_R15 __r15 #define CTX_RIP __rip #elif defined(_M_IX86) - typedef x86_thread_state_t SContext; + typedef x86_thread_state32_t SContext; #define CTX_EAX __eax #define CTX_EBX __ebx #define CTX_ECX __ecx From 2f1aeb162d489a2681a32bb99ef3c1a5fa8654fd Mon Sep 17 00:00:00 2001 From: comex Date: Mon, 23 Sep 2013 20:11:17 -0400 Subject: [PATCH 002/202] Fix warnings in externals. --- Externals/SDL/src/joystick/darwin/SDL_sysjoystick.c | 2 +- Externals/SOIL/CMakeLists.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Externals/SDL/src/joystick/darwin/SDL_sysjoystick.c b/Externals/SDL/src/joystick/darwin/SDL_sysjoystick.c index 32d82f6df39b..8592a00a8080 100644 --- a/Externals/SDL/src/joystick/darwin/SDL_sysjoystick.c +++ b/Externals/SDL/src/joystick/darwin/SDL_sysjoystick.c @@ -186,7 +186,7 @@ static IOReturn HIDCreateOpenDeviceInterface (io_object_t hidDevice, recDevice * plugInResult = (*ppPlugInInterface)->QueryInterface (ppPlugInInterface, CFUUIDGetUUIDBytes (kIOHIDDeviceInterfaceID), (void *) &(pDevice->interface)); if (S_OK != plugInResult) - HIDReportErrorNum ("CouldnŐt query HID class device interface from plugInInterface", plugInResult); + HIDReportErrorNum ("Couldn't query HID class device interface from plugInInterface", plugInResult); (*ppPlugInInterface)->Release (ppPlugInInterface); } else diff --git a/Externals/SOIL/CMakeLists.txt b/Externals/SOIL/CMakeLists.txt index 9657ba60f0f6..bc21a970823d 100644 --- a/Externals/SOIL/CMakeLists.txt +++ b/Externals/SOIL/CMakeLists.txt @@ -3,4 +3,5 @@ set(SRCS image_DXT.c SOIL.c stb_image_aug.c) +add_definitions(-Wno-parentheses-equality) add_library(SOIL STATIC ${SRCS}) From abb3fbdf93fd2b797bec805f44332c7cb5adaf85 Mon Sep 17 00:00:00 2001 From: comex Date: Mon, 23 Sep 2013 21:17:59 -0400 Subject: [PATCH 003/202] Add enet. Nice simple library that can be used as the basis for UDP communication, which is necessary (a) for STUN and (b) potentially to improve performance when it competes with other connections. Todo: Satisfy the license clause about including a copy of the license with all redistributions, as it is not included with binary redistributions. As far as I can tell, Dolphin currently ignores similar clauses in several source files, which should be fixed, and it doesn't seem like a good reason not to use a particular project. --- CMakeLists.txt | 19 +- CMakeTests/FindMiniupnpc.cmake | 179 -- Externals/enet/CMakeLists.txt | 15 + Externals/enet/LICENSE | 7 + Externals/enet/callbacks.c | 53 + Externals/enet/callbacks.h | 27 + Externals/enet/compress.c | 654 ++++++ Externals/enet/enet.h | 587 +++++ .../miniupnpc.vcxproj => enet/enet.vcxproj} | 321 ++- Externals/enet/enet.vcxproj.filters | 23 + Externals/enet/git-revision | 1 + Externals/enet/host.c | 490 +++++ Externals/enet/list.c | 75 + Externals/enet/list.h | 43 + Externals/enet/packet.c | 165 ++ Externals/enet/peer.c | 989 +++++++++ Externals/enet/protocol.c | 1925 +++++++++++++++++ Externals/enet/protocol.h | 199 ++ Externals/enet/time.h | 18 + Externals/enet/types.h | 13 + Externals/enet/unix.c | 547 +++++ Externals/enet/unix.h | 47 + Externals/enet/update-enet.sh | 10 + Externals/enet/utility.h | 12 + Externals/enet/win32.c | 410 ++++ Externals/enet/win32.h | 57 + Externals/miniupnpc/CMakeLists.txt | 37 - Externals/miniupnpc/Changelog.txt | 556 ----- Externals/miniupnpc/LICENSE | 27 - Externals/miniupnpc/README | 66 - Externals/miniupnpc/VERSION | 1 - Externals/miniupnpc/apiversions.txt | 117 - Externals/miniupnpc/miniupnpc.vcxproj.filters | 108 - Externals/miniupnpc/src/bsdqueue.h | 531 ----- Externals/miniupnpc/src/codelength.h | 31 - Externals/miniupnpc/src/connecthostport.c | 250 --- Externals/miniupnpc/src/connecthostport.h | 18 - Externals/miniupnpc/src/declspec.h | 5 - Externals/miniupnpc/src/igd_desc_parse.c | 125 -- Externals/miniupnpc/src/igd_desc_parse.h | 48 - Externals/miniupnpc/src/minisoap.c | 121 -- Externals/miniupnpc/src/minisoap.h | 15 - Externals/miniupnpc/src/minissdpc.c | 133 -- Externals/miniupnpc/src/minissdpc.h | 15 - Externals/miniupnpc/src/miniupnpc.c | 987 --------- Externals/miniupnpc/src/miniupnpc.def | 42 - Externals/miniupnpc/src/miniupnpc.h | 130 -- Externals/miniupnpc/src/miniupnpcmodule.c | 544 ----- Externals/miniupnpc/src/miniupnpcstrings.h | 27 - Externals/miniupnpc/src/miniupnpctypes.h | 19 - Externals/miniupnpc/src/miniwget.c | 574 ----- Externals/miniupnpc/src/miniwget.h | 30 - Externals/miniupnpc/src/minixml.c | 216 -- Externals/miniupnpc/src/minixml.h | 37 - Externals/miniupnpc/src/portlistingparse.c | 159 -- Externals/miniupnpc/src/portlistingparse.h | 71 - Externals/miniupnpc/src/receivedata.c | 104 - Externals/miniupnpc/src/receivedata.h | 19 - Externals/miniupnpc/src/upnpc.c | 715 ------ Externals/miniupnpc/src/upnpcommands.c | 1097 ---------- Externals/miniupnpc/src/upnpcommands.h | 271 --- Externals/miniupnpc/src/upnperrors.c | 104 - Externals/miniupnpc/src/upnperrors.h | 26 - Externals/miniupnpc/src/upnpreplyparse.c | 152 -- Externals/miniupnpc/src/upnpreplyparse.h | 64 - Source/Core/Common/Common.vcxproj | 12 +- Source/Core/Core/Core.vcxproj | 14 +- Source/Core/Core/Src/NetPlayServer.cpp | 164 -- Source/Core/Core/Src/NetPlayServer.h | 20 - Source/Core/DolphinWX/CMakeLists.txt | 5 +- Source/Core/DolphinWX/Dolphin.vcxproj | 20 +- Source/Core/DolphinWX/Src/NetWindow.cpp | 8 - Source/Core/DolphinWX/Src/NetWindow.h | 3 - Source/Dolphin_2010.sln | 183 +- Source/VSProps/Base.props | 2 +- 75 files changed, 6735 insertions(+), 8174 deletions(-) delete mode 100644 CMakeTests/FindMiniupnpc.cmake create mode 100644 Externals/enet/CMakeLists.txt create mode 100644 Externals/enet/LICENSE create mode 100644 Externals/enet/callbacks.c create mode 100644 Externals/enet/callbacks.h create mode 100644 Externals/enet/compress.c create mode 100644 Externals/enet/enet.h rename Externals/{miniupnpc/miniupnpc.vcxproj => enet/enet.vcxproj} (81%) create mode 100644 Externals/enet/enet.vcxproj.filters create mode 100644 Externals/enet/git-revision create mode 100644 Externals/enet/host.c create mode 100644 Externals/enet/list.c create mode 100644 Externals/enet/list.h create mode 100644 Externals/enet/packet.c create mode 100644 Externals/enet/peer.c create mode 100644 Externals/enet/protocol.c create mode 100644 Externals/enet/protocol.h create mode 100644 Externals/enet/time.h create mode 100644 Externals/enet/types.h create mode 100644 Externals/enet/unix.c create mode 100644 Externals/enet/unix.h create mode 100755 Externals/enet/update-enet.sh create mode 100644 Externals/enet/utility.h create mode 100644 Externals/enet/win32.c create mode 100644 Externals/enet/win32.h delete mode 100644 Externals/miniupnpc/CMakeLists.txt delete mode 100644 Externals/miniupnpc/Changelog.txt delete mode 100644 Externals/miniupnpc/LICENSE delete mode 100644 Externals/miniupnpc/README delete mode 100644 Externals/miniupnpc/VERSION delete mode 100644 Externals/miniupnpc/apiversions.txt delete mode 100644 Externals/miniupnpc/miniupnpc.vcxproj.filters delete mode 100644 Externals/miniupnpc/src/bsdqueue.h delete mode 100644 Externals/miniupnpc/src/codelength.h delete mode 100644 Externals/miniupnpc/src/connecthostport.c delete mode 100644 Externals/miniupnpc/src/connecthostport.h delete mode 100644 Externals/miniupnpc/src/declspec.h delete mode 100644 Externals/miniupnpc/src/igd_desc_parse.c delete mode 100644 Externals/miniupnpc/src/igd_desc_parse.h delete mode 100644 Externals/miniupnpc/src/minisoap.c delete mode 100644 Externals/miniupnpc/src/minisoap.h delete mode 100644 Externals/miniupnpc/src/minissdpc.c delete mode 100644 Externals/miniupnpc/src/minissdpc.h delete mode 100644 Externals/miniupnpc/src/miniupnpc.c delete mode 100644 Externals/miniupnpc/src/miniupnpc.def delete mode 100644 Externals/miniupnpc/src/miniupnpc.h delete mode 100644 Externals/miniupnpc/src/miniupnpcmodule.c delete mode 100644 Externals/miniupnpc/src/miniupnpcstrings.h delete mode 100644 Externals/miniupnpc/src/miniupnpctypes.h delete mode 100644 Externals/miniupnpc/src/miniwget.c delete mode 100644 Externals/miniupnpc/src/miniwget.h delete mode 100644 Externals/miniupnpc/src/minixml.c delete mode 100644 Externals/miniupnpc/src/minixml.h delete mode 100644 Externals/miniupnpc/src/portlistingparse.c delete mode 100644 Externals/miniupnpc/src/portlistingparse.h delete mode 100644 Externals/miniupnpc/src/receivedata.c delete mode 100644 Externals/miniupnpc/src/receivedata.h delete mode 100644 Externals/miniupnpc/src/upnpc.c delete mode 100644 Externals/miniupnpc/src/upnpcommands.c delete mode 100644 Externals/miniupnpc/src/upnpcommands.h delete mode 100644 Externals/miniupnpc/src/upnperrors.c delete mode 100644 Externals/miniupnpc/src/upnperrors.h delete mode 100644 Externals/miniupnpc/src/upnpreplyparse.c delete mode 100644 Externals/miniupnpc/src/upnpreplyparse.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cc9f87065adc..76493eaec62a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,6 @@ option(USE_X11 "Enables X11 Support" ON) option(USE_WAYLAND "Enables Wayland Support" OFF) option(USE_GLES "Enables GLES2 And EGL, disables OGL" OFF) option(USE_GLES3 "Enables GLES3 and EGL" OFF) -option(USE_UPNP "Enables UPnP port mapping support" ON) option(DISABLE_WX "Disable wxWidgets (use CLI interface)" OFF) option(FASTLOG "Enable all logs" OFF) @@ -286,7 +285,6 @@ if(ANDROID) add_definitions(-DANDROID) set(USE_X11 0) set(USE_WAYLAND 0) - set(USE_UPNP 0) set(USE_GLES3 1) endif() @@ -510,6 +508,8 @@ include_directories(Source/Core/VideoUICommon/Src) # add_subdirectory(Externals/Bochs_disasm) include_directories(Externals/Bochs_disasm) +include_directories(Externals) +add_subdirectory(Externals/enet) if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Darwin" AND NOT ANDROID) check_lib(LZO lzo2 lzo/lzo1x.h QUIET) @@ -588,21 +588,6 @@ else() include_directories(Externals/SFML/include) endif() -if(USE_UPNP) - if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Darwin" AND NOT ANDROID) - include(FindMiniupnpc) - endif() - if(MINIUPNP_FOUND AND MINIUPNPC_VERSION_1_7_OR_HIGHER) - message("Using shared miniupnpc") - include_directories(${MINIUPNP_INCLUDE_DIR}) - else() - message("Using static miniupnpc from Externals") - add_subdirectory(Externals/miniupnpc) - include_directories(Externals/miniupnpc/src) - endif() - add_definitions(-DUSE_UPNP) -endif() - if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Darwin" AND NOT ANDROID) include(FindPolarSSL) endif() diff --git a/CMakeTests/FindMiniupnpc.cmake b/CMakeTests/FindMiniupnpc.cmake deleted file mode 100644 index 8f919cacb348..000000000000 --- a/CMakeTests/FindMiniupnpc.cmake +++ /dev/null @@ -1,179 +0,0 @@ -# Locate miniupnp library -# This module defines -# MINIUPNP_FOUND, if false, do not try to link to miniupnp -# MINIUPNP_LIBRARY, the miniupnp variant -# MINIUPNP_INCLUDE_DIR, where to find miniupnpc.h and family) -# MINIUPNPC_VERSION_PRE1_6 --> set if we detect the version of miniupnpc is -# pre 1.6 -# MINIUPNPC_VERSION_PRE1_5 --> set if we detect the version of miniupnpc is -# pre 1.5 -# -# Note that the expected include convention is -# #include "miniupnpc.h" -# and not -# #include -# This is because, the miniupnpc location is not standardized and may exist -# in locations other than miniupnpc/ - -#============================================================================= -# Copyright 2011 Mark Vejvoda -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distributed this file outside of CMake, substitute the full -# License text for the above reference.) - -if (MINIUPNP_INCLUDE_DIR AND MINIUPNP_LIBRARY) - # Already in cache, be silent - set(MINIUPNP_FIND_QUIETLY TRUE) -endif (MINIUPNP_INCLUDE_DIR AND MINIUPNP_LIBRARY) - -find_path(MINIUPNP_INCLUDE_DIR miniupnpc.h - PATH_SUFFIXES miniupnpc) -find_library(MINIUPNP_LIBRARY miniupnpc) - -if (MINIUPNP_INCLUDE_DIR AND MINIUPNP_LIBRARY) - set (MINIUPNP_FOUND TRUE) -endif () - -if (MINIUPNP_FOUND) - if (NOT MINIUPNP_FIND_QUIETLY) - message (STATUS "Found the miniupnpc libraries at ${MINIUPNP_LIBRARY}") - message (STATUS "Found the miniupnpc headers at ${MINIUPNP_INCLUDE_DIR}") - endif (NOT MINIUPNP_FIND_QUIETLY) - - message(STATUS "Detecting version of miniupnpc in path: ${MINIUPNP_INCLUDE_DIR}") - - set(CMAKE_REQUIRED_INCLUDES ${MINIUPNP_INCLUDE_DIR}) - set(CMAKE_REQUIRED_LIBRARIES ${MINIUPNP_LIBRARY}) - check_cxx_source_runs(" - #include - #include - #include - #include - int main() - { - static struct UPNPUrls urls; - static struct IGDdatas data; - - GetUPNPUrls (&urls, &data, \"myurl\",0); - - return 0; - }" - MINIUPNPC_VERSION_1_7_OR_HIGHER) - -IF (NOT MINIUPNPC_VERSION_1_7_OR_HIGHER) - set(CMAKE_REQUIRED_INCLUDES ${MINIUPNP_INCLUDE_DIR}) - set(CMAKE_REQUIRED_LIBRARIES ${MINIUPNP_LIBRARY}) - check_cxx_source_runs(" - #include - #include - #include - #include - int main() - { - struct UPNPDev *devlist = NULL; - int upnp_delay = 5000; - const char *upnp_multicastif = NULL; - const char *upnp_minissdpdsock = NULL; - int upnp_sameport = 0; - int upnp_ipv6 = 0; - int upnp_error = 0; - devlist = upnpDiscover(upnp_delay, upnp_multicastif, upnp_minissdpdsock, upnp_sameport, upnp_ipv6, &upnp_error); - - return 0; - }" - MINIUPNPC_VERSION_PRE1_7) - ENDIF() - - IF (NOT MINIUPNPC_VERSION_PRE1_7 AND NOT MINIUPNPC_VERSION_1_7_OR_HIGHER) - set(CMAKE_REQUIRED_INCLUDES ${MINIUPNP_INCLUDE_DIR}) - set(CMAKE_REQUIRED_LIBRARIES ${MINIUPNP_LIBRARY}) - check_cxx_source_runs(" - #include - #include - #include - #include - int main() - { - struct UPNPDev *devlist = NULL; - int upnp_delay = 5000; - const char *upnp_multicastif = NULL; - const char *upnp_minissdpdsock = NULL; - int upnp_sameport = 0; - int upnp_ipv6 = 0; - int upnp_error = 0; - devlist = upnpDiscover(upnp_delay, upnp_multicastif, upnp_minissdpdsock, upnp_sameport); - - return 0; - }" - MINIUPNPC_VERSION_PRE1_6) - - ENDIF() - - IF (NOT MINIUPNPC_VERSION_PRE1_6 AND NOT MINIUPNPC_VERSION_PRE1_7 AND NOT MINIUPNPC_VERSION_1_7_OR_HIGHER) - set(CMAKE_REQUIRED_INCLUDES ${MINIUPNP_INCLUDE_DIR}) - set(CMAKE_REQUIRED_LIBRARIES ${MINIUPNP_LIBRARY}) - check_cxx_source_runs(" - #include - #include - #include - #include - static struct UPNPUrls urls; - static struct IGDdatas data; - int main() - { - char externalIP[16] = ""; - UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIP); - - return 0; - }" - MINIUPNPC_VERSION_1_5_OR_HIGHER) - ENDIF() - - IF (NOT MINIUPNPC_VERSION_1_5_OR_HIGHER AND NOT MINIUPNPC_VERSION_PRE1_6 AND NOT MINIUPNPC_VERSION_PRE1_7 AND NOT MINIUPNPC_VERSION_1_7_OR_HIGHER) - set(CMAKE_REQUIRED_INCLUDES ${MINIUPNP_INCLUDE_DIR}) - set(CMAKE_REQUIRED_LIBRARIES ${MINIUPNP_LIBRARY}) - check_cxx_source_runs(" - #include - #include - #include - #include - static struct UPNPUrls urls; - static struct IGDdatas data; - int main() - { - char externalIP[16] = ""; - UPNP_GetExternalIPAddress(urls.controlURL, data.servicetype, externalIP); - - return 0; - }" - MINIUPNPC_VERSION_PRE1_5) - -ENDIF() - -IF(MINIUPNPC_VERSION_PRE1_5) - message(STATUS "Found miniupnpc version is pre v1.5") -ENDIF() -IF(MINIUPNPC_VERSION_PRE1_6) - message(STATUS "Found miniupnpc version is pre v1.6") -ENDIF() -IF(MINIUPNPC_VERSION_PRE1_7) - message(STATUS "Found miniupnpc version is pre v1.7") -ENDIF() - -IF(NOT MINIUPNPC_VERSION_PRE1_5 AND NOT MINIUPNPC_VERSION_PRE1_6 AND NOT MINIUPNPC_VERSION_PRE1_7) - message(STATUS "Found miniupnpc version is v1.7 or higher") -ENDIF() - -else () - message (STATUS "Could not find miniupnp") -endif () - -MARK_AS_ADVANCED(MINIUPNP_INCLUDE_DIR MINIUPNP_LIBRARY) - diff --git a/Externals/enet/CMakeLists.txt b/Externals/enet/CMakeLists.txt new file mode 100644 index 000000000000..822060ef43e6 --- /dev/null +++ b/Externals/enet/CMakeLists.txt @@ -0,0 +1,15 @@ +set(SRCS + callbacks.c + compress.c + host.c + list.c + packet.c + peer.c + protocol.c) +if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + set(SRCS ${SRCS} win32.c) +else() + set(SRCS ${SRCS} unix.c) +endif() +add_definitions(-Wno-parentheses-equality -DHAS_SOCKLEN_T) +add_library(enet STATIC ${SRCS}) diff --git a/Externals/enet/LICENSE b/Externals/enet/LICENSE new file mode 100644 index 000000000000..87961d3b6821 --- /dev/null +++ b/Externals/enet/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2002-2013 Lee Salzman + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Externals/enet/callbacks.c b/Externals/enet/callbacks.c new file mode 100644 index 000000000000..b3990af1fb91 --- /dev/null +++ b/Externals/enet/callbacks.c @@ -0,0 +1,53 @@ +/** + @file callbacks.c + @brief ENet callback functions +*/ +#define ENET_BUILDING_LIB 1 +#include "enet/enet.h" + +static ENetCallbacks callbacks = { malloc, free, abort }; + +int +enet_initialize_with_callbacks (ENetVersion version, const ENetCallbacks * inits) +{ + if (version < ENET_VERSION_CREATE (1, 3, 0)) + return -1; + + if (inits -> malloc != NULL || inits -> free != NULL) + { + if (inits -> malloc == NULL || inits -> free == NULL) + return -1; + + callbacks.malloc = inits -> malloc; + callbacks.free = inits -> free; + } + + if (inits -> no_memory != NULL) + callbacks.no_memory = inits -> no_memory; + + return enet_initialize (); +} + +ENetVersion +enet_linked_version (void) +{ + return ENET_VERSION; +} + +void * +enet_malloc (size_t size) +{ + void * memory = callbacks.malloc (size); + + if (memory == NULL) + callbacks.no_memory (); + + return memory; +} + +void +enet_free (void * memory) +{ + callbacks.free (memory); +} + diff --git a/Externals/enet/callbacks.h b/Externals/enet/callbacks.h new file mode 100644 index 000000000000..340a4a9896b0 --- /dev/null +++ b/Externals/enet/callbacks.h @@ -0,0 +1,27 @@ +/** + @file callbacks.h + @brief ENet callbacks +*/ +#ifndef __ENET_CALLBACKS_H__ +#define __ENET_CALLBACKS_H__ + +#include + +typedef struct _ENetCallbacks +{ + void * (ENET_CALLBACK * malloc) (size_t size); + void (ENET_CALLBACK * free) (void * memory); + void (ENET_CALLBACK * no_memory) (void); +} ENetCallbacks; + +/** @defgroup callbacks ENet internal callbacks + @{ + @ingroup private +*/ +extern void * enet_malloc (size_t); +extern void enet_free (void *); + +/** @} */ + +#endif /* __ENET_CALLBACKS_H__ */ + diff --git a/Externals/enet/compress.c b/Externals/enet/compress.c new file mode 100644 index 000000000000..784489a7877c --- /dev/null +++ b/Externals/enet/compress.c @@ -0,0 +1,654 @@ +/** + @file compress.c + @brief An adaptive order-2 PPM range coder +*/ +#define ENET_BUILDING_LIB 1 +#include +#include "enet/enet.h" + +typedef struct _ENetSymbol +{ + /* binary indexed tree of symbols */ + enet_uint8 value; + enet_uint8 count; + enet_uint16 under; + enet_uint16 left, right; + + /* context defined by this symbol */ + enet_uint16 symbols; + enet_uint16 escapes; + enet_uint16 total; + enet_uint16 parent; +} ENetSymbol; + +/* adaptation constants tuned aggressively for small packet sizes rather than large file compression */ +enum +{ + ENET_RANGE_CODER_TOP = 1<<24, + ENET_RANGE_CODER_BOTTOM = 1<<16, + + ENET_CONTEXT_SYMBOL_DELTA = 3, + ENET_CONTEXT_SYMBOL_MINIMUM = 1, + ENET_CONTEXT_ESCAPE_MINIMUM = 1, + + ENET_SUBCONTEXT_ORDER = 2, + ENET_SUBCONTEXT_SYMBOL_DELTA = 2, + ENET_SUBCONTEXT_ESCAPE_DELTA = 5 +}; + +/* context exclusion roughly halves compression speed, so disable for now */ +#undef ENET_CONTEXT_EXCLUSION + +typedef struct _ENetRangeCoder +{ + /* only allocate enough symbols for reasonable MTUs, would need to be larger for large file compression */ + ENetSymbol symbols[4096]; +} ENetRangeCoder; + +void * +enet_range_coder_create (void) +{ + ENetRangeCoder * rangeCoder = (ENetRangeCoder *) enet_malloc (sizeof (ENetRangeCoder)); + if (rangeCoder == NULL) + return NULL; + + return rangeCoder; +} + +void +enet_range_coder_destroy (void * context) +{ + ENetRangeCoder * rangeCoder = (ENetRangeCoder *) context; + if (rangeCoder == NULL) + return; + + enet_free (rangeCoder); +} + +#define ENET_SYMBOL_CREATE(symbol, value_, count_) \ +{ \ + symbol = & rangeCoder -> symbols [nextSymbol ++]; \ + symbol -> value = value_; \ + symbol -> count = count_; \ + symbol -> under = count_; \ + symbol -> left = 0; \ + symbol -> right = 0; \ + symbol -> symbols = 0; \ + symbol -> escapes = 0; \ + symbol -> total = 0; \ + symbol -> parent = 0; \ +} + +#define ENET_CONTEXT_CREATE(context, escapes_, minimum) \ +{ \ + ENET_SYMBOL_CREATE (context, 0, 0); \ + (context) -> escapes = escapes_; \ + (context) -> total = escapes_ + 256*minimum; \ + (context) -> symbols = 0; \ +} + +static enet_uint16 +enet_symbol_rescale (ENetSymbol * symbol) +{ + enet_uint16 total = 0; + for (;;) + { + symbol -> count -= symbol->count >> 1; + symbol -> under = symbol -> count; + if (symbol -> left) + symbol -> under += enet_symbol_rescale (symbol + symbol -> left); + total += symbol -> under; + if (! symbol -> right) break; + symbol += symbol -> right; + } + return total; +} + +#define ENET_CONTEXT_RESCALE(context, minimum) \ +{ \ + (context) -> total = (context) -> symbols ? enet_symbol_rescale ((context) + (context) -> symbols) : 0; \ + (context) -> escapes -= (context) -> escapes >> 1; \ + (context) -> total += (context) -> escapes + 256*minimum; \ +} + +#define ENET_RANGE_CODER_OUTPUT(value) \ +{ \ + if (outData >= outEnd) \ + return 0; \ + * outData ++ = value; \ +} + +#define ENET_RANGE_CODER_ENCODE(under, count, total) \ +{ \ + encodeRange /= (total); \ + encodeLow += (under) * encodeRange; \ + encodeRange *= (count); \ + for (;;) \ + { \ + if((encodeLow ^ (encodeLow + encodeRange)) >= ENET_RANGE_CODER_TOP) \ + { \ + if(encodeRange >= ENET_RANGE_CODER_BOTTOM) break; \ + encodeRange = -encodeLow & (ENET_RANGE_CODER_BOTTOM - 1); \ + } \ + ENET_RANGE_CODER_OUTPUT (encodeLow >> 24); \ + encodeRange <<= 8; \ + encodeLow <<= 8; \ + } \ +} + +#define ENET_RANGE_CODER_FLUSH \ +{ \ + while (encodeLow) \ + { \ + ENET_RANGE_CODER_OUTPUT (encodeLow >> 24); \ + encodeLow <<= 8; \ + } \ +} + +#define ENET_RANGE_CODER_FREE_SYMBOLS \ +{ \ + if (nextSymbol >= sizeof (rangeCoder -> symbols) / sizeof (ENetSymbol) - ENET_SUBCONTEXT_ORDER ) \ + { \ + nextSymbol = 0; \ + ENET_CONTEXT_CREATE (root, ENET_CONTEXT_ESCAPE_MINIMUM, ENET_CONTEXT_SYMBOL_MINIMUM); \ + predicted = 0; \ + order = 0; \ + } \ +} + +#define ENET_CONTEXT_ENCODE(context, symbol_, value_, under_, count_, update, minimum) \ +{ \ + under_ = value*minimum; \ + count_ = minimum; \ + if (! (context) -> symbols) \ + { \ + ENET_SYMBOL_CREATE (symbol_, value_, update); \ + (context) -> symbols = symbol_ - (context); \ + } \ + else \ + { \ + ENetSymbol * node = (context) + (context) -> symbols; \ + for (;;) \ + { \ + if (value_ < node -> value) \ + { \ + node -> under += update; \ + if (node -> left) { node += node -> left; continue; } \ + ENET_SYMBOL_CREATE (symbol_, value_, update); \ + node -> left = symbol_ - node; \ + } \ + else \ + if (value_ > node -> value) \ + { \ + under_ += node -> under; \ + if (node -> right) { node += node -> right; continue; } \ + ENET_SYMBOL_CREATE (symbol_, value_, update); \ + node -> right = symbol_ - node; \ + } \ + else \ + { \ + count_ += node -> count; \ + under_ += node -> under - node -> count; \ + node -> under += update; \ + node -> count += update; \ + symbol_ = node; \ + } \ + break; \ + } \ + } \ +} + +#ifdef ENET_CONTEXT_EXCLUSION +static const ENetSymbol emptyContext = { 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + +#define ENET_CONTEXT_WALK(context, body) \ +{ \ + const ENetSymbol * node = (context) + (context) -> symbols; \ + const ENetSymbol * stack [256]; \ + size_t stackSize = 0; \ + while (node -> left) \ + { \ + stack [stackSize ++] = node; \ + node += node -> left; \ + } \ + for (;;) \ + { \ + body; \ + if (node -> right) \ + { \ + node += node -> right; \ + while (node -> left) \ + { \ + stack [stackSize ++] = node; \ + node += node -> left; \ + } \ + } \ + else \ + if (stackSize <= 0) \ + break; \ + else \ + node = stack [-- stackSize]; \ + } \ +} + +#define ENET_CONTEXT_ENCODE_EXCLUDE(context, value_, under, total, minimum) \ +ENET_CONTEXT_WALK(context, { \ + if (node -> value != value_) \ + { \ + enet_uint16 parentCount = rangeCoder -> symbols [node -> parent].count + minimum; \ + if (node -> value < value_) \ + under -= parentCount; \ + total -= parentCount; \ + } \ +}) +#endif + +size_t +enet_range_coder_compress (void * context, const ENetBuffer * inBuffers, size_t inBufferCount, size_t inLimit, enet_uint8 * outData, size_t outLimit) +{ + ENetRangeCoder * rangeCoder = (ENetRangeCoder *) context; + enet_uint8 * outStart = outData, * outEnd = & outData [outLimit]; + const enet_uint8 * inData, * inEnd; + enet_uint32 encodeLow = 0, encodeRange = ~0; + ENetSymbol * root; + enet_uint16 predicted = 0; + size_t order = 0, nextSymbol = 0; + + if (rangeCoder == NULL || inBufferCount <= 0 || inLimit <= 0) + return 0; + + inData = (const enet_uint8 *) inBuffers -> data; + inEnd = & inData [inBuffers -> dataLength]; + inBuffers ++; + inBufferCount --; + + ENET_CONTEXT_CREATE (root, ENET_CONTEXT_ESCAPE_MINIMUM, ENET_CONTEXT_SYMBOL_MINIMUM); + + for (;;) + { + ENetSymbol * subcontext, * symbol; +#ifdef ENET_CONTEXT_EXCLUSION + const ENetSymbol * childContext = & emptyContext; +#endif + enet_uint8 value; + enet_uint16 count, under, * parent = & predicted, total; + if (inData >= inEnd) + { + if (inBufferCount <= 0) + break; + inData = (const enet_uint8 *) inBuffers -> data; + inEnd = & inData [inBuffers -> dataLength]; + inBuffers ++; + inBufferCount --; + } + value = * inData ++; + + for (subcontext = & rangeCoder -> symbols [predicted]; + subcontext != root; +#ifdef ENET_CONTEXT_EXCLUSION + childContext = subcontext, +#endif + subcontext = & rangeCoder -> symbols [subcontext -> parent]) + { + ENET_CONTEXT_ENCODE (subcontext, symbol, value, under, count, ENET_SUBCONTEXT_SYMBOL_DELTA, 0); + * parent = symbol - rangeCoder -> symbols; + parent = & symbol -> parent; + total = subcontext -> total; +#ifdef ENET_CONTEXT_EXCLUSION + if (childContext -> total > ENET_SUBCONTEXT_SYMBOL_DELTA + ENET_SUBCONTEXT_ESCAPE_DELTA) + ENET_CONTEXT_ENCODE_EXCLUDE (childContext, value, under, total, 0); +#endif + if (count > 0) + { + ENET_RANGE_CODER_ENCODE (subcontext -> escapes + under, count, total); + } + else + { + if (subcontext -> escapes > 0 && subcontext -> escapes < total) + ENET_RANGE_CODER_ENCODE (0, subcontext -> escapes, total); + subcontext -> escapes += ENET_SUBCONTEXT_ESCAPE_DELTA; + subcontext -> total += ENET_SUBCONTEXT_ESCAPE_DELTA; + } + subcontext -> total += ENET_SUBCONTEXT_SYMBOL_DELTA; + if (count > 0xFF - 2*ENET_SUBCONTEXT_SYMBOL_DELTA || subcontext -> total > ENET_RANGE_CODER_BOTTOM - 0x100) + ENET_CONTEXT_RESCALE (subcontext, 0); + if (count > 0) goto nextInput; + } + + ENET_CONTEXT_ENCODE (root, symbol, value, under, count, ENET_CONTEXT_SYMBOL_DELTA, ENET_CONTEXT_SYMBOL_MINIMUM); + * parent = symbol - rangeCoder -> symbols; + parent = & symbol -> parent; + total = root -> total; +#ifdef ENET_CONTEXT_EXCLUSION + if (childContext -> total > ENET_SUBCONTEXT_SYMBOL_DELTA + ENET_SUBCONTEXT_ESCAPE_DELTA) + ENET_CONTEXT_ENCODE_EXCLUDE (childContext, value, under, total, ENET_CONTEXT_SYMBOL_MINIMUM); +#endif + ENET_RANGE_CODER_ENCODE (root -> escapes + under, count, total); + root -> total += ENET_CONTEXT_SYMBOL_DELTA; + if (count > 0xFF - 2*ENET_CONTEXT_SYMBOL_DELTA + ENET_CONTEXT_SYMBOL_MINIMUM || root -> total > ENET_RANGE_CODER_BOTTOM - 0x100) + ENET_CONTEXT_RESCALE (root, ENET_CONTEXT_SYMBOL_MINIMUM); + + nextInput: + if (order >= ENET_SUBCONTEXT_ORDER) + predicted = rangeCoder -> symbols [predicted].parent; + else + order ++; + ENET_RANGE_CODER_FREE_SYMBOLS; + } + + ENET_RANGE_CODER_FLUSH; + + return (size_t) (outData - outStart); +} + +#define ENET_RANGE_CODER_SEED \ +{ \ + if (inData < inEnd) decodeCode |= * inData ++ << 24; \ + if (inData < inEnd) decodeCode |= * inData ++ << 16; \ + if (inData < inEnd) decodeCode |= * inData ++ << 8; \ + if (inData < inEnd) decodeCode |= * inData ++; \ +} + +#define ENET_RANGE_CODER_READ(total) ((decodeCode - decodeLow) / (decodeRange /= (total))) + +#define ENET_RANGE_CODER_DECODE(under, count, total) \ +{ \ + decodeLow += (under) * decodeRange; \ + decodeRange *= (count); \ + for (;;) \ + { \ + if((decodeLow ^ (decodeLow + decodeRange)) >= ENET_RANGE_CODER_TOP) \ + { \ + if(decodeRange >= ENET_RANGE_CODER_BOTTOM) break; \ + decodeRange = -decodeLow & (ENET_RANGE_CODER_BOTTOM - 1); \ + } \ + decodeCode <<= 8; \ + if (inData < inEnd) \ + decodeCode |= * inData ++; \ + decodeRange <<= 8; \ + decodeLow <<= 8; \ + } \ +} + +#define ENET_CONTEXT_DECODE(context, symbol_, code, value_, under_, count_, update, minimum, createRoot, visitNode, createRight, createLeft) \ +{ \ + under_ = 0; \ + count_ = minimum; \ + if (! (context) -> symbols) \ + { \ + createRoot; \ + } \ + else \ + { \ + ENetSymbol * node = (context) + (context) -> symbols; \ + for (;;) \ + { \ + enet_uint16 after = under_ + node -> under + (node -> value + 1)*minimum, before = node -> count + minimum; \ + visitNode; \ + if (code >= after) \ + { \ + under_ += node -> under; \ + if (node -> right) { node += node -> right; continue; } \ + createRight; \ + } \ + else \ + if (code < after - before) \ + { \ + node -> under += update; \ + if (node -> left) { node += node -> left; continue; } \ + createLeft; \ + } \ + else \ + { \ + value_ = node -> value; \ + count_ += node -> count; \ + under_ = after - before; \ + node -> under += update; \ + node -> count += update; \ + symbol_ = node; \ + } \ + break; \ + } \ + } \ +} + +#define ENET_CONTEXT_TRY_DECODE(context, symbol_, code, value_, under_, count_, update, minimum, exclude) \ +ENET_CONTEXT_DECODE (context, symbol_, code, value_, under_, count_, update, minimum, return 0, exclude (node -> value, after, before), return 0, return 0) + +#define ENET_CONTEXT_ROOT_DECODE(context, symbol_, code, value_, under_, count_, update, minimum, exclude) \ +ENET_CONTEXT_DECODE (context, symbol_, code, value_, under_, count_, update, minimum, \ + { \ + value_ = code / minimum; \ + under_ = code - code%minimum; \ + ENET_SYMBOL_CREATE (symbol_, value_, update); \ + (context) -> symbols = symbol_ - (context); \ + }, \ + exclude (node -> value, after, before), \ + { \ + value_ = node->value + 1 + (code - after)/minimum; \ + under_ = code - (code - after)%minimum; \ + ENET_SYMBOL_CREATE (symbol_, value_, update); \ + node -> right = symbol_ - node; \ + }, \ + { \ + value_ = node->value - 1 - (after - before - code - 1)/minimum; \ + under_ = code - (after - before - code - 1)%minimum; \ + ENET_SYMBOL_CREATE (symbol_, value_, update); \ + node -> left = symbol_ - node; \ + }) \ + +#ifdef ENET_CONTEXT_EXCLUSION +typedef struct _ENetExclude +{ + enet_uint8 value; + enet_uint16 under; +} ENetExclude; + +#define ENET_CONTEXT_DECODE_EXCLUDE(context, total, minimum) \ +{ \ + enet_uint16 under = 0; \ + nextExclude = excludes; \ + ENET_CONTEXT_WALK (context, { \ + under += rangeCoder -> symbols [node -> parent].count + minimum; \ + nextExclude -> value = node -> value; \ + nextExclude -> under = under; \ + nextExclude ++; \ + }); \ + total -= under; \ +} + +#define ENET_CONTEXT_EXCLUDED(value_, after, before) \ +{ \ + size_t low = 0, high = nextExclude - excludes; \ + for(;;) \ + { \ + size_t mid = (low + high) >> 1; \ + const ENetExclude * exclude = & excludes [mid]; \ + if (value_ < exclude -> value) \ + { \ + if (low + 1 < high) \ + { \ + high = mid; \ + continue; \ + } \ + if (exclude > excludes) \ + after -= exclude [-1].under; \ + } \ + else \ + { \ + if (value_ > exclude -> value) \ + { \ + if (low + 1 < high) \ + { \ + low = mid; \ + continue; \ + } \ + } \ + else \ + before = 0; \ + after -= exclude -> under; \ + } \ + break; \ + } \ +} +#endif + +#define ENET_CONTEXT_NOT_EXCLUDED(value_, after, before) + +size_t +enet_range_coder_decompress (void * context, const enet_uint8 * inData, size_t inLimit, enet_uint8 * outData, size_t outLimit) +{ + ENetRangeCoder * rangeCoder = (ENetRangeCoder *) context; + enet_uint8 * outStart = outData, * outEnd = & outData [outLimit]; + const enet_uint8 * inEnd = & inData [inLimit]; + enet_uint32 decodeLow = 0, decodeCode = 0, decodeRange = ~0; + ENetSymbol * root; + enet_uint16 predicted = 0; + size_t order = 0, nextSymbol = 0; +#ifdef ENET_CONTEXT_EXCLUSION + ENetExclude excludes [256]; + ENetExclude * nextExclude = excludes; +#endif + + if (rangeCoder == NULL || inLimit <= 0) + return 0; + + ENET_CONTEXT_CREATE (root, ENET_CONTEXT_ESCAPE_MINIMUM, ENET_CONTEXT_SYMBOL_MINIMUM); + + ENET_RANGE_CODER_SEED; + + for (;;) + { + ENetSymbol * subcontext, * symbol, * patch; +#ifdef ENET_CONTEXT_EXCLUSION + const ENetSymbol * childContext = & emptyContext; +#endif + enet_uint8 value = 0; + enet_uint16 code, under, count, bottom, * parent = & predicted, total; + + for (subcontext = & rangeCoder -> symbols [predicted]; + subcontext != root; +#ifdef ENET_CONTEXT_EXCLUSION + childContext = subcontext, +#endif + subcontext = & rangeCoder -> symbols [subcontext -> parent]) + { + if (subcontext -> escapes <= 0) + continue; + total = subcontext -> total; +#ifdef ENET_CONTEXT_EXCLUSION + if (childContext -> total > 0) + ENET_CONTEXT_DECODE_EXCLUDE (childContext, total, 0); +#endif + if (subcontext -> escapes >= total) + continue; + code = ENET_RANGE_CODER_READ (total); + if (code < subcontext -> escapes) + { + ENET_RANGE_CODER_DECODE (0, subcontext -> escapes, total); + continue; + } + code -= subcontext -> escapes; +#ifdef ENET_CONTEXT_EXCLUSION + if (childContext -> total > 0) + { + ENET_CONTEXT_TRY_DECODE (subcontext, symbol, code, value, under, count, ENET_SUBCONTEXT_SYMBOL_DELTA, 0, ENET_CONTEXT_EXCLUDED); + } + else +#endif + { + ENET_CONTEXT_TRY_DECODE (subcontext, symbol, code, value, under, count, ENET_SUBCONTEXT_SYMBOL_DELTA, 0, ENET_CONTEXT_NOT_EXCLUDED); + } + bottom = symbol - rangeCoder -> symbols; + ENET_RANGE_CODER_DECODE (subcontext -> escapes + under, count, total); + subcontext -> total += ENET_SUBCONTEXT_SYMBOL_DELTA; + if (count > 0xFF - 2*ENET_SUBCONTEXT_SYMBOL_DELTA || subcontext -> total > ENET_RANGE_CODER_BOTTOM - 0x100) + ENET_CONTEXT_RESCALE (subcontext, 0); + goto patchContexts; + } + + total = root -> total; +#ifdef ENET_CONTEXT_EXCLUSION + if (childContext -> total > 0) + ENET_CONTEXT_DECODE_EXCLUDE (childContext, total, ENET_CONTEXT_SYMBOL_MINIMUM); +#endif + code = ENET_RANGE_CODER_READ (total); + if (code < root -> escapes) + { + ENET_RANGE_CODER_DECODE (0, root -> escapes, total); + break; + } + code -= root -> escapes; +#ifdef ENET_CONTEXT_EXCLUSION + if (childContext -> total > 0) + { + ENET_CONTEXT_ROOT_DECODE (root, symbol, code, value, under, count, ENET_CONTEXT_SYMBOL_DELTA, ENET_CONTEXT_SYMBOL_MINIMUM, ENET_CONTEXT_EXCLUDED); + } + else +#endif + { + ENET_CONTEXT_ROOT_DECODE (root, symbol, code, value, under, count, ENET_CONTEXT_SYMBOL_DELTA, ENET_CONTEXT_SYMBOL_MINIMUM, ENET_CONTEXT_NOT_EXCLUDED); + } + bottom = symbol - rangeCoder -> symbols; + ENET_RANGE_CODER_DECODE (root -> escapes + under, count, total); + root -> total += ENET_CONTEXT_SYMBOL_DELTA; + if (count > 0xFF - 2*ENET_CONTEXT_SYMBOL_DELTA + ENET_CONTEXT_SYMBOL_MINIMUM || root -> total > ENET_RANGE_CODER_BOTTOM - 0x100) + ENET_CONTEXT_RESCALE (root, ENET_CONTEXT_SYMBOL_MINIMUM); + + patchContexts: + for (patch = & rangeCoder -> symbols [predicted]; + patch != subcontext; + patch = & rangeCoder -> symbols [patch -> parent]) + { + ENET_CONTEXT_ENCODE (patch, symbol, value, under, count, ENET_SUBCONTEXT_SYMBOL_DELTA, 0); + * parent = symbol - rangeCoder -> symbols; + parent = & symbol -> parent; + if (count <= 0) + { + patch -> escapes += ENET_SUBCONTEXT_ESCAPE_DELTA; + patch -> total += ENET_SUBCONTEXT_ESCAPE_DELTA; + } + patch -> total += ENET_SUBCONTEXT_SYMBOL_DELTA; + if (count > 0xFF - 2*ENET_SUBCONTEXT_SYMBOL_DELTA || patch -> total > ENET_RANGE_CODER_BOTTOM - 0x100) + ENET_CONTEXT_RESCALE (patch, 0); + } + * parent = bottom; + + ENET_RANGE_CODER_OUTPUT (value); + + if (order >= ENET_SUBCONTEXT_ORDER) + predicted = rangeCoder -> symbols [predicted].parent; + else + order ++; + ENET_RANGE_CODER_FREE_SYMBOLS; + } + + return (size_t) (outData - outStart); +} + +/** @defgroup host ENet host functions + @{ +*/ + +/** Sets the packet compressor the host should use to the default range coder. + @param host host to enable the range coder for + @returns 0 on success, < 0 on failure +*/ +int +enet_host_compress_with_range_coder (ENetHost * host) +{ + ENetCompressor compressor; + memset (& compressor, 0, sizeof (compressor)); + compressor.context = enet_range_coder_create(); + if (compressor.context == NULL) + return -1; + compressor.compress = enet_range_coder_compress; + compressor.decompress = enet_range_coder_decompress; + compressor.destroy = enet_range_coder_destroy; + enet_host_compress (host, & compressor); + return 0; +} + +/** @} */ + + diff --git a/Externals/enet/enet.h b/Externals/enet/enet.h new file mode 100644 index 000000000000..c690e42848aa --- /dev/null +++ b/Externals/enet/enet.h @@ -0,0 +1,587 @@ +/** + @file enet.h + @brief ENet public header file +*/ +#ifndef __ENET_ENET_H__ +#define __ENET_ENET_H__ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include + +#ifdef _WIN32 +#include "enet/win32.h" +#else +#include "enet/unix.h" +#endif + +#include "enet/types.h" +#include "enet/protocol.h" +#include "enet/list.h" +#include "enet/callbacks.h" + +#define ENET_VERSION_MAJOR 1 +#define ENET_VERSION_MINOR 3 +#define ENET_VERSION_PATCH 9 +#define ENET_VERSION_CREATE(major, minor, patch) (((major)<<16) | ((minor)<<8) | (patch)) +#define ENET_VERSION_GET_MAJOR(version) (((version)>>16)&0xFF) +#define ENET_VERSION_GET_MINOR(version) (((version)>>8)&0xFF) +#define ENET_VERSION_GET_PATCH(version) ((version)&0xFF) +#define ENET_VERSION ENET_VERSION_CREATE(ENET_VERSION_MAJOR, ENET_VERSION_MINOR, ENET_VERSION_PATCH) + +typedef enet_uint32 ENetVersion; + +struct _ENetHost; +struct _ENetEvent; +struct _ENetPacket; + +typedef enum _ENetSocketType +{ + ENET_SOCKET_TYPE_STREAM = 1, + ENET_SOCKET_TYPE_DATAGRAM = 2 +} ENetSocketType; + +typedef enum _ENetSocketWait +{ + ENET_SOCKET_WAIT_NONE = 0, + ENET_SOCKET_WAIT_SEND = (1 << 0), + ENET_SOCKET_WAIT_RECEIVE = (1 << 1), + ENET_SOCKET_WAIT_INTERRUPT = (1 << 2) +} ENetSocketWait; + +typedef enum _ENetSocketOption +{ + ENET_SOCKOPT_NONBLOCK = 1, + ENET_SOCKOPT_BROADCAST = 2, + ENET_SOCKOPT_RCVBUF = 3, + ENET_SOCKOPT_SNDBUF = 4, + ENET_SOCKOPT_REUSEADDR = 5, + ENET_SOCKOPT_RCVTIMEO = 6, + ENET_SOCKOPT_SNDTIMEO = 7, + ENET_SOCKOPT_ERROR = 8, + ENET_SOCKOPT_NODELAY = 9 +} ENetSocketOption; + +typedef enum _ENetSocketShutdown +{ + ENET_SOCKET_SHUTDOWN_READ = 0, + ENET_SOCKET_SHUTDOWN_WRITE = 1, + ENET_SOCKET_SHUTDOWN_READ_WRITE = 2 +} ENetSocketShutdown; + +#define ENET_HOST_ANY 0 +#define ENET_HOST_BROADCAST 0xFFFFFFFFU +#define ENET_PORT_ANY 0 + +/** + * Portable internet address structure. + * + * The host must be specified in network byte-order, and the port must be in host + * byte-order. The constant ENET_HOST_ANY may be used to specify the default + * server host. The constant ENET_HOST_BROADCAST may be used to specify the + * broadcast address (255.255.255.255). This makes sense for enet_host_connect, + * but not for enet_host_create. Once a server responds to a broadcast, the + * address is updated from ENET_HOST_BROADCAST to the server's actual IP address. + */ +typedef struct _ENetAddress +{ + enet_uint32 host; + enet_uint16 port; +} ENetAddress; + +/** + * Packet flag bit constants. + * + * The host must be specified in network byte-order, and the port must be in + * host byte-order. The constant ENET_HOST_ANY may be used to specify the + * default server host. + + @sa ENetPacket +*/ +typedef enum _ENetPacketFlag +{ + /** packet must be received by the target peer and resend attempts should be + * made until the packet is delivered */ + ENET_PACKET_FLAG_RELIABLE = (1 << 0), + /** packet will not be sequenced with other packets + * not supported for reliable packets + */ + ENET_PACKET_FLAG_UNSEQUENCED = (1 << 1), + /** packet will not allocate data, and user must supply it instead */ + ENET_PACKET_FLAG_NO_ALLOCATE = (1 << 2), + /** packet will be fragmented using unreliable (instead of reliable) sends + * if it exceeds the MTU */ + ENET_PACKET_FLAG_UNRELIABLE_FRAGMENT = (1 << 3), + + /** whether the packet has been sent from all queues it has been entered into */ + ENET_PACKET_FLAG_SENT = (1<<8) +} ENetPacketFlag; + +typedef void (ENET_CALLBACK * ENetPacketFreeCallback) (struct _ENetPacket *); + +/** + * ENet packet structure. + * + * An ENet data packet that may be sent to or received from a peer. The shown + * fields should only be read and never modified. The data field contains the + * allocated data for the packet. The dataLength fields specifies the length + * of the allocated data. The flags field is either 0 (specifying no flags), + * or a bitwise-or of any combination of the following flags: + * + * ENET_PACKET_FLAG_RELIABLE - packet must be received by the target peer + * and resend attempts should be made until the packet is delivered + * + * ENET_PACKET_FLAG_UNSEQUENCED - packet will not be sequenced with other packets + * (not supported for reliable packets) + * + * ENET_PACKET_FLAG_NO_ALLOCATE - packet will not allocate data, and user must supply it instead + + @sa ENetPacketFlag + */ +typedef struct _ENetPacket +{ + size_t referenceCount; /**< internal use only */ + enet_uint32 flags; /**< bitwise-or of ENetPacketFlag constants */ + enet_uint8 * data; /**< allocated data for packet */ + size_t dataLength; /**< length of data */ + ENetPacketFreeCallback freeCallback; /**< function to be called when the packet is no longer in use */ + void * userData; /**< application private data, may be freely modified */ +} ENetPacket; + +typedef struct _ENetAcknowledgement +{ + ENetListNode acknowledgementList; + enet_uint32 sentTime; + ENetProtocol command; +} ENetAcknowledgement; + +typedef struct _ENetOutgoingCommand +{ + ENetListNode outgoingCommandList; + enet_uint16 reliableSequenceNumber; + enet_uint16 unreliableSequenceNumber; + enet_uint32 sentTime; + enet_uint32 roundTripTimeout; + enet_uint32 roundTripTimeoutLimit; + enet_uint32 fragmentOffset; + enet_uint16 fragmentLength; + enet_uint16 sendAttempts; + ENetProtocol command; + ENetPacket * packet; +} ENetOutgoingCommand; + +typedef struct _ENetIncomingCommand +{ + ENetListNode incomingCommandList; + enet_uint16 reliableSequenceNumber; + enet_uint16 unreliableSequenceNumber; + ENetProtocol command; + enet_uint32 fragmentCount; + enet_uint32 fragmentsRemaining; + enet_uint32 * fragments; + ENetPacket * packet; +} ENetIncomingCommand; + +typedef enum _ENetPeerState +{ + ENET_PEER_STATE_DISCONNECTED = 0, + ENET_PEER_STATE_CONNECTING = 1, + ENET_PEER_STATE_ACKNOWLEDGING_CONNECT = 2, + ENET_PEER_STATE_CONNECTION_PENDING = 3, + ENET_PEER_STATE_CONNECTION_SUCCEEDED = 4, + ENET_PEER_STATE_CONNECTED = 5, + ENET_PEER_STATE_DISCONNECT_LATER = 6, + ENET_PEER_STATE_DISCONNECTING = 7, + ENET_PEER_STATE_ACKNOWLEDGING_DISCONNECT = 8, + ENET_PEER_STATE_ZOMBIE = 9 +} ENetPeerState; + +#ifndef ENET_BUFFER_MAXIMUM +#define ENET_BUFFER_MAXIMUM (1 + 2 * ENET_PROTOCOL_MAXIMUM_PACKET_COMMANDS) +#endif + +enum +{ + ENET_HOST_RECEIVE_BUFFER_SIZE = 256 * 1024, + ENET_HOST_SEND_BUFFER_SIZE = 256 * 1024, + ENET_HOST_BANDWIDTH_THROTTLE_INTERVAL = 1000, + ENET_HOST_DEFAULT_MTU = 1400, + + ENET_PEER_DEFAULT_ROUND_TRIP_TIME = 500, + ENET_PEER_DEFAULT_PACKET_THROTTLE = 32, + ENET_PEER_PACKET_THROTTLE_SCALE = 32, + ENET_PEER_PACKET_THROTTLE_COUNTER = 7, + ENET_PEER_PACKET_THROTTLE_ACCELERATION = 2, + ENET_PEER_PACKET_THROTTLE_DECELERATION = 2, + ENET_PEER_PACKET_THROTTLE_INTERVAL = 5000, + ENET_PEER_PACKET_LOSS_SCALE = (1 << 16), + ENET_PEER_PACKET_LOSS_INTERVAL = 10000, + ENET_PEER_WINDOW_SIZE_SCALE = 64 * 1024, + ENET_PEER_TIMEOUT_LIMIT = 32, + ENET_PEER_TIMEOUT_MINIMUM = 5000, + ENET_PEER_TIMEOUT_MAXIMUM = 30000, + ENET_PEER_PING_INTERVAL = 500, + ENET_PEER_UNSEQUENCED_WINDOWS = 64, + ENET_PEER_UNSEQUENCED_WINDOW_SIZE = 1024, + ENET_PEER_FREE_UNSEQUENCED_WINDOWS = 32, + ENET_PEER_RELIABLE_WINDOWS = 16, + ENET_PEER_RELIABLE_WINDOW_SIZE = 0x1000, + ENET_PEER_FREE_RELIABLE_WINDOWS = 8 +}; + +typedef struct _ENetChannel +{ + enet_uint16 outgoingReliableSequenceNumber; + enet_uint16 outgoingUnreliableSequenceNumber; + enet_uint16 usedReliableWindows; + enet_uint16 reliableWindows [ENET_PEER_RELIABLE_WINDOWS]; + enet_uint16 incomingReliableSequenceNumber; + enet_uint16 incomingUnreliableSequenceNumber; + ENetList incomingReliableCommands; + ENetList incomingUnreliableCommands; +} ENetChannel; + +/** + * An ENet peer which data packets may be sent or received from. + * + * No fields should be modified unless otherwise specified. + */ +typedef struct _ENetPeer +{ + ENetListNode dispatchList; + struct _ENetHost * host; + enet_uint16 outgoingPeerID; + enet_uint16 incomingPeerID; + enet_uint32 connectID; + enet_uint8 outgoingSessionID; + enet_uint8 incomingSessionID; + ENetAddress address; /**< Internet address of the peer */ + void * data; /**< Application private data, may be freely modified */ + ENetPeerState state; + ENetChannel * channels; + size_t channelCount; /**< Number of channels allocated for communication with peer */ + enet_uint32 incomingBandwidth; /**< Downstream bandwidth of the client in bytes/second */ + enet_uint32 outgoingBandwidth; /**< Upstream bandwidth of the client in bytes/second */ + enet_uint32 incomingBandwidthThrottleEpoch; + enet_uint32 outgoingBandwidthThrottleEpoch; + enet_uint32 incomingDataTotal; + enet_uint32 outgoingDataTotal; + enet_uint32 lastSendTime; + enet_uint32 lastReceiveTime; + enet_uint32 nextTimeout; + enet_uint32 earliestTimeout; + enet_uint32 packetLossEpoch; + enet_uint32 packetsSent; + enet_uint32 packetsLost; + enet_uint32 packetLoss; /**< mean packet loss of reliable packets as a ratio with respect to the constant ENET_PEER_PACKET_LOSS_SCALE */ + enet_uint32 packetLossVariance; + enet_uint32 packetThrottle; + enet_uint32 packetThrottleLimit; + enet_uint32 packetThrottleCounter; + enet_uint32 packetThrottleEpoch; + enet_uint32 packetThrottleAcceleration; + enet_uint32 packetThrottleDeceleration; + enet_uint32 packetThrottleInterval; + enet_uint32 pingInterval; + enet_uint32 timeoutLimit; + enet_uint32 timeoutMinimum; + enet_uint32 timeoutMaximum; + enet_uint32 lastRoundTripTime; + enet_uint32 lowestRoundTripTime; + enet_uint32 lastRoundTripTimeVariance; + enet_uint32 highestRoundTripTimeVariance; + enet_uint32 roundTripTime; /**< mean round trip time (RTT), in milliseconds, between sending a reliable packet and receiving its acknowledgement */ + enet_uint32 roundTripTimeVariance; + enet_uint32 mtu; + enet_uint32 windowSize; + enet_uint32 reliableDataInTransit; + enet_uint16 outgoingReliableSequenceNumber; + ENetList acknowledgements; + ENetList sentReliableCommands; + ENetList sentUnreliableCommands; + ENetList outgoingReliableCommands; + ENetList outgoingUnreliableCommands; + ENetList dispatchedCommands; + int needsDispatch; + enet_uint16 incomingUnsequencedGroup; + enet_uint16 outgoingUnsequencedGroup; + enet_uint32 unsequencedWindow [ENET_PEER_UNSEQUENCED_WINDOW_SIZE / 32]; + enet_uint32 eventData; +} ENetPeer; + +/** An ENet packet compressor for compressing UDP packets before socket sends or receives. + */ +typedef struct _ENetCompressor +{ + /** Context data for the compressor. Must be non-NULL. */ + void * context; + /** Compresses from inBuffers[0:inBufferCount-1], containing inLimit bytes, to outData, outputting at most outLimit bytes. Should return 0 on failure. */ + size_t (ENET_CALLBACK * compress) (void * context, const ENetBuffer * inBuffers, size_t inBufferCount, size_t inLimit, enet_uint8 * outData, size_t outLimit); + /** Decompresses from inData, containing inLimit bytes, to outData, outputting at most outLimit bytes. Should return 0 on failure. */ + size_t (ENET_CALLBACK * decompress) (void * context, const enet_uint8 * inData, size_t inLimit, enet_uint8 * outData, size_t outLimit); + /** Destroys the context when compression is disabled or the host is destroyed. May be NULL. */ + void (ENET_CALLBACK * destroy) (void * context); +} ENetCompressor; + +/** Callback that computes the checksum of the data held in buffers[0:bufferCount-1] */ +typedef enet_uint32 (ENET_CALLBACK * ENetChecksumCallback) (const ENetBuffer * buffers, size_t bufferCount); + +/** Callback for intercepting received raw UDP packets. Should return 1 to intercept, 0 to ignore, or -1 to propagate an error. */ +typedef int (ENET_CALLBACK * ENetInterceptCallback) (struct _ENetHost * host, struct _ENetEvent * event); + +/** An ENet host for communicating with peers. + * + * No fields should be modified unless otherwise stated. + + @sa enet_host_create() + @sa enet_host_destroy() + @sa enet_host_connect() + @sa enet_host_service() + @sa enet_host_flush() + @sa enet_host_broadcast() + @sa enet_host_compress() + @sa enet_host_compress_with_range_coder() + @sa enet_host_channel_limit() + @sa enet_host_bandwidth_limit() + @sa enet_host_bandwidth_throttle() + */ +typedef struct _ENetHost +{ + ENetSocket socket; + ENetAddress address; /**< Internet address of the host */ + enet_uint32 incomingBandwidth; /**< downstream bandwidth of the host */ + enet_uint32 outgoingBandwidth; /**< upstream bandwidth of the host */ + enet_uint32 bandwidthThrottleEpoch; + enet_uint32 mtu; + enet_uint32 randomSeed; + int recalculateBandwidthLimits; + ENetPeer * peers; /**< array of peers allocated for this host */ + size_t peerCount; /**< number of peers allocated for this host */ + size_t channelLimit; /**< maximum number of channels allowed for connected peers */ + enet_uint32 serviceTime; + ENetList dispatchQueue; + int continueSending; + size_t packetSize; + enet_uint16 headerFlags; + ENetProtocol commands [ENET_PROTOCOL_MAXIMUM_PACKET_COMMANDS]; + size_t commandCount; + ENetBuffer buffers [ENET_BUFFER_MAXIMUM]; + size_t bufferCount; + ENetChecksumCallback checksum; /**< callback the user can set to enable packet checksums for this host */ + ENetCompressor compressor; + enet_uint8 packetData [2][ENET_PROTOCOL_MAXIMUM_MTU]; + ENetAddress receivedAddress; + enet_uint8 * receivedData; + size_t receivedDataLength; + enet_uint32 totalSentData; /**< total data sent, user should reset to 0 as needed to prevent overflow */ + enet_uint32 totalSentPackets; /**< total UDP packets sent, user should reset to 0 as needed to prevent overflow */ + enet_uint32 totalReceivedData; /**< total data received, user should reset to 0 as needed to prevent overflow */ + enet_uint32 totalReceivedPackets; /**< total UDP packets received, user should reset to 0 as needed to prevent overflow */ + ENetInterceptCallback intercept; /**< callback the user can set to intercept received raw UDP packets */ + size_t connectedPeers; + size_t bandwidthLimitedPeers; + size_t duplicatePeers; /**< optional number of allowed peers from duplicate IPs, defaults to ENET_PROTOCOL_MAXIMUM_PEER_ID */ +} ENetHost; + +/** + * An ENet event type, as specified in @ref ENetEvent. + */ +typedef enum _ENetEventType +{ + /** no event occurred within the specified time limit */ + ENET_EVENT_TYPE_NONE = 0, + + /** a connection request initiated by enet_host_connect has completed. + * The peer field contains the peer which successfully connected. + */ + ENET_EVENT_TYPE_CONNECT = 1, + + /** a peer has disconnected. This event is generated on a successful + * completion of a disconnect initiated by enet_pper_disconnect, if + * a peer has timed out, or if a connection request intialized by + * enet_host_connect has timed out. The peer field contains the peer + * which disconnected. The data field contains user supplied data + * describing the disconnection, or 0, if none is available. + */ + ENET_EVENT_TYPE_DISCONNECT = 2, + + /** a packet has been received from a peer. The peer field specifies the + * peer which sent the packet. The channelID field specifies the channel + * number upon which the packet was received. The packet field contains + * the packet that was received; this packet must be destroyed with + * enet_packet_destroy after use. + */ + ENET_EVENT_TYPE_RECEIVE = 3 +} ENetEventType; + +/** + * An ENet event as returned by enet_host_service(). + + @sa enet_host_service + */ +typedef struct _ENetEvent +{ + ENetEventType type; /**< type of the event */ + ENetPeer * peer; /**< peer that generated a connect, disconnect or receive event */ + enet_uint8 channelID; /**< channel on the peer that generated the event, if appropriate */ + enet_uint32 data; /**< data associated with the event, if appropriate */ + ENetPacket * packet; /**< packet associated with the event, if appropriate */ +} ENetEvent; + +/** @defgroup global ENet global functions + @{ +*/ + +/** + Initializes ENet globally. Must be called prior to using any functions in + ENet. + @returns 0 on success, < 0 on failure +*/ +ENET_API int enet_initialize (void); + +/** + Initializes ENet globally and supplies user-overridden callbacks. Must be called prior to using any functions in ENet. Do not use enet_initialize() if you use this variant. Make sure the ENetCallbacks structure is zeroed out so that any additional callbacks added in future versions will be properly ignored. + + @param version the constant ENET_VERSION should be supplied so ENet knows which version of ENetCallbacks struct to use + @param inits user-overriden callbacks where any NULL callbacks will use ENet's defaults + @returns 0 on success, < 0 on failure +*/ +ENET_API int enet_initialize_with_callbacks (ENetVersion version, const ENetCallbacks * inits); + +/** + Shuts down ENet globally. Should be called when a program that has + initialized ENet exits. +*/ +ENET_API void enet_deinitialize (void); + +/** + Gives the linked version of the ENet library. + @returns the version number +*/ +ENET_API ENetVersion enet_linked_version (void); + +/** @} */ + +/** @defgroup private ENet private implementation functions */ + +/** + Returns the wall-time in milliseconds. Its initial value is unspecified + unless otherwise set. + */ +ENET_API enet_uint32 enet_time_get (void); +/** + Sets the current wall-time in milliseconds. + */ +ENET_API void enet_time_set (enet_uint32); + +/** @defgroup socket ENet socket functions + @{ +*/ +ENET_API ENetSocket enet_socket_create (ENetSocketType); +ENET_API int enet_socket_bind (ENetSocket, const ENetAddress *); +ENET_API int enet_socket_get_address (ENetSocket, ENetAddress *); +ENET_API int enet_socket_listen (ENetSocket, int); +ENET_API ENetSocket enet_socket_accept (ENetSocket, ENetAddress *); +ENET_API int enet_socket_connect (ENetSocket, const ENetAddress *); +ENET_API int enet_socket_send (ENetSocket, const ENetAddress *, const ENetBuffer *, size_t); +ENET_API int enet_socket_receive (ENetSocket, ENetAddress *, ENetBuffer *, size_t); +ENET_API int enet_socket_wait (ENetSocket, enet_uint32 *, enet_uint32); +ENET_API int enet_socket_set_option (ENetSocket, ENetSocketOption, int); +ENET_API int enet_socket_get_option (ENetSocket, ENetSocketOption, int *); +ENET_API int enet_socket_shutdown (ENetSocket, ENetSocketShutdown); +ENET_API void enet_socket_destroy (ENetSocket); +ENET_API int enet_socketset_select (ENetSocket, ENetSocketSet *, ENetSocketSet *, enet_uint32); + +/** @} */ + +/** @defgroup Address ENet address functions + @{ +*/ +/** Attempts to resolve the host named by the parameter hostName and sets + the host field in the address parameter if successful. + @param address destination to store resolved address + @param hostName host name to lookup + @retval 0 on success + @retval < 0 on failure + @returns the address of the given hostName in address on success +*/ +ENET_API int enet_address_set_host (ENetAddress * address, const char * hostName); + +/** Gives the printable form of the ip address specified in the address parameter. + @param address address printed + @param hostName destination for name, must not be NULL + @param nameLength maximum length of hostName. + @returns the null-terminated name of the host in hostName on success + @retval 0 on success + @retval < 0 on failure +*/ +ENET_API int enet_address_get_host_ip (const ENetAddress * address, char * hostName, size_t nameLength); + +/** Attempts to do a reverse lookup of the host field in the address parameter. + @param address address used for reverse lookup + @param hostName destination for name, must not be NULL + @param nameLength maximum length of hostName. + @returns the null-terminated name of the host in hostName on success + @retval 0 on success + @retval < 0 on failure +*/ +ENET_API int enet_address_get_host (const ENetAddress * address, char * hostName, size_t nameLength); + +/** @} */ + +ENET_API ENetPacket * enet_packet_create (const void *, size_t, enet_uint32); +ENET_API void enet_packet_destroy (ENetPacket *); +ENET_API int enet_packet_resize (ENetPacket *, size_t); +ENET_API enet_uint32 enet_crc32 (const ENetBuffer *, size_t); + +ENET_API ENetHost * enet_host_create (const ENetAddress *, size_t, size_t, enet_uint32, enet_uint32); +ENET_API void enet_host_destroy (ENetHost *); +ENET_API ENetPeer * enet_host_connect (ENetHost *, const ENetAddress *, size_t, enet_uint32); +ENET_API int enet_host_check_events (ENetHost *, ENetEvent *); +ENET_API int enet_host_service (ENetHost *, ENetEvent *, enet_uint32); +ENET_API void enet_host_flush (ENetHost *); +ENET_API void enet_host_broadcast (ENetHost *, enet_uint8, ENetPacket *); +ENET_API void enet_host_compress (ENetHost *, const ENetCompressor *); +ENET_API int enet_host_compress_with_range_coder (ENetHost * host); +ENET_API void enet_host_channel_limit (ENetHost *, size_t); +ENET_API void enet_host_bandwidth_limit (ENetHost *, enet_uint32, enet_uint32); +extern void enet_host_bandwidth_throttle (ENetHost *); +extern enet_uint32 enet_host_random_seed (void); + +ENET_API int enet_peer_send (ENetPeer *, enet_uint8, ENetPacket *); +ENET_API ENetPacket * enet_peer_receive (ENetPeer *, enet_uint8 * channelID); +ENET_API void enet_peer_ping (ENetPeer *); +ENET_API void enet_peer_ping_interval (ENetPeer *, enet_uint32); +ENET_API void enet_peer_timeout (ENetPeer *, enet_uint32, enet_uint32, enet_uint32); +ENET_API void enet_peer_reset (ENetPeer *); +ENET_API void enet_peer_disconnect (ENetPeer *, enet_uint32); +ENET_API void enet_peer_disconnect_now (ENetPeer *, enet_uint32); +ENET_API void enet_peer_disconnect_later (ENetPeer *, enet_uint32); +ENET_API void enet_peer_throttle_configure (ENetPeer *, enet_uint32, enet_uint32, enet_uint32); +extern int enet_peer_throttle (ENetPeer *, enet_uint32); +extern void enet_peer_reset_queues (ENetPeer *); +extern void enet_peer_setup_outgoing_command (ENetPeer *, ENetOutgoingCommand *); +extern ENetOutgoingCommand * enet_peer_queue_outgoing_command (ENetPeer *, const ENetProtocol *, ENetPacket *, enet_uint32, enet_uint16); +extern ENetIncomingCommand * enet_peer_queue_incoming_command (ENetPeer *, const ENetProtocol *, ENetPacket *, enet_uint32); +extern ENetAcknowledgement * enet_peer_queue_acknowledgement (ENetPeer *, const ENetProtocol *, enet_uint16); +extern void enet_peer_dispatch_incoming_unreliable_commands (ENetPeer *, ENetChannel *); +extern void enet_peer_dispatch_incoming_reliable_commands (ENetPeer *, ENetChannel *); +extern void enet_peer_on_connect (ENetPeer *); +extern void enet_peer_on_disconnect (ENetPeer *); + +ENET_API void * enet_range_coder_create (void); +ENET_API void enet_range_coder_destroy (void *); +ENET_API size_t enet_range_coder_compress (void *, const ENetBuffer *, size_t, size_t, enet_uint8 *, size_t); +ENET_API size_t enet_range_coder_decompress (void *, const enet_uint8 *, size_t, enet_uint8 *, size_t); + +extern size_t enet_protocol_command_size (enet_uint8); + +#ifdef __cplusplus +} +#endif + +#endif /* __ENET_ENET_H__ */ + diff --git a/Externals/miniupnpc/miniupnpc.vcxproj b/Externals/enet/enet.vcxproj similarity index 81% rename from Externals/miniupnpc/miniupnpc.vcxproj rename to Externals/enet/enet.vcxproj index db92cd61f93b..2d9bda32e94a 100644 --- a/Externals/miniupnpc/miniupnpc.vcxproj +++ b/Externals/enet/enet.vcxproj @@ -1,164 +1,159 @@ -ďťż - - - - Debug - Win32 - - - Debug - x64 - - - Release - Win32 - - - Release - x64 - - - - {A680190D-0764-485B-9CF3-A82C5EDD5715} - miniupnpc - - - - StaticLibrary - true - MultiByte - - - StaticLibrary - true - MultiByte - - - StaticLibrary - false - true - MultiByte - - - StaticLibrary - false - true - MultiByte - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Level3 - Disabled - _WIN32_WINNT=0x0501;STATICLIB;DEBUG - - - true - ws2_32.lib;iphlpapi.lib;%(AdditionalDependencies) - - - - - Level3 - Disabled - _WIN32_WINNT=0x0501;STATICLIB;DEBUG - - - true - ws2_32.lib;iphlpapi.lib;%(AdditionalDependencies) - - - - - Level3 - MaxSpeed - true - true - _WIN32_WINNT=0x0501;STATICLIB - - - true - true - true - ws2_32.lib;iphlpapi.lib;%(AdditionalDependencies) - - - - - Level3 - MaxSpeed - true - true - _WIN32_WINNT=0x0501;STATICLIB - - - true - true - true - ws2_32.lib;iphlpapi.lib;%(AdditionalDependencies) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +ďťż + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + + + + + + + + + + + + + + + + + + + + + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC} + enet + + + + StaticLibrary + true + MultiByte + + + StaticLibrary + true + MultiByte + + + StaticLibrary + false + true + MultiByte + + + StaticLibrary + false + true + MultiByte + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Level3 + Disabled + _WIN32_WINNT=0x0501;STATICLIB;DEBUG + $(ProjectDir)\.. + 4146 + + + true + ws2_32.lib;iphlpapi.lib;%(AdditionalDependencies) + + + + + Level3 + Disabled + _WIN32_WINNT=0x0501;STATICLIB;DEBUG + $(ProjectDir)\.. + 4146 + + + true + ws2_32.lib;iphlpapi.lib;%(AdditionalDependencies) + + + + + Level3 + MaxSpeed + true + true + _WIN32_WINNT=0x0501;STATICLIB + $(ProjectDir)\.. + 4146 + + + true + true + true + ws2_32.lib;iphlpapi.lib;%(AdditionalDependencies) + + + + + Level3 + MaxSpeed + true + true + _WIN32_WINNT=0x0501;STATICLIB + $(ProjectDir)\.. + 4146 + + + true + true + true + ws2_32.lib;iphlpapi.lib;%(AdditionalDependencies) + + + + + \ No newline at end of file diff --git a/Externals/enet/enet.vcxproj.filters b/Externals/enet/enet.vcxproj.filters new file mode 100644 index 000000000000..695c2a58f9d1 --- /dev/null +++ b/Externals/enet/enet.vcxproj.filters @@ -0,0 +1,23 @@ +ďťż + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Externals/enet/git-revision b/Externals/enet/git-revision new file mode 100644 index 000000000000..5ed4da933377 --- /dev/null +++ b/Externals/enet/git-revision @@ -0,0 +1 @@ +2c5dd69b176f1a3089bc10affc0c65d7fcbbab07 diff --git a/Externals/enet/host.c b/Externals/enet/host.c new file mode 100644 index 000000000000..fc57f7410dde --- /dev/null +++ b/Externals/enet/host.c @@ -0,0 +1,490 @@ +/** + @file host.c + @brief ENet host management functions +*/ +#define ENET_BUILDING_LIB 1 +#include +#include "enet/enet.h" + +/** @defgroup host ENet host functions + @{ +*/ + +/** Creates a host for communicating to peers. + + @param address the address at which other peers may connect to this host. If NULL, then no peers may connect to the host. + @param peerCount the maximum number of peers that should be allocated for the host. + @param channelLimit the maximum number of channels allowed; if 0, then this is equivalent to ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT + @param incomingBandwidth downstream bandwidth of the host in bytes/second; if 0, ENet will assume unlimited bandwidth. + @param outgoingBandwidth upstream bandwidth of the host in bytes/second; if 0, ENet will assume unlimited bandwidth. + + @returns the host on success and NULL on failure + + @remarks ENet will strategically drop packets on specific sides of a connection between hosts + to ensure the host's bandwidth is not overwhelmed. The bandwidth parameters also determine + the window size of a connection which limits the amount of reliable packets that may be in transit + at any given time. +*/ +ENetHost * +enet_host_create (const ENetAddress * address, size_t peerCount, size_t channelLimit, enet_uint32 incomingBandwidth, enet_uint32 outgoingBandwidth) +{ + ENetHost * host; + ENetPeer * currentPeer; + + if (peerCount > ENET_PROTOCOL_MAXIMUM_PEER_ID) + return NULL; + + host = (ENetHost *) enet_malloc (sizeof (ENetHost)); + if (host == NULL) + return NULL; + memset (host, 0, sizeof (ENetHost)); + + host -> peers = (ENetPeer *) enet_malloc (peerCount * sizeof (ENetPeer)); + if (host -> peers == NULL) + { + enet_free (host); + + return NULL; + } + memset (host -> peers, 0, peerCount * sizeof (ENetPeer)); + + host -> socket = enet_socket_create (ENET_SOCKET_TYPE_DATAGRAM); + if (host -> socket == ENET_SOCKET_NULL || (address != NULL && enet_socket_bind (host -> socket, address) < 0)) + { + if (host -> socket != ENET_SOCKET_NULL) + enet_socket_destroy (host -> socket); + + enet_free (host -> peers); + enet_free (host); + + return NULL; + } + + enet_socket_set_option (host -> socket, ENET_SOCKOPT_NONBLOCK, 1); + enet_socket_set_option (host -> socket, ENET_SOCKOPT_BROADCAST, 1); + enet_socket_set_option (host -> socket, ENET_SOCKOPT_RCVBUF, ENET_HOST_RECEIVE_BUFFER_SIZE); + enet_socket_set_option (host -> socket, ENET_SOCKOPT_SNDBUF, ENET_HOST_SEND_BUFFER_SIZE); + + if (address != NULL && enet_socket_get_address (host -> socket, & host -> address) < 0) + host -> address = * address; + + if (! channelLimit || channelLimit > ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT) + channelLimit = ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT; + else + if (channelLimit < ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT) + channelLimit = ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT; + + host -> randomSeed = (enet_uint32) (size_t) host; + host -> randomSeed += enet_host_random_seed (); + host -> randomSeed = (host -> randomSeed << 16) | (host -> randomSeed >> 16); + host -> channelLimit = channelLimit; + host -> incomingBandwidth = incomingBandwidth; + host -> outgoingBandwidth = outgoingBandwidth; + host -> bandwidthThrottleEpoch = 0; + host -> recalculateBandwidthLimits = 0; + host -> mtu = ENET_HOST_DEFAULT_MTU; + host -> peerCount = peerCount; + host -> commandCount = 0; + host -> bufferCount = 0; + host -> checksum = NULL; + host -> receivedAddress.host = ENET_HOST_ANY; + host -> receivedAddress.port = 0; + host -> receivedData = NULL; + host -> receivedDataLength = 0; + + host -> totalSentData = 0; + host -> totalSentPackets = 0; + host -> totalReceivedData = 0; + host -> totalReceivedPackets = 0; + + host -> connectedPeers = 0; + host -> bandwidthLimitedPeers = 0; + host -> duplicatePeers = ENET_PROTOCOL_MAXIMUM_PEER_ID; + + host -> compressor.context = NULL; + host -> compressor.compress = NULL; + host -> compressor.decompress = NULL; + host -> compressor.destroy = NULL; + + host -> intercept = NULL; + + enet_list_clear (& host -> dispatchQueue); + + for (currentPeer = host -> peers; + currentPeer < & host -> peers [host -> peerCount]; + ++ currentPeer) + { + currentPeer -> host = host; + currentPeer -> incomingPeerID = currentPeer - host -> peers; + currentPeer -> outgoingSessionID = currentPeer -> incomingSessionID = 0xFF; + currentPeer -> data = NULL; + + enet_list_clear (& currentPeer -> acknowledgements); + enet_list_clear (& currentPeer -> sentReliableCommands); + enet_list_clear (& currentPeer -> sentUnreliableCommands); + enet_list_clear (& currentPeer -> outgoingReliableCommands); + enet_list_clear (& currentPeer -> outgoingUnreliableCommands); + enet_list_clear (& currentPeer -> dispatchedCommands); + + enet_peer_reset (currentPeer); + } + + return host; +} + +/** Destroys the host and all resources associated with it. + @param host pointer to the host to destroy +*/ +void +enet_host_destroy (ENetHost * host) +{ + ENetPeer * currentPeer; + + if (host == NULL) + return; + + enet_socket_destroy (host -> socket); + + for (currentPeer = host -> peers; + currentPeer < & host -> peers [host -> peerCount]; + ++ currentPeer) + { + enet_peer_reset (currentPeer); + } + + if (host -> compressor.context != NULL && host -> compressor.destroy) + (* host -> compressor.destroy) (host -> compressor.context); + + enet_free (host -> peers); + enet_free (host); +} + +/** Initiates a connection to a foreign host. + @param host host seeking the connection + @param address destination for the connection + @param channelCount number of channels to allocate + @param data user data supplied to the receiving host + @returns a peer representing the foreign host on success, NULL on failure + @remarks The peer returned will have not completed the connection until enet_host_service() + notifies of an ENET_EVENT_TYPE_CONNECT event for the peer. +*/ +ENetPeer * +enet_host_connect (ENetHost * host, const ENetAddress * address, size_t channelCount, enet_uint32 data) +{ + ENetPeer * currentPeer; + ENetChannel * channel; + ENetProtocol command; + + if (channelCount < ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT) + channelCount = ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT; + else + if (channelCount > ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT) + channelCount = ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT; + + for (currentPeer = host -> peers; + currentPeer < & host -> peers [host -> peerCount]; + ++ currentPeer) + { + if (currentPeer -> state == ENET_PEER_STATE_DISCONNECTED) + break; + } + + if (currentPeer >= & host -> peers [host -> peerCount]) + return NULL; + + currentPeer -> channels = (ENetChannel *) enet_malloc (channelCount * sizeof (ENetChannel)); + if (currentPeer -> channels == NULL) + return NULL; + currentPeer -> channelCount = channelCount; + currentPeer -> state = ENET_PEER_STATE_CONNECTING; + currentPeer -> address = * address; + currentPeer -> connectID = ++ host -> randomSeed; + + if (host -> outgoingBandwidth == 0) + currentPeer -> windowSize = ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE; + else + currentPeer -> windowSize = (host -> outgoingBandwidth / + ENET_PEER_WINDOW_SIZE_SCALE) * + ENET_PROTOCOL_MINIMUM_WINDOW_SIZE; + + if (currentPeer -> windowSize < ENET_PROTOCOL_MINIMUM_WINDOW_SIZE) + currentPeer -> windowSize = ENET_PROTOCOL_MINIMUM_WINDOW_SIZE; + else + if (currentPeer -> windowSize > ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE) + currentPeer -> windowSize = ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE; + + for (channel = currentPeer -> channels; + channel < & currentPeer -> channels [channelCount]; + ++ channel) + { + channel -> outgoingReliableSequenceNumber = 0; + channel -> outgoingUnreliableSequenceNumber = 0; + channel -> incomingReliableSequenceNumber = 0; + channel -> incomingUnreliableSequenceNumber = 0; + + enet_list_clear (& channel -> incomingReliableCommands); + enet_list_clear (& channel -> incomingUnreliableCommands); + + channel -> usedReliableWindows = 0; + memset (channel -> reliableWindows, 0, sizeof (channel -> reliableWindows)); + } + + command.header.command = ENET_PROTOCOL_COMMAND_CONNECT | ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE; + command.header.channelID = 0xFF; + command.connect.outgoingPeerID = ENET_HOST_TO_NET_16 (currentPeer -> incomingPeerID); + command.connect.incomingSessionID = currentPeer -> incomingSessionID; + command.connect.outgoingSessionID = currentPeer -> outgoingSessionID; + command.connect.mtu = ENET_HOST_TO_NET_32 (currentPeer -> mtu); + command.connect.windowSize = ENET_HOST_TO_NET_32 (currentPeer -> windowSize); + command.connect.channelCount = ENET_HOST_TO_NET_32 (channelCount); + command.connect.incomingBandwidth = ENET_HOST_TO_NET_32 (host -> incomingBandwidth); + command.connect.outgoingBandwidth = ENET_HOST_TO_NET_32 (host -> outgoingBandwidth); + command.connect.packetThrottleInterval = ENET_HOST_TO_NET_32 (currentPeer -> packetThrottleInterval); + command.connect.packetThrottleAcceleration = ENET_HOST_TO_NET_32 (currentPeer -> packetThrottleAcceleration); + command.connect.packetThrottleDeceleration = ENET_HOST_TO_NET_32 (currentPeer -> packetThrottleDeceleration); + command.connect.connectID = currentPeer -> connectID; + command.connect.data = ENET_HOST_TO_NET_32 (data); + + enet_peer_queue_outgoing_command (currentPeer, & command, NULL, 0, 0); + + return currentPeer; +} + +/** Queues a packet to be sent to all peers associated with the host. + @param host host on which to broadcast the packet + @param channelID channel on which to broadcast + @param packet packet to broadcast +*/ +void +enet_host_broadcast (ENetHost * host, enet_uint8 channelID, ENetPacket * packet) +{ + ENetPeer * currentPeer; + + for (currentPeer = host -> peers; + currentPeer < & host -> peers [host -> peerCount]; + ++ currentPeer) + { + if (currentPeer -> state != ENET_PEER_STATE_CONNECTED) + continue; + + enet_peer_send (currentPeer, channelID, packet); + } + + if (packet -> referenceCount == 0) + enet_packet_destroy (packet); +} + +/** Sets the packet compressor the host should use to compress and decompress packets. + @param host host to enable or disable compression for + @param compressor callbacks for for the packet compressor; if NULL, then compression is disabled +*/ +void +enet_host_compress (ENetHost * host, const ENetCompressor * compressor) +{ + if (host -> compressor.context != NULL && host -> compressor.destroy) + (* host -> compressor.destroy) (host -> compressor.context); + + if (compressor) + host -> compressor = * compressor; + else + host -> compressor.context = NULL; +} + +/** Limits the maximum allowed channels of future incoming connections. + @param host host to limit + @param channelLimit the maximum number of channels allowed; if 0, then this is equivalent to ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT +*/ +void +enet_host_channel_limit (ENetHost * host, size_t channelLimit) +{ + if (! channelLimit || channelLimit > ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT) + channelLimit = ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT; + else + if (channelLimit < ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT) + channelLimit = ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT; + + host -> channelLimit = channelLimit; +} + + +/** Adjusts the bandwidth limits of a host. + @param host host to adjust + @param incomingBandwidth new incoming bandwidth + @param outgoingBandwidth new outgoing bandwidth + @remarks the incoming and outgoing bandwidth parameters are identical in function to those + specified in enet_host_create(). +*/ +void +enet_host_bandwidth_limit (ENetHost * host, enet_uint32 incomingBandwidth, enet_uint32 outgoingBandwidth) +{ + host -> incomingBandwidth = incomingBandwidth; + host -> outgoingBandwidth = outgoingBandwidth; + host -> recalculateBandwidthLimits = 1; +} + +void +enet_host_bandwidth_throttle (ENetHost * host) +{ + enet_uint32 timeCurrent = enet_time_get (), + elapsedTime = timeCurrent - host -> bandwidthThrottleEpoch, + peersRemaining = (enet_uint32) host -> connectedPeers, + dataTotal = ~0, + bandwidth = ~0, + throttle = 0, + bandwidthLimit = 0; + int needsAdjustment = host -> bandwidthLimitedPeers > 0 ? 1 : 0; + ENetPeer * peer; + ENetProtocol command; + + if (elapsedTime < ENET_HOST_BANDWIDTH_THROTTLE_INTERVAL) + return; + + host -> bandwidthThrottleEpoch = timeCurrent; + + if (peersRemaining == 0) + return; + + if (host -> outgoingBandwidth != 0) + { + dataTotal = 0; + bandwidth = (host -> outgoingBandwidth * elapsedTime) / 1000; + + for (peer = host -> peers; + peer < & host -> peers [host -> peerCount]; + ++ peer) + { + if (peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER) + continue; + + dataTotal += peer -> outgoingDataTotal; + } + } + + while (peersRemaining > 0 && needsAdjustment != 0) + { + needsAdjustment = 0; + + if (dataTotal <= bandwidth) + throttle = ENET_PEER_PACKET_THROTTLE_SCALE; + else + throttle = (bandwidth * ENET_PEER_PACKET_THROTTLE_SCALE) / dataTotal; + + for (peer = host -> peers; + peer < & host -> peers [host -> peerCount]; + ++ peer) + { + enet_uint32 peerBandwidth; + + if ((peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER) || + peer -> incomingBandwidth == 0 || + peer -> outgoingBandwidthThrottleEpoch == timeCurrent) + continue; + + peerBandwidth = (peer -> incomingBandwidth * elapsedTime) / 1000; + if ((throttle * peer -> outgoingDataTotal) / ENET_PEER_PACKET_THROTTLE_SCALE <= peerBandwidth) + continue; + + peer -> packetThrottleLimit = (peerBandwidth * + ENET_PEER_PACKET_THROTTLE_SCALE) / peer -> outgoingDataTotal; + + if (peer -> packetThrottleLimit == 0) + peer -> packetThrottleLimit = 1; + + if (peer -> packetThrottle > peer -> packetThrottleLimit) + peer -> packetThrottle = peer -> packetThrottleLimit; + + peer -> outgoingBandwidthThrottleEpoch = timeCurrent; + + peer -> incomingDataTotal = 0; + peer -> outgoingDataTotal = 0; + + needsAdjustment = 1; + -- peersRemaining; + bandwidth -= peerBandwidth; + dataTotal -= peerBandwidth; + } + } + + if (peersRemaining > 0) + { + if (dataTotal <= bandwidth) + throttle = ENET_PEER_PACKET_THROTTLE_SCALE; + else + throttle = (bandwidth * ENET_PEER_PACKET_THROTTLE_SCALE) / dataTotal; + + for (peer = host -> peers; + peer < & host -> peers [host -> peerCount]; + ++ peer) + { + if ((peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER) || + peer -> outgoingBandwidthThrottleEpoch == timeCurrent) + continue; + + peer -> packetThrottleLimit = throttle; + + if (peer -> packetThrottle > peer -> packetThrottleLimit) + peer -> packetThrottle = peer -> packetThrottleLimit; + + peer -> incomingDataTotal = 0; + peer -> outgoingDataTotal = 0; + } + } + + if (host -> recalculateBandwidthLimits) + { + host -> recalculateBandwidthLimits = 0; + + peersRemaining = (enet_uint32) host -> connectedPeers; + bandwidth = host -> incomingBandwidth; + needsAdjustment = 1; + + if (bandwidth == 0) + bandwidthLimit = 0; + else + while (peersRemaining > 0 && needsAdjustment != 0) + { + needsAdjustment = 0; + bandwidthLimit = bandwidth / peersRemaining; + + for (peer = host -> peers; + peer < & host -> peers [host -> peerCount]; + ++ peer) + { + if ((peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER) || + peer -> incomingBandwidthThrottleEpoch == timeCurrent) + continue; + + if (peer -> outgoingBandwidth > 0 && + peer -> outgoingBandwidth >= bandwidthLimit) + continue; + + peer -> incomingBandwidthThrottleEpoch = timeCurrent; + + needsAdjustment = 1; + -- peersRemaining; + bandwidth -= peer -> outgoingBandwidth; + } + } + + for (peer = host -> peers; + peer < & host -> peers [host -> peerCount]; + ++ peer) + { + if (peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER) + continue; + + command.header.command = ENET_PROTOCOL_COMMAND_BANDWIDTH_LIMIT | ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE; + command.header.channelID = 0xFF; + command.bandwidthLimit.outgoingBandwidth = ENET_HOST_TO_NET_32 (host -> outgoingBandwidth); + + if (peer -> incomingBandwidthThrottleEpoch == timeCurrent) + command.bandwidthLimit.incomingBandwidth = ENET_HOST_TO_NET_32 (peer -> outgoingBandwidth); + else + command.bandwidthLimit.incomingBandwidth = ENET_HOST_TO_NET_32 (bandwidthLimit); + + enet_peer_queue_outgoing_command (peer, & command, NULL, 0, 0); + } + } +} + +/** @} */ diff --git a/Externals/enet/list.c b/Externals/enet/list.c new file mode 100644 index 000000000000..1c1a8dfaafc9 --- /dev/null +++ b/Externals/enet/list.c @@ -0,0 +1,75 @@ +/** + @file list.c + @brief ENet linked list functions +*/ +#define ENET_BUILDING_LIB 1 +#include "enet/enet.h" + +/** + @defgroup list ENet linked list utility functions + @ingroup private + @{ +*/ +void +enet_list_clear (ENetList * list) +{ + list -> sentinel.next = & list -> sentinel; + list -> sentinel.previous = & list -> sentinel; +} + +ENetListIterator +enet_list_insert (ENetListIterator position, void * data) +{ + ENetListIterator result = (ENetListIterator) data; + + result -> previous = position -> previous; + result -> next = position; + + result -> previous -> next = result; + position -> previous = result; + + return result; +} + +void * +enet_list_remove (ENetListIterator position) +{ + position -> previous -> next = position -> next; + position -> next -> previous = position -> previous; + + return position; +} + +ENetListIterator +enet_list_move (ENetListIterator position, void * dataFirst, void * dataLast) +{ + ENetListIterator first = (ENetListIterator) dataFirst, + last = (ENetListIterator) dataLast; + + first -> previous -> next = last -> next; + last -> next -> previous = first -> previous; + + first -> previous = position -> previous; + last -> next = position; + + first -> previous -> next = first; + position -> previous = last; + + return first; +} + +size_t +enet_list_size (ENetList * list) +{ + size_t size = 0; + ENetListIterator position; + + for (position = enet_list_begin (list); + position != enet_list_end (list); + position = enet_list_next (position)) + ++ size; + + return size; +} + +/** @} */ diff --git a/Externals/enet/list.h b/Externals/enet/list.h new file mode 100644 index 000000000000..d7b2600848ff --- /dev/null +++ b/Externals/enet/list.h @@ -0,0 +1,43 @@ +/** + @file list.h + @brief ENet list management +*/ +#ifndef __ENET_LIST_H__ +#define __ENET_LIST_H__ + +#include + +typedef struct _ENetListNode +{ + struct _ENetListNode * next; + struct _ENetListNode * previous; +} ENetListNode; + +typedef ENetListNode * ENetListIterator; + +typedef struct _ENetList +{ + ENetListNode sentinel; +} ENetList; + +extern void enet_list_clear (ENetList *); + +extern ENetListIterator enet_list_insert (ENetListIterator, void *); +extern void * enet_list_remove (ENetListIterator); +extern ENetListIterator enet_list_move (ENetListIterator, void *, void *); + +extern size_t enet_list_size (ENetList *); + +#define enet_list_begin(list) ((list) -> sentinel.next) +#define enet_list_end(list) (& (list) -> sentinel) + +#define enet_list_empty(list) (enet_list_begin (list) == enet_list_end (list)) + +#define enet_list_next(iterator) ((iterator) -> next) +#define enet_list_previous(iterator) ((iterator) -> previous) + +#define enet_list_front(list) ((void *) (list) -> sentinel.next) +#define enet_list_back(list) ((void *) (list) -> sentinel.previous) + +#endif /* __ENET_LIST_H__ */ + diff --git a/Externals/enet/packet.c b/Externals/enet/packet.c new file mode 100644 index 000000000000..9a997be4e29d --- /dev/null +++ b/Externals/enet/packet.c @@ -0,0 +1,165 @@ +/** + @file packet.c + @brief ENet packet management functions +*/ +#include +#define ENET_BUILDING_LIB 1 +#include "enet/enet.h" + +/** @defgroup Packet ENet packet functions + @{ +*/ + +/** Creates a packet that may be sent to a peer. + @param dataContents initial contents of the packet's data; the packet's data will remain uninitialized if dataContents is NULL. + @param dataLength size of the data allocated for this packet + @param flags flags for this packet as described for the ENetPacket structure. + @returns the packet on success, NULL on failure +*/ +ENetPacket * +enet_packet_create (const void * data, size_t dataLength, enet_uint32 flags) +{ + ENetPacket * packet = (ENetPacket *) enet_malloc (sizeof (ENetPacket)); + if (packet == NULL) + return NULL; + + if (flags & ENET_PACKET_FLAG_NO_ALLOCATE) + packet -> data = (enet_uint8 *) data; + else + if (dataLength <= 0) + packet -> data = NULL; + else + { + packet -> data = (enet_uint8 *) enet_malloc (dataLength); + if (packet -> data == NULL) + { + enet_free (packet); + return NULL; + } + + if (data != NULL) + memcpy (packet -> data, data, dataLength); + } + + packet -> referenceCount = 0; + packet -> flags = flags; + packet -> dataLength = dataLength; + packet -> freeCallback = NULL; + packet -> userData = NULL; + + return packet; +} + +/** Destroys the packet and deallocates its data. + @param packet packet to be destroyed +*/ +void +enet_packet_destroy (ENetPacket * packet) +{ + if (packet == NULL) + return; + + if (packet -> freeCallback != NULL) + (* packet -> freeCallback) (packet); + if (! (packet -> flags & ENET_PACKET_FLAG_NO_ALLOCATE) && + packet -> data != NULL) + enet_free (packet -> data); + enet_free (packet); +} + +/** Attempts to resize the data in the packet to length specified in the + dataLength parameter + @param packet packet to resize + @param dataLength new size for the packet data + @returns 0 on success, < 0 on failure +*/ +int +enet_packet_resize (ENetPacket * packet, size_t dataLength) +{ + enet_uint8 * newData; + + if (dataLength <= packet -> dataLength || (packet -> flags & ENET_PACKET_FLAG_NO_ALLOCATE)) + { + packet -> dataLength = dataLength; + + return 0; + } + + newData = (enet_uint8 *) enet_malloc (dataLength); + if (newData == NULL) + return -1; + + memcpy (newData, packet -> data, packet -> dataLength); + enet_free (packet -> data); + + packet -> data = newData; + packet -> dataLength = dataLength; + + return 0; +} + +static int initializedCRC32 = 0; +static enet_uint32 crcTable [256]; + +static enet_uint32 +reflect_crc (int val, int bits) +{ + int result = 0, bit; + + for (bit = 0; bit < bits; bit ++) + { + if(val & 1) result |= 1 << (bits - 1 - bit); + val >>= 1; + } + + return result; +} + +static void +initialize_crc32 (void) +{ + int byte; + + for (byte = 0; byte < 256; ++ byte) + { + enet_uint32 crc = reflect_crc (byte, 8) << 24; + int offset; + + for(offset = 0; offset < 8; ++ offset) + { + if (crc & 0x80000000) + crc = (crc << 1) ^ 0x04c11db7; + else + crc <<= 1; + } + + crcTable [byte] = reflect_crc (crc, 32); + } + + initializedCRC32 = 1; +} + +enet_uint32 +enet_crc32 (const ENetBuffer * buffers, size_t bufferCount) +{ + enet_uint32 crc = 0xFFFFFFFF; + + if (! initializedCRC32) initialize_crc32 (); + + while (bufferCount -- > 0) + { + const enet_uint8 * data = (const enet_uint8 *) buffers -> data, + * dataEnd = & data [buffers -> dataLength]; + + while (data < dataEnd) + { + crc = (crc >> 8) ^ crcTable [(crc & 0xFF) ^ *data++]; + } + + ++ buffers; + } + + return ENET_HOST_TO_NET_32 (~ crc); +} + +/** @} */ diff --git a/Externals/enet/peer.c b/Externals/enet/peer.c new file mode 100644 index 000000000000..318f2c259ad0 --- /dev/null +++ b/Externals/enet/peer.c @@ -0,0 +1,989 @@ +/** + @file peer.c + @brief ENet peer management functions +*/ +#include +#define ENET_BUILDING_LIB 1 +#include "enet/enet.h" + +/** @defgroup peer ENet peer functions + @{ +*/ + +/** Configures throttle parameter for a peer. + + Unreliable packets are dropped by ENet in response to the varying conditions + of the Internet connection to the peer. The throttle represents a probability + that an unreliable packet should not be dropped and thus sent by ENet to the peer. + The lowest mean round trip time from the sending of a reliable packet to the + receipt of its acknowledgement is measured over an amount of time specified by + the interval parameter in milliseconds. If a measured round trip time happens to + be significantly less than the mean round trip time measured over the interval, + then the throttle probability is increased to allow more traffic by an amount + specified in the acceleration parameter, which is a ratio to the ENET_PEER_PACKET_THROTTLE_SCALE + constant. If a measured round trip time happens to be significantly greater than + the mean round trip time measured over the interval, then the throttle probability + is decreased to limit traffic by an amount specified in the deceleration parameter, which + is a ratio to the ENET_PEER_PACKET_THROTTLE_SCALE constant. When the throttle has + a value of ENET_PEER_PACKET_THROTTLE_SCALE, no unreliable packets are dropped by + ENet, and so 100% of all unreliable packets will be sent. When the throttle has a + value of 0, all unreliable packets are dropped by ENet, and so 0% of all unreliable + packets will be sent. Intermediate values for the throttle represent intermediate + probabilities between 0% and 100% of unreliable packets being sent. The bandwidth + limits of the local and foreign hosts are taken into account to determine a + sensible limit for the throttle probability above which it should not raise even in + the best of conditions. + + @param peer peer to configure + @param interval interval, in milliseconds, over which to measure lowest mean RTT; the default value is ENET_PEER_PACKET_THROTTLE_INTERVAL. + @param acceleration rate at which to increase the throttle probability as mean RTT declines + @param deceleration rate at which to decrease the throttle probability as mean RTT increases +*/ +void +enet_peer_throttle_configure (ENetPeer * peer, enet_uint32 interval, enet_uint32 acceleration, enet_uint32 deceleration) +{ + ENetProtocol command; + + peer -> packetThrottleInterval = interval; + peer -> packetThrottleAcceleration = acceleration; + peer -> packetThrottleDeceleration = deceleration; + + command.header.command = ENET_PROTOCOL_COMMAND_THROTTLE_CONFIGURE | ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE; + command.header.channelID = 0xFF; + + command.throttleConfigure.packetThrottleInterval = ENET_HOST_TO_NET_32 (interval); + command.throttleConfigure.packetThrottleAcceleration = ENET_HOST_TO_NET_32 (acceleration); + command.throttleConfigure.packetThrottleDeceleration = ENET_HOST_TO_NET_32 (deceleration); + + enet_peer_queue_outgoing_command (peer, & command, NULL, 0, 0); +} + +int +enet_peer_throttle (ENetPeer * peer, enet_uint32 rtt) +{ + if (peer -> lastRoundTripTime <= peer -> lastRoundTripTimeVariance) + { + peer -> packetThrottle = peer -> packetThrottleLimit; + } + else + if (rtt < peer -> lastRoundTripTime) + { + peer -> packetThrottle += peer -> packetThrottleAcceleration; + + if (peer -> packetThrottle > peer -> packetThrottleLimit) + peer -> packetThrottle = peer -> packetThrottleLimit; + + return 1; + } + else + if (rtt > peer -> lastRoundTripTime + 2 * peer -> lastRoundTripTimeVariance) + { + if (peer -> packetThrottle > peer -> packetThrottleDeceleration) + peer -> packetThrottle -= peer -> packetThrottleDeceleration; + else + peer -> packetThrottle = 0; + + return -1; + } + + return 0; +} + +/** Queues a packet to be sent. + @param peer destination for the packet + @param channelID channel on which to send + @param packet packet to send + @retval 0 on success + @retval < 0 on failure +*/ +int +enet_peer_send (ENetPeer * peer, enet_uint8 channelID, ENetPacket * packet) +{ + ENetChannel * channel = & peer -> channels [channelID]; + ENetProtocol command; + size_t fragmentLength; + + if (peer -> state != ENET_PEER_STATE_CONNECTED || + channelID >= peer -> channelCount || + packet -> dataLength > ENET_PROTOCOL_MAXIMUM_PACKET_SIZE) + return -1; + + fragmentLength = peer -> mtu - sizeof (ENetProtocolHeader) - sizeof (ENetProtocolSendFragment); + if (peer -> host -> checksum != NULL) + fragmentLength -= sizeof(enet_uint32); + + if (packet -> dataLength > fragmentLength) + { + enet_uint32 fragmentCount = (packet -> dataLength + fragmentLength - 1) / fragmentLength, + fragmentNumber, + fragmentOffset; + enet_uint8 commandNumber; + enet_uint16 startSequenceNumber; + ENetList fragments; + ENetOutgoingCommand * fragment; + + if (fragmentCount > ENET_PROTOCOL_MAXIMUM_FRAGMENT_COUNT) + return -1; + + if ((packet -> flags & (ENET_PACKET_FLAG_RELIABLE | ENET_PACKET_FLAG_UNRELIABLE_FRAGMENT)) == ENET_PACKET_FLAG_UNRELIABLE_FRAGMENT && + channel -> outgoingUnreliableSequenceNumber < 0xFFFF) + { + commandNumber = ENET_PROTOCOL_COMMAND_SEND_UNRELIABLE_FRAGMENT; + startSequenceNumber = ENET_HOST_TO_NET_16 (channel -> outgoingUnreliableSequenceNumber + 1); + } + else + { + commandNumber = ENET_PROTOCOL_COMMAND_SEND_FRAGMENT | ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE; + startSequenceNumber = ENET_HOST_TO_NET_16 (channel -> outgoingReliableSequenceNumber + 1); + } + + enet_list_clear (& fragments); + + for (fragmentNumber = 0, + fragmentOffset = 0; + fragmentOffset < packet -> dataLength; + ++ fragmentNumber, + fragmentOffset += fragmentLength) + { + if (packet -> dataLength - fragmentOffset < fragmentLength) + fragmentLength = packet -> dataLength - fragmentOffset; + + fragment = (ENetOutgoingCommand *) enet_malloc (sizeof (ENetOutgoingCommand)); + if (fragment == NULL) + { + while (! enet_list_empty (& fragments)) + { + fragment = (ENetOutgoingCommand *) enet_list_remove (enet_list_begin (& fragments)); + + enet_free (fragment); + } + + return -1; + } + + fragment -> fragmentOffset = fragmentOffset; + fragment -> fragmentLength = fragmentLength; + fragment -> packet = packet; + fragment -> command.header.command = commandNumber; + fragment -> command.header.channelID = channelID; + fragment -> command.sendFragment.startSequenceNumber = startSequenceNumber; + fragment -> command.sendFragment.dataLength = ENET_HOST_TO_NET_16 (fragmentLength); + fragment -> command.sendFragment.fragmentCount = ENET_HOST_TO_NET_32 (fragmentCount); + fragment -> command.sendFragment.fragmentNumber = ENET_HOST_TO_NET_32 (fragmentNumber); + fragment -> command.sendFragment.totalLength = ENET_HOST_TO_NET_32 (packet -> dataLength); + fragment -> command.sendFragment.fragmentOffset = ENET_NET_TO_HOST_32 (fragmentOffset); + + enet_list_insert (enet_list_end (& fragments), fragment); + } + + packet -> referenceCount += fragmentNumber; + + while (! enet_list_empty (& fragments)) + { + fragment = (ENetOutgoingCommand *) enet_list_remove (enet_list_begin (& fragments)); + + enet_peer_setup_outgoing_command (peer, fragment); + } + + return 0; + } + + command.header.channelID = channelID; + + if ((packet -> flags & (ENET_PACKET_FLAG_RELIABLE | ENET_PACKET_FLAG_UNSEQUENCED)) == ENET_PACKET_FLAG_UNSEQUENCED) + { + command.header.command = ENET_PROTOCOL_COMMAND_SEND_UNSEQUENCED | ENET_PROTOCOL_COMMAND_FLAG_UNSEQUENCED; + command.sendUnsequenced.dataLength = ENET_HOST_TO_NET_16 (packet -> dataLength); + } + else + if (packet -> flags & ENET_PACKET_FLAG_RELIABLE || channel -> outgoingUnreliableSequenceNumber >= 0xFFFF) + { + command.header.command = ENET_PROTOCOL_COMMAND_SEND_RELIABLE | ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE; + command.sendReliable.dataLength = ENET_HOST_TO_NET_16 (packet -> dataLength); + } + else + { + command.header.command = ENET_PROTOCOL_COMMAND_SEND_UNRELIABLE; + command.sendUnreliable.dataLength = ENET_HOST_TO_NET_16 (packet -> dataLength); + } + + if (enet_peer_queue_outgoing_command (peer, & command, packet, 0, packet -> dataLength) == NULL) + return -1; + + return 0; +} + +/** Attempts to dequeue any incoming queued packet. + @param peer peer to dequeue packets from + @param channelID holds the channel ID of the channel the packet was received on success + @returns a pointer to the packet, or NULL if there are no available incoming queued packets +*/ +ENetPacket * +enet_peer_receive (ENetPeer * peer, enet_uint8 * channelID) +{ + ENetIncomingCommand * incomingCommand; + ENetPacket * packet; + + if (enet_list_empty (& peer -> dispatchedCommands)) + return NULL; + + incomingCommand = (ENetIncomingCommand *) enet_list_remove (enet_list_begin (& peer -> dispatchedCommands)); + + if (channelID != NULL) + * channelID = incomingCommand -> command.header.channelID; + + packet = incomingCommand -> packet; + + -- packet -> referenceCount; + + if (incomingCommand -> fragments != NULL) + enet_free (incomingCommand -> fragments); + + enet_free (incomingCommand); + + return packet; +} + +static void +enet_peer_reset_outgoing_commands (ENetList * queue) +{ + ENetOutgoingCommand * outgoingCommand; + + while (! enet_list_empty (queue)) + { + outgoingCommand = (ENetOutgoingCommand *) enet_list_remove (enet_list_begin (queue)); + + if (outgoingCommand -> packet != NULL) + { + -- outgoingCommand -> packet -> referenceCount; + + if (outgoingCommand -> packet -> referenceCount == 0) + enet_packet_destroy (outgoingCommand -> packet); + } + + enet_free (outgoingCommand); + } +} + +static void +enet_peer_remove_incoming_commands (ENetList * queue, ENetListIterator startCommand, ENetListIterator endCommand) +{ + ENetListIterator currentCommand; + + for (currentCommand = startCommand; currentCommand != endCommand; ) + { + ENetIncomingCommand * incomingCommand = (ENetIncomingCommand *) currentCommand; + + currentCommand = enet_list_next (currentCommand); + + enet_list_remove (& incomingCommand -> incomingCommandList); + + if (incomingCommand -> packet != NULL) + { + -- incomingCommand -> packet -> referenceCount; + + if (incomingCommand -> packet -> referenceCount == 0) + enet_packet_destroy (incomingCommand -> packet); + } + + if (incomingCommand -> fragments != NULL) + enet_free (incomingCommand -> fragments); + + enet_free (incomingCommand); + } +} + +static void +enet_peer_reset_incoming_commands (ENetList * queue) +{ + enet_peer_remove_incoming_commands(queue, enet_list_begin (queue), enet_list_end (queue)); +} + +void +enet_peer_reset_queues (ENetPeer * peer) +{ + ENetChannel * channel; + + if (peer -> needsDispatch) + { + enet_list_remove (& peer -> dispatchList); + + peer -> needsDispatch = 0; + } + + while (! enet_list_empty (& peer -> acknowledgements)) + enet_free (enet_list_remove (enet_list_begin (& peer -> acknowledgements))); + + enet_peer_reset_outgoing_commands (& peer -> sentReliableCommands); + enet_peer_reset_outgoing_commands (& peer -> sentUnreliableCommands); + enet_peer_reset_outgoing_commands (& peer -> outgoingReliableCommands); + enet_peer_reset_outgoing_commands (& peer -> outgoingUnreliableCommands); + enet_peer_reset_incoming_commands (& peer -> dispatchedCommands); + + if (peer -> channels != NULL && peer -> channelCount > 0) + { + for (channel = peer -> channels; + channel < & peer -> channels [peer -> channelCount]; + ++ channel) + { + enet_peer_reset_incoming_commands (& channel -> incomingReliableCommands); + enet_peer_reset_incoming_commands (& channel -> incomingUnreliableCommands); + } + + enet_free (peer -> channels); + } + + peer -> channels = NULL; + peer -> channelCount = 0; +} + +void +enet_peer_on_connect (ENetPeer * peer) +{ + if (peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER) + { + if (peer -> incomingBandwidth != 0) + ++ peer -> host -> bandwidthLimitedPeers; + + ++ peer -> host -> connectedPeers; + } +} + +void +enet_peer_on_disconnect (ENetPeer * peer) +{ + if (peer -> state == ENET_PEER_STATE_CONNECTED || peer -> state == ENET_PEER_STATE_DISCONNECT_LATER) + { + if (peer -> incomingBandwidth != 0) + -- peer -> host -> bandwidthLimitedPeers; + + -- peer -> host -> connectedPeers; + } +} + +/** Forcefully disconnects a peer. + @param peer peer to forcefully disconnect + @remarks The foreign host represented by the peer is not notified of the disconnection and will timeout + on its connection to the local host. +*/ +void +enet_peer_reset (ENetPeer * peer) +{ + enet_peer_on_disconnect (peer); + + peer -> outgoingPeerID = ENET_PROTOCOL_MAXIMUM_PEER_ID; + peer -> connectID = 0; + + peer -> state = ENET_PEER_STATE_DISCONNECTED; + + peer -> incomingBandwidth = 0; + peer -> outgoingBandwidth = 0; + peer -> incomingBandwidthThrottleEpoch = 0; + peer -> outgoingBandwidthThrottleEpoch = 0; + peer -> incomingDataTotal = 0; + peer -> outgoingDataTotal = 0; + peer -> lastSendTime = 0; + peer -> lastReceiveTime = 0; + peer -> nextTimeout = 0; + peer -> earliestTimeout = 0; + peer -> packetLossEpoch = 0; + peer -> packetsSent = 0; + peer -> packetsLost = 0; + peer -> packetLoss = 0; + peer -> packetLossVariance = 0; + peer -> packetThrottle = ENET_PEER_DEFAULT_PACKET_THROTTLE; + peer -> packetThrottleLimit = ENET_PEER_PACKET_THROTTLE_SCALE; + peer -> packetThrottleCounter = 0; + peer -> packetThrottleEpoch = 0; + peer -> packetThrottleAcceleration = ENET_PEER_PACKET_THROTTLE_ACCELERATION; + peer -> packetThrottleDeceleration = ENET_PEER_PACKET_THROTTLE_DECELERATION; + peer -> packetThrottleInterval = ENET_PEER_PACKET_THROTTLE_INTERVAL; + peer -> pingInterval = ENET_PEER_PING_INTERVAL; + peer -> timeoutLimit = ENET_PEER_TIMEOUT_LIMIT; + peer -> timeoutMinimum = ENET_PEER_TIMEOUT_MINIMUM; + peer -> timeoutMaximum = ENET_PEER_TIMEOUT_MAXIMUM; + peer -> lastRoundTripTime = ENET_PEER_DEFAULT_ROUND_TRIP_TIME; + peer -> lowestRoundTripTime = ENET_PEER_DEFAULT_ROUND_TRIP_TIME; + peer -> lastRoundTripTimeVariance = 0; + peer -> highestRoundTripTimeVariance = 0; + peer -> roundTripTime = ENET_PEER_DEFAULT_ROUND_TRIP_TIME; + peer -> roundTripTimeVariance = 0; + peer -> mtu = peer -> host -> mtu; + peer -> reliableDataInTransit = 0; + peer -> outgoingReliableSequenceNumber = 0; + peer -> windowSize = ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE; + peer -> incomingUnsequencedGroup = 0; + peer -> outgoingUnsequencedGroup = 0; + peer -> eventData = 0; + + memset (peer -> unsequencedWindow, 0, sizeof (peer -> unsequencedWindow)); + + enet_peer_reset_queues (peer); +} + +/** Sends a ping request to a peer. + @param peer destination for the ping request + @remarks ping requests factor into the mean round trip time as designated by the + roundTripTime field in the ENetPeer structure. Enet automatically pings all connected + peers at regular intervals, however, this function may be called to ensure more + frequent ping requests. +*/ +void +enet_peer_ping (ENetPeer * peer) +{ + ENetProtocol command; + + if (peer -> state != ENET_PEER_STATE_CONNECTED) + return; + + command.header.command = ENET_PROTOCOL_COMMAND_PING | ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE; + command.header.channelID = 0xFF; + + enet_peer_queue_outgoing_command (peer, & command, NULL, 0, 0); +} + +/** Sets the interval at which pings will be sent to a peer. + + Pings are used both to monitor the liveness of the connection and also to dynamically + adjust the throttle during periods of low traffic so that the throttle has reasonable + responsiveness during traffic spikes. + + @param peer the peer to adjust + @param pingInterval the interval at which to send pings; defaults to ENET_PEER_PING_INTERVAL if 0 +*/ +void +enet_peer_ping_interval (ENetPeer * peer, enet_uint32 pingInterval) +{ + peer -> pingInterval = pingInterval ? pingInterval : ENET_PEER_PING_INTERVAL; +} + +/** Sets the timeout parameters for a peer. + + The timeout parameter control how and when a peer will timeout from a failure to acknowledge + reliable traffic. Timeout values use an exponential backoff mechanism, where if a reliable + packet is not acknowledge within some multiple of the average RTT plus a variance tolerance, + the timeout will be doubled until it reaches a set limit. If the timeout is thus at this + limit and reliable packets have been sent but not acknowledged within a certain minimum time + period, the peer will be disconnected. Alternatively, if reliable packets have been sent + but not acknowledged for a certain maximum time period, the peer will be disconnected regardless + of the current timeout limit value. + + @param peer the peer to adjust + @param timeoutLimit the timeout limit; defaults to ENET_PEER_TIMEOUT_LIMIT if 0 + @param timeoutMinimum the timeout minimum; defaults to ENET_PEER_TIMEOUT_MINIMUM if 0 + @param timeoutMaximum the timeout maximum; defaults to ENET_PEER_TIMEOUT_MAXIMUM if 0 +*/ + +void +enet_peer_timeout (ENetPeer * peer, enet_uint32 timeoutLimit, enet_uint32 timeoutMinimum, enet_uint32 timeoutMaximum) +{ + peer -> timeoutLimit = timeoutLimit ? timeoutLimit : ENET_PEER_TIMEOUT_LIMIT; + peer -> timeoutMinimum = timeoutMinimum ? timeoutMinimum : ENET_PEER_TIMEOUT_MINIMUM; + peer -> timeoutMaximum = timeoutMaximum ? timeoutMaximum : ENET_PEER_TIMEOUT_MAXIMUM; +} + +/** Force an immediate disconnection from a peer. + @param peer peer to disconnect + @param data data describing the disconnection + @remarks No ENET_EVENT_DISCONNECT event will be generated. The foreign peer is not + guarenteed to receive the disconnect notification, and is reset immediately upon + return from this function. +*/ +void +enet_peer_disconnect_now (ENetPeer * peer, enet_uint32 data) +{ + ENetProtocol command; + + if (peer -> state == ENET_PEER_STATE_DISCONNECTED) + return; + + if (peer -> state != ENET_PEER_STATE_ZOMBIE && + peer -> state != ENET_PEER_STATE_DISCONNECTING) + { + enet_peer_reset_queues (peer); + + command.header.command = ENET_PROTOCOL_COMMAND_DISCONNECT | ENET_PROTOCOL_COMMAND_FLAG_UNSEQUENCED; + command.header.channelID = 0xFF; + command.disconnect.data = ENET_HOST_TO_NET_32 (data); + + enet_peer_queue_outgoing_command (peer, & command, NULL, 0, 0); + + enet_host_flush (peer -> host); + } + + enet_peer_reset (peer); +} + +/** Request a disconnection from a peer. + @param peer peer to request a disconnection + @param data data describing the disconnection + @remarks An ENET_EVENT_DISCONNECT event will be generated by enet_host_service() + once the disconnection is complete. +*/ +void +enet_peer_disconnect (ENetPeer * peer, enet_uint32 data) +{ + ENetProtocol command; + + if (peer -> state == ENET_PEER_STATE_DISCONNECTING || + peer -> state == ENET_PEER_STATE_DISCONNECTED || + peer -> state == ENET_PEER_STATE_ACKNOWLEDGING_DISCONNECT || + peer -> state == ENET_PEER_STATE_ZOMBIE) + return; + + enet_peer_reset_queues (peer); + + command.header.command = ENET_PROTOCOL_COMMAND_DISCONNECT; + command.header.channelID = 0xFF; + command.disconnect.data = ENET_HOST_TO_NET_32 (data); + + if (peer -> state == ENET_PEER_STATE_CONNECTED || peer -> state == ENET_PEER_STATE_DISCONNECT_LATER) + command.header.command |= ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE; + else + command.header.command |= ENET_PROTOCOL_COMMAND_FLAG_UNSEQUENCED; + + enet_peer_queue_outgoing_command (peer, & command, NULL, 0, 0); + + if (peer -> state == ENET_PEER_STATE_CONNECTED || peer -> state == ENET_PEER_STATE_DISCONNECT_LATER) + { + enet_peer_on_disconnect (peer); + + peer -> state = ENET_PEER_STATE_DISCONNECTING; + } + else + { + enet_host_flush (peer -> host); + enet_peer_reset (peer); + } +} + +/** Request a disconnection from a peer, but only after all queued outgoing packets are sent. + @param peer peer to request a disconnection + @param data data describing the disconnection + @remarks An ENET_EVENT_DISCONNECT event will be generated by enet_host_service() + once the disconnection is complete. +*/ +void +enet_peer_disconnect_later (ENetPeer * peer, enet_uint32 data) +{ + if ((peer -> state == ENET_PEER_STATE_CONNECTED || peer -> state == ENET_PEER_STATE_DISCONNECT_LATER) && + ! (enet_list_empty (& peer -> outgoingReliableCommands) && + enet_list_empty (& peer -> outgoingUnreliableCommands) && + enet_list_empty (& peer -> sentReliableCommands))) + { + peer -> state = ENET_PEER_STATE_DISCONNECT_LATER; + peer -> eventData = data; + } + else + enet_peer_disconnect (peer, data); +} + +ENetAcknowledgement * +enet_peer_queue_acknowledgement (ENetPeer * peer, const ENetProtocol * command, enet_uint16 sentTime) +{ + ENetAcknowledgement * acknowledgement; + + if (command -> header.channelID < peer -> channelCount) + { + ENetChannel * channel = & peer -> channels [command -> header.channelID]; + enet_uint16 reliableWindow = command -> header.reliableSequenceNumber / ENET_PEER_RELIABLE_WINDOW_SIZE, + currentWindow = channel -> incomingReliableSequenceNumber / ENET_PEER_RELIABLE_WINDOW_SIZE; + + if (command -> header.reliableSequenceNumber < channel -> incomingReliableSequenceNumber) + reliableWindow += ENET_PEER_RELIABLE_WINDOWS; + + if (reliableWindow >= currentWindow + ENET_PEER_FREE_RELIABLE_WINDOWS - 1 && reliableWindow <= currentWindow + ENET_PEER_FREE_RELIABLE_WINDOWS) + return NULL; + } + + acknowledgement = (ENetAcknowledgement *) enet_malloc (sizeof (ENetAcknowledgement)); + if (acknowledgement == NULL) + return NULL; + + peer -> outgoingDataTotal += sizeof (ENetProtocolAcknowledge); + + acknowledgement -> sentTime = sentTime; + acknowledgement -> command = * command; + + enet_list_insert (enet_list_end (& peer -> acknowledgements), acknowledgement); + + return acknowledgement; +} + +void +enet_peer_setup_outgoing_command (ENetPeer * peer, ENetOutgoingCommand * outgoingCommand) +{ + ENetChannel * channel = & peer -> channels [outgoingCommand -> command.header.channelID]; + + peer -> outgoingDataTotal += enet_protocol_command_size (outgoingCommand -> command.header.command) + outgoingCommand -> fragmentLength; + + if (outgoingCommand -> command.header.channelID == 0xFF) + { + ++ peer -> outgoingReliableSequenceNumber; + + outgoingCommand -> reliableSequenceNumber = peer -> outgoingReliableSequenceNumber; + outgoingCommand -> unreliableSequenceNumber = 0; + } + else + if (outgoingCommand -> command.header.command & ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE) + { + ++ channel -> outgoingReliableSequenceNumber; + channel -> outgoingUnreliableSequenceNumber = 0; + + outgoingCommand -> reliableSequenceNumber = channel -> outgoingReliableSequenceNumber; + outgoingCommand -> unreliableSequenceNumber = 0; + } + else + if (outgoingCommand -> command.header.command & ENET_PROTOCOL_COMMAND_FLAG_UNSEQUENCED) + { + ++ peer -> outgoingUnsequencedGroup; + + outgoingCommand -> reliableSequenceNumber = 0; + outgoingCommand -> unreliableSequenceNumber = 0; + } + else + { + if (outgoingCommand -> fragmentOffset == 0) + ++ channel -> outgoingUnreliableSequenceNumber; + + outgoingCommand -> reliableSequenceNumber = channel -> outgoingReliableSequenceNumber; + outgoingCommand -> unreliableSequenceNumber = channel -> outgoingUnreliableSequenceNumber; + } + + outgoingCommand -> sendAttempts = 0; + outgoingCommand -> sentTime = 0; + outgoingCommand -> roundTripTimeout = 0; + outgoingCommand -> roundTripTimeoutLimit = 0; + outgoingCommand -> command.header.reliableSequenceNumber = ENET_HOST_TO_NET_16 (outgoingCommand -> reliableSequenceNumber); + + switch (outgoingCommand -> command.header.command & ENET_PROTOCOL_COMMAND_MASK) + { + case ENET_PROTOCOL_COMMAND_SEND_UNRELIABLE: + outgoingCommand -> command.sendUnreliable.unreliableSequenceNumber = ENET_HOST_TO_NET_16 (outgoingCommand -> unreliableSequenceNumber); + break; + + case ENET_PROTOCOL_COMMAND_SEND_UNSEQUENCED: + outgoingCommand -> command.sendUnsequenced.unsequencedGroup = ENET_HOST_TO_NET_16 (peer -> outgoingUnsequencedGroup); + break; + + default: + break; + } + + if (outgoingCommand -> command.header.command & ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE) + enet_list_insert (enet_list_end (& peer -> outgoingReliableCommands), outgoingCommand); + else + enet_list_insert (enet_list_end (& peer -> outgoingUnreliableCommands), outgoingCommand); +} + +ENetOutgoingCommand * +enet_peer_queue_outgoing_command (ENetPeer * peer, const ENetProtocol * command, ENetPacket * packet, enet_uint32 offset, enet_uint16 length) +{ + ENetOutgoingCommand * outgoingCommand = (ENetOutgoingCommand *) enet_malloc (sizeof (ENetOutgoingCommand)); + if (outgoingCommand == NULL) + return NULL; + + outgoingCommand -> command = * command; + outgoingCommand -> fragmentOffset = offset; + outgoingCommand -> fragmentLength = length; + outgoingCommand -> packet = packet; + if (packet != NULL) + ++ packet -> referenceCount; + + enet_peer_setup_outgoing_command (peer, outgoingCommand); + + return outgoingCommand; +} + +void +enet_peer_dispatch_incoming_unreliable_commands (ENetPeer * peer, ENetChannel * channel) +{ + ENetListIterator droppedCommand, startCommand, currentCommand; + + for (droppedCommand = startCommand = currentCommand = enet_list_begin (& channel -> incomingUnreliableCommands); + currentCommand != enet_list_end (& channel -> incomingUnreliableCommands); + currentCommand = enet_list_next (currentCommand)) + { + ENetIncomingCommand * incomingCommand = (ENetIncomingCommand *) currentCommand; + + if ((incomingCommand -> command.header.command & ENET_PROTOCOL_COMMAND_MASK) == ENET_PROTOCOL_COMMAND_SEND_UNSEQUENCED) + continue; + + if (incomingCommand -> reliableSequenceNumber == channel -> incomingReliableSequenceNumber) + { + if (incomingCommand -> fragmentsRemaining <= 0) + { + channel -> incomingUnreliableSequenceNumber = incomingCommand -> unreliableSequenceNumber; + continue; + } + + if (startCommand != currentCommand) + { + enet_list_move (enet_list_end (& peer -> dispatchedCommands), startCommand, enet_list_previous (currentCommand)); + + if (! peer -> needsDispatch) + { + enet_list_insert (enet_list_end (& peer -> host -> dispatchQueue), & peer -> dispatchList); + + peer -> needsDispatch = 1; + } + + droppedCommand = currentCommand; + } + else + if (droppedCommand != currentCommand) + droppedCommand = enet_list_previous (currentCommand); + } + else + { + enet_uint16 reliableWindow = incomingCommand -> reliableSequenceNumber / ENET_PEER_RELIABLE_WINDOW_SIZE, + currentWindow = channel -> incomingReliableSequenceNumber / ENET_PEER_RELIABLE_WINDOW_SIZE; + if (incomingCommand -> reliableSequenceNumber < channel -> incomingReliableSequenceNumber) + reliableWindow += ENET_PEER_RELIABLE_WINDOWS; + if (reliableWindow >= currentWindow && reliableWindow < currentWindow + ENET_PEER_FREE_RELIABLE_WINDOWS - 1) + break; + + droppedCommand = enet_list_next (currentCommand); + + if (startCommand != currentCommand) + { + enet_list_move (enet_list_end (& peer -> dispatchedCommands), startCommand, enet_list_previous (currentCommand)); + + if (! peer -> needsDispatch) + { + enet_list_insert (enet_list_end (& peer -> host -> dispatchQueue), & peer -> dispatchList); + + peer -> needsDispatch = 1; + } + } + } + + startCommand = enet_list_next (currentCommand); + } + + if (startCommand != currentCommand) + { + enet_list_move (enet_list_end (& peer -> dispatchedCommands), startCommand, enet_list_previous (currentCommand)); + + if (! peer -> needsDispatch) + { + enet_list_insert (enet_list_end (& peer -> host -> dispatchQueue), & peer -> dispatchList); + + peer -> needsDispatch = 1; + } + + droppedCommand = currentCommand; + } + + enet_peer_remove_incoming_commands (& channel -> incomingUnreliableCommands, enet_list_begin (& channel -> incomingUnreliableCommands), droppedCommand); +} + +void +enet_peer_dispatch_incoming_reliable_commands (ENetPeer * peer, ENetChannel * channel) +{ + ENetListIterator currentCommand; + + for (currentCommand = enet_list_begin (& channel -> incomingReliableCommands); + currentCommand != enet_list_end (& channel -> incomingReliableCommands); + currentCommand = enet_list_next (currentCommand)) + { + ENetIncomingCommand * incomingCommand = (ENetIncomingCommand *) currentCommand; + + if (incomingCommand -> fragmentsRemaining > 0 || + incomingCommand -> reliableSequenceNumber != (enet_uint16) (channel -> incomingReliableSequenceNumber + 1)) + break; + + channel -> incomingReliableSequenceNumber = incomingCommand -> reliableSequenceNumber; + + if (incomingCommand -> fragmentCount > 0) + channel -> incomingReliableSequenceNumber += incomingCommand -> fragmentCount - 1; + } + + if (currentCommand == enet_list_begin (& channel -> incomingReliableCommands)) + return; + + channel -> incomingUnreliableSequenceNumber = 0; + + enet_list_move (enet_list_end (& peer -> dispatchedCommands), enet_list_begin (& channel -> incomingReliableCommands), enet_list_previous (currentCommand)); + + if (! peer -> needsDispatch) + { + enet_list_insert (enet_list_end (& peer -> host -> dispatchQueue), & peer -> dispatchList); + + peer -> needsDispatch = 1; + } + + if (! enet_list_empty (& channel -> incomingUnreliableCommands)) + enet_peer_dispatch_incoming_unreliable_commands (peer, channel); +} + +ENetIncomingCommand * +enet_peer_queue_incoming_command (ENetPeer * peer, const ENetProtocol * command, ENetPacket * packet, enet_uint32 fragmentCount) +{ + static ENetIncomingCommand dummyCommand; + + ENetChannel * channel = & peer -> channels [command -> header.channelID]; + enet_uint32 unreliableSequenceNumber = 0, reliableSequenceNumber = 0; + enet_uint16 reliableWindow, currentWindow; + ENetIncomingCommand * incomingCommand; + ENetListIterator currentCommand; + + if (peer -> state == ENET_PEER_STATE_DISCONNECT_LATER) + goto freePacket; + + if ((command -> header.command & ENET_PROTOCOL_COMMAND_MASK) != ENET_PROTOCOL_COMMAND_SEND_UNSEQUENCED) + { + reliableSequenceNumber = command -> header.reliableSequenceNumber; + reliableWindow = reliableSequenceNumber / ENET_PEER_RELIABLE_WINDOW_SIZE; + currentWindow = channel -> incomingReliableSequenceNumber / ENET_PEER_RELIABLE_WINDOW_SIZE; + + if (reliableSequenceNumber < channel -> incomingReliableSequenceNumber) + reliableWindow += ENET_PEER_RELIABLE_WINDOWS; + + if (reliableWindow < currentWindow || reliableWindow >= currentWindow + ENET_PEER_FREE_RELIABLE_WINDOWS - 1) + goto freePacket; + } + + switch (command -> header.command & ENET_PROTOCOL_COMMAND_MASK) + { + case ENET_PROTOCOL_COMMAND_SEND_FRAGMENT: + case ENET_PROTOCOL_COMMAND_SEND_RELIABLE: + if (reliableSequenceNumber == channel -> incomingReliableSequenceNumber) + goto freePacket; + + for (currentCommand = enet_list_previous (enet_list_end (& channel -> incomingReliableCommands)); + currentCommand != enet_list_end (& channel -> incomingReliableCommands); + currentCommand = enet_list_previous (currentCommand)) + { + incomingCommand = (ENetIncomingCommand *) currentCommand; + + if (reliableSequenceNumber >= channel -> incomingReliableSequenceNumber) + { + if (incomingCommand -> reliableSequenceNumber < channel -> incomingReliableSequenceNumber) + continue; + } + else + if (incomingCommand -> reliableSequenceNumber >= channel -> incomingReliableSequenceNumber) + break; + + if (incomingCommand -> reliableSequenceNumber <= reliableSequenceNumber) + { + if (incomingCommand -> reliableSequenceNumber < reliableSequenceNumber) + break; + + goto freePacket; + } + } + break; + + case ENET_PROTOCOL_COMMAND_SEND_UNRELIABLE: + case ENET_PROTOCOL_COMMAND_SEND_UNRELIABLE_FRAGMENT: + unreliableSequenceNumber = ENET_NET_TO_HOST_16 (command -> sendUnreliable.unreliableSequenceNumber); + + if (reliableSequenceNumber == channel -> incomingReliableSequenceNumber && + unreliableSequenceNumber <= channel -> incomingUnreliableSequenceNumber) + goto freePacket; + + for (currentCommand = enet_list_previous (enet_list_end (& channel -> incomingUnreliableCommands)); + currentCommand != enet_list_end (& channel -> incomingUnreliableCommands); + currentCommand = enet_list_previous (currentCommand)) + { + incomingCommand = (ENetIncomingCommand *) currentCommand; + + if ((command -> header.command & ENET_PROTOCOL_COMMAND_MASK) == ENET_PROTOCOL_COMMAND_SEND_UNSEQUENCED) + continue; + + if (reliableSequenceNumber >= channel -> incomingReliableSequenceNumber) + { + if (incomingCommand -> reliableSequenceNumber < channel -> incomingReliableSequenceNumber) + continue; + } + else + if (incomingCommand -> reliableSequenceNumber >= channel -> incomingReliableSequenceNumber) + break; + + if (incomingCommand -> reliableSequenceNumber < reliableSequenceNumber) + break; + + if (incomingCommand -> reliableSequenceNumber > reliableSequenceNumber) + continue; + + if (incomingCommand -> unreliableSequenceNumber <= unreliableSequenceNumber) + { + if (incomingCommand -> unreliableSequenceNumber < unreliableSequenceNumber) + break; + + goto freePacket; + } + } + break; + + case ENET_PROTOCOL_COMMAND_SEND_UNSEQUENCED: + currentCommand = enet_list_end (& channel -> incomingUnreliableCommands); + break; + + default: + goto freePacket; + } + + incomingCommand = (ENetIncomingCommand *) enet_malloc (sizeof (ENetIncomingCommand)); + if (incomingCommand == NULL) + goto notifyError; + + incomingCommand -> reliableSequenceNumber = command -> header.reliableSequenceNumber; + incomingCommand -> unreliableSequenceNumber = unreliableSequenceNumber & 0xFFFF; + incomingCommand -> command = * command; + incomingCommand -> fragmentCount = fragmentCount; + incomingCommand -> fragmentsRemaining = fragmentCount; + incomingCommand -> packet = packet; + incomingCommand -> fragments = NULL; + + if (fragmentCount > 0) + { + if (fragmentCount <= ENET_PROTOCOL_MAXIMUM_FRAGMENT_COUNT) + incomingCommand -> fragments = (enet_uint32 *) enet_malloc ((fragmentCount + 31) / 32 * sizeof (enet_uint32)); + if (incomingCommand -> fragments == NULL) + { + enet_free (incomingCommand); + + goto notifyError; + } + memset (incomingCommand -> fragments, 0, (fragmentCount + 31) / 32 * sizeof (enet_uint32)); + } + + if (packet != NULL) + ++ packet -> referenceCount; + + enet_list_insert (enet_list_next (currentCommand), incomingCommand); + + switch (command -> header.command & ENET_PROTOCOL_COMMAND_MASK) + { + case ENET_PROTOCOL_COMMAND_SEND_FRAGMENT: + case ENET_PROTOCOL_COMMAND_SEND_RELIABLE: + enet_peer_dispatch_incoming_reliable_commands (peer, channel); + break; + + default: + enet_peer_dispatch_incoming_unreliable_commands (peer, channel); + break; + } + + return incomingCommand; + +freePacket: + if (fragmentCount > 0) + goto notifyError; + + if (packet != NULL && packet -> referenceCount == 0) + enet_packet_destroy (packet); + + return & dummyCommand; + +notifyError: + if (packet != NULL && packet -> referenceCount == 0) + enet_packet_destroy (packet); + + return NULL; +} + +/** @} */ diff --git a/Externals/enet/protocol.c b/Externals/enet/protocol.c new file mode 100644 index 000000000000..3438a5dadb5d --- /dev/null +++ b/Externals/enet/protocol.c @@ -0,0 +1,1925 @@ +/** + @file protocol.c + @brief ENet protocol functions +*/ +#include +#include +#define ENET_BUILDING_LIB 1 +#include "enet/utility.h" +#include "enet/time.h" +#include "enet/enet.h" + +static size_t commandSizes [ENET_PROTOCOL_COMMAND_COUNT] = +{ + 0, + sizeof (ENetProtocolAcknowledge), + sizeof (ENetProtocolConnect), + sizeof (ENetProtocolVerifyConnect), + sizeof (ENetProtocolDisconnect), + sizeof (ENetProtocolPing), + sizeof (ENetProtocolSendReliable), + sizeof (ENetProtocolSendUnreliable), + sizeof (ENetProtocolSendFragment), + sizeof (ENetProtocolSendUnsequenced), + sizeof (ENetProtocolBandwidthLimit), + sizeof (ENetProtocolThrottleConfigure), + sizeof (ENetProtocolSendFragment) +}; + +size_t +enet_protocol_command_size (enet_uint8 commandNumber) +{ + return commandSizes [commandNumber & ENET_PROTOCOL_COMMAND_MASK]; +} + +static void +enet_protocol_change_state (ENetHost * host, ENetPeer * peer, ENetPeerState state) +{ + if (state == ENET_PEER_STATE_CONNECTED || state == ENET_PEER_STATE_DISCONNECT_LATER) + enet_peer_on_connect (peer); + else + enet_peer_on_disconnect (peer); + + peer -> state = state; +} + +static void +enet_protocol_dispatch_state (ENetHost * host, ENetPeer * peer, ENetPeerState state) +{ + enet_protocol_change_state (host, peer, state); + + if (! peer -> needsDispatch) + { + enet_list_insert (enet_list_end (& host -> dispatchQueue), & peer -> dispatchList); + + peer -> needsDispatch = 1; + } +} + +static int +enet_protocol_dispatch_incoming_commands (ENetHost * host, ENetEvent * event) +{ + while (! enet_list_empty (& host -> dispatchQueue)) + { + ENetPeer * peer = (ENetPeer *) enet_list_remove (enet_list_begin (& host -> dispatchQueue)); + + peer -> needsDispatch = 0; + + switch (peer -> state) + { + case ENET_PEER_STATE_CONNECTION_PENDING: + case ENET_PEER_STATE_CONNECTION_SUCCEEDED: + enet_protocol_change_state (host, peer, ENET_PEER_STATE_CONNECTED); + + event -> type = ENET_EVENT_TYPE_CONNECT; + event -> peer = peer; + event -> data = peer -> eventData; + + return 1; + + case ENET_PEER_STATE_ZOMBIE: + host -> recalculateBandwidthLimits = 1; + + event -> type = ENET_EVENT_TYPE_DISCONNECT; + event -> peer = peer; + event -> data = peer -> eventData; + + enet_peer_reset (peer); + + return 1; + + case ENET_PEER_STATE_CONNECTED: + if (enet_list_empty (& peer -> dispatchedCommands)) + continue; + + event -> packet = enet_peer_receive (peer, & event -> channelID); + if (event -> packet == NULL) + continue; + + event -> type = ENET_EVENT_TYPE_RECEIVE; + event -> peer = peer; + + if (! enet_list_empty (& peer -> dispatchedCommands)) + { + peer -> needsDispatch = 1; + + enet_list_insert (enet_list_end (& host -> dispatchQueue), & peer -> dispatchList); + } + + return 1; + + default: + break; + } + } + + return 0; +} + +static void +enet_protocol_notify_connect (ENetHost * host, ENetPeer * peer, ENetEvent * event) +{ + host -> recalculateBandwidthLimits = 1; + + if (event != NULL) + { + enet_protocol_change_state (host, peer, ENET_PEER_STATE_CONNECTED); + + event -> type = ENET_EVENT_TYPE_CONNECT; + event -> peer = peer; + event -> data = peer -> eventData; + } + else + enet_protocol_dispatch_state (host, peer, peer -> state == ENET_PEER_STATE_CONNECTING ? ENET_PEER_STATE_CONNECTION_SUCCEEDED : ENET_PEER_STATE_CONNECTION_PENDING); +} + +static void +enet_protocol_notify_disconnect (ENetHost * host, ENetPeer * peer, ENetEvent * event) +{ + if (peer -> state >= ENET_PEER_STATE_CONNECTION_PENDING) + host -> recalculateBandwidthLimits = 1; + + if (peer -> state != ENET_PEER_STATE_CONNECTING && peer -> state < ENET_PEER_STATE_CONNECTION_SUCCEEDED) + enet_peer_reset (peer); + else + if (event != NULL) + { + event -> type = ENET_EVENT_TYPE_DISCONNECT; + event -> peer = peer; + event -> data = 0; + + enet_peer_reset (peer); + } + else + { + peer -> eventData = 0; + + enet_protocol_dispatch_state (host, peer, ENET_PEER_STATE_ZOMBIE); + } +} + +static void +enet_protocol_remove_sent_unreliable_commands (ENetPeer * peer) +{ + ENetOutgoingCommand * outgoingCommand; + + while (! enet_list_empty (& peer -> sentUnreliableCommands)) + { + outgoingCommand = (ENetOutgoingCommand *) enet_list_front (& peer -> sentUnreliableCommands); + + enet_list_remove (& outgoingCommand -> outgoingCommandList); + + if (outgoingCommand -> packet != NULL) + { + -- outgoingCommand -> packet -> referenceCount; + + if (outgoingCommand -> packet -> referenceCount == 0) + { + outgoingCommand -> packet -> flags |= ENET_PACKET_FLAG_SENT; + + enet_packet_destroy (outgoingCommand -> packet); + } + } + + enet_free (outgoingCommand); + } +} + +static ENetProtocolCommand +enet_protocol_remove_sent_reliable_command (ENetPeer * peer, enet_uint16 reliableSequenceNumber, enet_uint8 channelID) +{ + ENetOutgoingCommand * outgoingCommand = NULL; + ENetListIterator currentCommand; + ENetProtocolCommand commandNumber; + int wasSent = 1; + + for (currentCommand = enet_list_begin (& peer -> sentReliableCommands); + currentCommand != enet_list_end (& peer -> sentReliableCommands); + currentCommand = enet_list_next (currentCommand)) + { + outgoingCommand = (ENetOutgoingCommand *) currentCommand; + + if (outgoingCommand -> reliableSequenceNumber == reliableSequenceNumber && + outgoingCommand -> command.header.channelID == channelID) + break; + } + + if (currentCommand == enet_list_end (& peer -> sentReliableCommands)) + { + for (currentCommand = enet_list_begin (& peer -> outgoingReliableCommands); + currentCommand != enet_list_end (& peer -> outgoingReliableCommands); + currentCommand = enet_list_next (currentCommand)) + { + outgoingCommand = (ENetOutgoingCommand *) currentCommand; + + if (outgoingCommand -> sendAttempts < 1) return ENET_PROTOCOL_COMMAND_NONE; + + if (outgoingCommand -> reliableSequenceNumber == reliableSequenceNumber && + outgoingCommand -> command.header.channelID == channelID) + break; + } + + if (currentCommand == enet_list_end (& peer -> outgoingReliableCommands)) + return ENET_PROTOCOL_COMMAND_NONE; + + wasSent = 0; + } + + if (outgoingCommand == NULL) + return ENET_PROTOCOL_COMMAND_NONE; + + if (channelID < peer -> channelCount) + { + ENetChannel * channel = & peer -> channels [channelID]; + enet_uint16 reliableWindow = reliableSequenceNumber / ENET_PEER_RELIABLE_WINDOW_SIZE; + if (channel -> reliableWindows [reliableWindow] > 0) + { + -- channel -> reliableWindows [reliableWindow]; + if (! channel -> reliableWindows [reliableWindow]) + channel -> usedReliableWindows &= ~ (1 << reliableWindow); + } + } + + commandNumber = (ENetProtocolCommand) (outgoingCommand -> command.header.command & ENET_PROTOCOL_COMMAND_MASK); + + enet_list_remove (& outgoingCommand -> outgoingCommandList); + + if (outgoingCommand -> packet != NULL) + { + if (wasSent) + peer -> reliableDataInTransit -= outgoingCommand -> fragmentLength; + + -- outgoingCommand -> packet -> referenceCount; + + if (outgoingCommand -> packet -> referenceCount == 0) + { + outgoingCommand -> packet -> flags |= ENET_PACKET_FLAG_SENT; + + enet_packet_destroy (outgoingCommand -> packet); + } + } + + enet_free (outgoingCommand); + + if (enet_list_empty (& peer -> sentReliableCommands)) + return commandNumber; + + outgoingCommand = (ENetOutgoingCommand *) enet_list_front (& peer -> sentReliableCommands); + + peer -> nextTimeout = outgoingCommand -> sentTime + outgoingCommand -> roundTripTimeout; + + return commandNumber; +} + +static ENetPeer * +enet_protocol_handle_connect (ENetHost * host, ENetProtocolHeader * header, ENetProtocol * command) +{ + enet_uint8 incomingSessionID, outgoingSessionID; + enet_uint32 mtu, windowSize; + ENetChannel * channel; + size_t channelCount, duplicatePeers = 0; + ENetPeer * currentPeer, * peer = NULL; + ENetProtocol verifyCommand; + + channelCount = ENET_NET_TO_HOST_32 (command -> connect.channelCount); + + if (channelCount < ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT || + channelCount > ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT) + return NULL; + + for (currentPeer = host -> peers; + currentPeer < & host -> peers [host -> peerCount]; + ++ currentPeer) + { + if (currentPeer -> state == ENET_PEER_STATE_DISCONNECTED) + { + if (peer == NULL) + peer = currentPeer; + } + else + if (currentPeer -> address.host == host -> receivedAddress.host) + { + if (currentPeer -> address.port == host -> receivedAddress.port && + currentPeer -> connectID == command -> connect.connectID) + return NULL; + + ++ duplicatePeers; + } + } + + if (peer == NULL || duplicatePeers >= host -> duplicatePeers) + return NULL; + + if (channelCount > host -> channelLimit) + channelCount = host -> channelLimit; + peer -> channels = (ENetChannel *) enet_malloc (channelCount * sizeof (ENetChannel)); + if (peer -> channels == NULL) + return NULL; + peer -> channelCount = channelCount; + peer -> state = ENET_PEER_STATE_ACKNOWLEDGING_CONNECT; + peer -> connectID = command -> connect.connectID; + peer -> address = host -> receivedAddress; + peer -> outgoingPeerID = ENET_NET_TO_HOST_16 (command -> connect.outgoingPeerID); + peer -> incomingBandwidth = ENET_NET_TO_HOST_32 (command -> connect.incomingBandwidth); + peer -> outgoingBandwidth = ENET_NET_TO_HOST_32 (command -> connect.outgoingBandwidth); + peer -> packetThrottleInterval = ENET_NET_TO_HOST_32 (command -> connect.packetThrottleInterval); + peer -> packetThrottleAcceleration = ENET_NET_TO_HOST_32 (command -> connect.packetThrottleAcceleration); + peer -> packetThrottleDeceleration = ENET_NET_TO_HOST_32 (command -> connect.packetThrottleDeceleration); + peer -> eventData = ENET_NET_TO_HOST_32 (command -> connect.data); + + incomingSessionID = command -> connect.incomingSessionID == 0xFF ? peer -> outgoingSessionID : command -> connect.incomingSessionID; + incomingSessionID = (incomingSessionID + 1) & (ENET_PROTOCOL_HEADER_SESSION_MASK >> ENET_PROTOCOL_HEADER_SESSION_SHIFT); + if (incomingSessionID == peer -> outgoingSessionID) + incomingSessionID = (incomingSessionID + 1) & (ENET_PROTOCOL_HEADER_SESSION_MASK >> ENET_PROTOCOL_HEADER_SESSION_SHIFT); + peer -> outgoingSessionID = incomingSessionID; + + outgoingSessionID = command -> connect.outgoingSessionID == 0xFF ? peer -> incomingSessionID : command -> connect.outgoingSessionID; + outgoingSessionID = (outgoingSessionID + 1) & (ENET_PROTOCOL_HEADER_SESSION_MASK >> ENET_PROTOCOL_HEADER_SESSION_SHIFT); + if (outgoingSessionID == peer -> incomingSessionID) + outgoingSessionID = (outgoingSessionID + 1) & (ENET_PROTOCOL_HEADER_SESSION_MASK >> ENET_PROTOCOL_HEADER_SESSION_SHIFT); + peer -> incomingSessionID = outgoingSessionID; + + for (channel = peer -> channels; + channel < & peer -> channels [channelCount]; + ++ channel) + { + channel -> outgoingReliableSequenceNumber = 0; + channel -> outgoingUnreliableSequenceNumber = 0; + channel -> incomingReliableSequenceNumber = 0; + channel -> incomingUnreliableSequenceNumber = 0; + + enet_list_clear (& channel -> incomingReliableCommands); + enet_list_clear (& channel -> incomingUnreliableCommands); + + channel -> usedReliableWindows = 0; + memset (channel -> reliableWindows, 0, sizeof (channel -> reliableWindows)); + } + + mtu = ENET_NET_TO_HOST_32 (command -> connect.mtu); + + if (mtu < ENET_PROTOCOL_MINIMUM_MTU) + mtu = ENET_PROTOCOL_MINIMUM_MTU; + else + if (mtu > ENET_PROTOCOL_MAXIMUM_MTU) + mtu = ENET_PROTOCOL_MAXIMUM_MTU; + + peer -> mtu = mtu; + + if (host -> outgoingBandwidth == 0 && + peer -> incomingBandwidth == 0) + peer -> windowSize = ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE; + else + if (host -> outgoingBandwidth == 0 || + peer -> incomingBandwidth == 0) + peer -> windowSize = (ENET_MAX (host -> outgoingBandwidth, peer -> incomingBandwidth) / + ENET_PEER_WINDOW_SIZE_SCALE) * + ENET_PROTOCOL_MINIMUM_WINDOW_SIZE; + else + peer -> windowSize = (ENET_MIN (host -> outgoingBandwidth, peer -> incomingBandwidth) / + ENET_PEER_WINDOW_SIZE_SCALE) * + ENET_PROTOCOL_MINIMUM_WINDOW_SIZE; + + if (peer -> windowSize < ENET_PROTOCOL_MINIMUM_WINDOW_SIZE) + peer -> windowSize = ENET_PROTOCOL_MINIMUM_WINDOW_SIZE; + else + if (peer -> windowSize > ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE) + peer -> windowSize = ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE; + + if (host -> incomingBandwidth == 0) + windowSize = ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE; + else + windowSize = (host -> incomingBandwidth / ENET_PEER_WINDOW_SIZE_SCALE) * + ENET_PROTOCOL_MINIMUM_WINDOW_SIZE; + + if (windowSize > ENET_NET_TO_HOST_32 (command -> connect.windowSize)) + windowSize = ENET_NET_TO_HOST_32 (command -> connect.windowSize); + + if (windowSize < ENET_PROTOCOL_MINIMUM_WINDOW_SIZE) + windowSize = ENET_PROTOCOL_MINIMUM_WINDOW_SIZE; + else + if (windowSize > ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE) + windowSize = ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE; + + verifyCommand.header.command = ENET_PROTOCOL_COMMAND_VERIFY_CONNECT | ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE; + verifyCommand.header.channelID = 0xFF; + verifyCommand.verifyConnect.outgoingPeerID = ENET_HOST_TO_NET_16 (peer -> incomingPeerID); + verifyCommand.verifyConnect.incomingSessionID = incomingSessionID; + verifyCommand.verifyConnect.outgoingSessionID = outgoingSessionID; + verifyCommand.verifyConnect.mtu = ENET_HOST_TO_NET_32 (peer -> mtu); + verifyCommand.verifyConnect.windowSize = ENET_HOST_TO_NET_32 (windowSize); + verifyCommand.verifyConnect.channelCount = ENET_HOST_TO_NET_32 (channelCount); + verifyCommand.verifyConnect.incomingBandwidth = ENET_HOST_TO_NET_32 (host -> incomingBandwidth); + verifyCommand.verifyConnect.outgoingBandwidth = ENET_HOST_TO_NET_32 (host -> outgoingBandwidth); + verifyCommand.verifyConnect.packetThrottleInterval = ENET_HOST_TO_NET_32 (peer -> packetThrottleInterval); + verifyCommand.verifyConnect.packetThrottleAcceleration = ENET_HOST_TO_NET_32 (peer -> packetThrottleAcceleration); + verifyCommand.verifyConnect.packetThrottleDeceleration = ENET_HOST_TO_NET_32 (peer -> packetThrottleDeceleration); + verifyCommand.verifyConnect.connectID = peer -> connectID; + + enet_peer_queue_outgoing_command (peer, & verifyCommand, NULL, 0, 0); + + return peer; +} + +static int +enet_protocol_handle_send_reliable (ENetHost * host, ENetPeer * peer, const ENetProtocol * command, enet_uint8 ** currentData) +{ + ENetPacket * packet; + size_t dataLength; + + if (command -> header.channelID >= peer -> channelCount || + (peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER)) + return -1; + + dataLength = ENET_NET_TO_HOST_16 (command -> sendReliable.dataLength); + * currentData += dataLength; + if (dataLength > ENET_PROTOCOL_MAXIMUM_PACKET_SIZE || + * currentData < host -> receivedData || + * currentData > & host -> receivedData [host -> receivedDataLength]) + return -1; + + packet = enet_packet_create ((const enet_uint8 *) command + sizeof (ENetProtocolSendReliable), + dataLength, + ENET_PACKET_FLAG_RELIABLE); + if (packet == NULL || + enet_peer_queue_incoming_command (peer, command, packet, 0) == NULL) + return -1; + + return 0; +} + +static int +enet_protocol_handle_send_unsequenced (ENetHost * host, ENetPeer * peer, const ENetProtocol * command, enet_uint8 ** currentData) +{ + ENetPacket * packet; + enet_uint32 unsequencedGroup, index; + size_t dataLength; + + if (command -> header.channelID >= peer -> channelCount || + (peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER)) + return -1; + + dataLength = ENET_NET_TO_HOST_16 (command -> sendUnsequenced.dataLength); + * currentData += dataLength; + if (dataLength > ENET_PROTOCOL_MAXIMUM_PACKET_SIZE || + * currentData < host -> receivedData || + * currentData > & host -> receivedData [host -> receivedDataLength]) + return -1; + + unsequencedGroup = ENET_NET_TO_HOST_16 (command -> sendUnsequenced.unsequencedGroup); + index = unsequencedGroup % ENET_PEER_UNSEQUENCED_WINDOW_SIZE; + + if (unsequencedGroup < peer -> incomingUnsequencedGroup) + unsequencedGroup += 0x10000; + + if (unsequencedGroup >= (enet_uint32) peer -> incomingUnsequencedGroup + ENET_PEER_FREE_UNSEQUENCED_WINDOWS * ENET_PEER_UNSEQUENCED_WINDOW_SIZE) + return 0; + + unsequencedGroup &= 0xFFFF; + + if (unsequencedGroup - index != peer -> incomingUnsequencedGroup) + { + peer -> incomingUnsequencedGroup = unsequencedGroup - index; + + memset (peer -> unsequencedWindow, 0, sizeof (peer -> unsequencedWindow)); + } + else + if (peer -> unsequencedWindow [index / 32] & (1 << (index % 32))) + return 0; + + packet = enet_packet_create ((const enet_uint8 *) command + sizeof (ENetProtocolSendUnsequenced), + dataLength, + ENET_PACKET_FLAG_UNSEQUENCED); + if (packet == NULL || + enet_peer_queue_incoming_command (peer, command, packet, 0) == NULL) + return -1; + + peer -> unsequencedWindow [index / 32] |= 1 << (index % 32); + + return 0; +} + +static int +enet_protocol_handle_send_unreliable (ENetHost * host, ENetPeer * peer, const ENetProtocol * command, enet_uint8 ** currentData) +{ + ENetPacket * packet; + size_t dataLength; + + if (command -> header.channelID >= peer -> channelCount || + (peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER)) + return -1; + + dataLength = ENET_NET_TO_HOST_16 (command -> sendUnreliable.dataLength); + * currentData += dataLength; + if (dataLength > ENET_PROTOCOL_MAXIMUM_PACKET_SIZE || + * currentData < host -> receivedData || + * currentData > & host -> receivedData [host -> receivedDataLength]) + return -1; + + packet = enet_packet_create ((const enet_uint8 *) command + sizeof (ENetProtocolSendUnreliable), + dataLength, + 0); + if (packet == NULL || + enet_peer_queue_incoming_command (peer, command, packet, 0) == NULL) + return -1; + + return 0; +} + +static int +enet_protocol_handle_send_fragment (ENetHost * host, ENetPeer * peer, const ENetProtocol * command, enet_uint8 ** currentData) +{ + enet_uint32 fragmentNumber, + fragmentCount, + fragmentOffset, + fragmentLength, + startSequenceNumber, + totalLength; + ENetChannel * channel; + enet_uint16 startWindow, currentWindow; + ENetListIterator currentCommand; + ENetIncomingCommand * startCommand = NULL; + + if (command -> header.channelID >= peer -> channelCount || + (peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER)) + return -1; + + fragmentLength = ENET_NET_TO_HOST_16 (command -> sendFragment.dataLength); + * currentData += fragmentLength; + if (fragmentLength > ENET_PROTOCOL_MAXIMUM_PACKET_SIZE || + * currentData < host -> receivedData || + * currentData > & host -> receivedData [host -> receivedDataLength]) + return -1; + + channel = & peer -> channels [command -> header.channelID]; + startSequenceNumber = ENET_NET_TO_HOST_16 (command -> sendFragment.startSequenceNumber); + startWindow = startSequenceNumber / ENET_PEER_RELIABLE_WINDOW_SIZE; + currentWindow = channel -> incomingReliableSequenceNumber / ENET_PEER_RELIABLE_WINDOW_SIZE; + + if (startSequenceNumber < channel -> incomingReliableSequenceNumber) + startWindow += ENET_PEER_RELIABLE_WINDOWS; + + if (startWindow < currentWindow || startWindow >= currentWindow + ENET_PEER_FREE_RELIABLE_WINDOWS - 1) + return 0; + + fragmentNumber = ENET_NET_TO_HOST_32 (command -> sendFragment.fragmentNumber); + fragmentCount = ENET_NET_TO_HOST_32 (command -> sendFragment.fragmentCount); + fragmentOffset = ENET_NET_TO_HOST_32 (command -> sendFragment.fragmentOffset); + totalLength = ENET_NET_TO_HOST_32 (command -> sendFragment.totalLength); + + if (fragmentCount > ENET_PROTOCOL_MAXIMUM_FRAGMENT_COUNT || + fragmentNumber >= fragmentCount || + totalLength > ENET_PROTOCOL_MAXIMUM_PACKET_SIZE || + fragmentOffset >= totalLength || + fragmentLength > totalLength - fragmentOffset) + return -1; + + for (currentCommand = enet_list_previous (enet_list_end (& channel -> incomingReliableCommands)); + currentCommand != enet_list_end (& channel -> incomingReliableCommands); + currentCommand = enet_list_previous (currentCommand)) + { + ENetIncomingCommand * incomingCommand = (ENetIncomingCommand *) currentCommand; + + if (startSequenceNumber >= channel -> incomingReliableSequenceNumber) + { + if (incomingCommand -> reliableSequenceNumber < channel -> incomingReliableSequenceNumber) + continue; + } + else + if (incomingCommand -> reliableSequenceNumber >= channel -> incomingReliableSequenceNumber) + break; + + if (incomingCommand -> reliableSequenceNumber <= startSequenceNumber) + { + if (incomingCommand -> reliableSequenceNumber < startSequenceNumber) + break; + + if ((incomingCommand -> command.header.command & ENET_PROTOCOL_COMMAND_MASK) != ENET_PROTOCOL_COMMAND_SEND_FRAGMENT || + totalLength != incomingCommand -> packet -> dataLength || + fragmentCount != incomingCommand -> fragmentCount) + return -1; + + startCommand = incomingCommand; + break; + } + } + + if (startCommand == NULL) + { + ENetProtocol hostCommand = * command; + ENetPacket * packet = enet_packet_create (NULL, totalLength, ENET_PACKET_FLAG_RELIABLE); + if (packet == NULL) + return -1; + + hostCommand.header.reliableSequenceNumber = startSequenceNumber; + + startCommand = enet_peer_queue_incoming_command (peer, & hostCommand, packet, fragmentCount); + if (startCommand == NULL) + return -1; + } + + if ((startCommand -> fragments [fragmentNumber / 32] & (1 << (fragmentNumber % 32))) == 0) + { + -- startCommand -> fragmentsRemaining; + + startCommand -> fragments [fragmentNumber / 32] |= (1 << (fragmentNumber % 32)); + + if (fragmentOffset + fragmentLength > startCommand -> packet -> dataLength) + fragmentLength = startCommand -> packet -> dataLength - fragmentOffset; + + memcpy (startCommand -> packet -> data + fragmentOffset, + (enet_uint8 *) command + sizeof (ENetProtocolSendFragment), + fragmentLength); + + if (startCommand -> fragmentsRemaining <= 0) + enet_peer_dispatch_incoming_reliable_commands (peer, channel); + } + + return 0; +} + +static int +enet_protocol_handle_send_unreliable_fragment (ENetHost * host, ENetPeer * peer, const ENetProtocol * command, enet_uint8 ** currentData) +{ + enet_uint32 fragmentNumber, + fragmentCount, + fragmentOffset, + fragmentLength, + reliableSequenceNumber, + startSequenceNumber, + totalLength; + enet_uint16 reliableWindow, currentWindow; + ENetChannel * channel; + ENetListIterator currentCommand; + ENetIncomingCommand * startCommand = NULL; + + if (command -> header.channelID >= peer -> channelCount || + (peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER)) + return -1; + + fragmentLength = ENET_NET_TO_HOST_16 (command -> sendFragment.dataLength); + * currentData += fragmentLength; + if (fragmentLength > ENET_PROTOCOL_MAXIMUM_PACKET_SIZE || + * currentData < host -> receivedData || + * currentData > & host -> receivedData [host -> receivedDataLength]) + return -1; + + channel = & peer -> channels [command -> header.channelID]; + reliableSequenceNumber = command -> header.reliableSequenceNumber; + startSequenceNumber = ENET_NET_TO_HOST_16 (command -> sendFragment.startSequenceNumber); + + reliableWindow = reliableSequenceNumber / ENET_PEER_RELIABLE_WINDOW_SIZE; + currentWindow = channel -> incomingReliableSequenceNumber / ENET_PEER_RELIABLE_WINDOW_SIZE; + + if (reliableSequenceNumber < channel -> incomingReliableSequenceNumber) + reliableWindow += ENET_PEER_RELIABLE_WINDOWS; + + if (reliableWindow < currentWindow || reliableWindow >= currentWindow + ENET_PEER_FREE_RELIABLE_WINDOWS - 1) + return 0; + + if (reliableSequenceNumber == channel -> incomingReliableSequenceNumber && + startSequenceNumber <= channel -> incomingUnreliableSequenceNumber) + return 0; + + fragmentNumber = ENET_NET_TO_HOST_32 (command -> sendFragment.fragmentNumber); + fragmentCount = ENET_NET_TO_HOST_32 (command -> sendFragment.fragmentCount); + fragmentOffset = ENET_NET_TO_HOST_32 (command -> sendFragment.fragmentOffset); + totalLength = ENET_NET_TO_HOST_32 (command -> sendFragment.totalLength); + + if (fragmentCount > ENET_PROTOCOL_MAXIMUM_FRAGMENT_COUNT || + fragmentNumber >= fragmentCount || + totalLength > ENET_PROTOCOL_MAXIMUM_PACKET_SIZE || + fragmentOffset >= totalLength || + fragmentLength > totalLength - fragmentOffset) + return -1; + + for (currentCommand = enet_list_previous (enet_list_end (& channel -> incomingUnreliableCommands)); + currentCommand != enet_list_end (& channel -> incomingUnreliableCommands); + currentCommand = enet_list_previous (currentCommand)) + { + ENetIncomingCommand * incomingCommand = (ENetIncomingCommand *) currentCommand; + + if (reliableSequenceNumber >= channel -> incomingReliableSequenceNumber) + { + if (incomingCommand -> reliableSequenceNumber < channel -> incomingReliableSequenceNumber) + continue; + } + else + if (incomingCommand -> reliableSequenceNumber >= channel -> incomingReliableSequenceNumber) + break; + + if (incomingCommand -> reliableSequenceNumber < reliableSequenceNumber) + break; + + if (incomingCommand -> reliableSequenceNumber > reliableSequenceNumber) + continue; + + if (incomingCommand -> unreliableSequenceNumber <= startSequenceNumber) + { + if (incomingCommand -> unreliableSequenceNumber < startSequenceNumber) + break; + + if ((incomingCommand -> command.header.command & ENET_PROTOCOL_COMMAND_MASK) != ENET_PROTOCOL_COMMAND_SEND_UNRELIABLE_FRAGMENT || + totalLength != incomingCommand -> packet -> dataLength || + fragmentCount != incomingCommand -> fragmentCount) + return -1; + + startCommand = incomingCommand; + break; + } + } + + if (startCommand == NULL) + { + ENetPacket * packet = enet_packet_create (NULL, totalLength, ENET_PACKET_FLAG_UNRELIABLE_FRAGMENT); + if (packet == NULL) + return -1; + + startCommand = enet_peer_queue_incoming_command (peer, command, packet, fragmentCount); + if (startCommand == NULL) + return -1; + } + + if ((startCommand -> fragments [fragmentNumber / 32] & (1 << (fragmentNumber % 32))) == 0) + { + -- startCommand -> fragmentsRemaining; + + startCommand -> fragments [fragmentNumber / 32] |= (1 << (fragmentNumber % 32)); + + if (fragmentOffset + fragmentLength > startCommand -> packet -> dataLength) + fragmentLength = startCommand -> packet -> dataLength - fragmentOffset; + + memcpy (startCommand -> packet -> data + fragmentOffset, + (enet_uint8 *) command + sizeof (ENetProtocolSendFragment), + fragmentLength); + + if (startCommand -> fragmentsRemaining <= 0) + enet_peer_dispatch_incoming_unreliable_commands (peer, channel); + } + + return 0; +} + +static int +enet_protocol_handle_ping (ENetHost * host, ENetPeer * peer, const ENetProtocol * command) +{ + if (peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER) + return -1; + + return 0; +} + +static int +enet_protocol_handle_bandwidth_limit (ENetHost * host, ENetPeer * peer, const ENetProtocol * command) +{ + if (peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER) + return -1; + + if (peer -> incomingBandwidth != 0) + -- host -> bandwidthLimitedPeers; + + peer -> incomingBandwidth = ENET_NET_TO_HOST_32 (command -> bandwidthLimit.incomingBandwidth); + peer -> outgoingBandwidth = ENET_NET_TO_HOST_32 (command -> bandwidthLimit.outgoingBandwidth); + + if (peer -> incomingBandwidth != 0) + ++ host -> bandwidthLimitedPeers; + + if (peer -> incomingBandwidth == 0 && host -> outgoingBandwidth == 0) + peer -> windowSize = ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE; + else + peer -> windowSize = (ENET_MIN (peer -> incomingBandwidth, host -> outgoingBandwidth) / + ENET_PEER_WINDOW_SIZE_SCALE) * ENET_PROTOCOL_MINIMUM_WINDOW_SIZE; + + if (peer -> windowSize < ENET_PROTOCOL_MINIMUM_WINDOW_SIZE) + peer -> windowSize = ENET_PROTOCOL_MINIMUM_WINDOW_SIZE; + else + if (peer -> windowSize > ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE) + peer -> windowSize = ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE; + + return 0; +} + +static int +enet_protocol_handle_throttle_configure (ENetHost * host, ENetPeer * peer, const ENetProtocol * command) +{ + if (peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER) + return -1; + + peer -> packetThrottleInterval = ENET_NET_TO_HOST_32 (command -> throttleConfigure.packetThrottleInterval); + peer -> packetThrottleAcceleration = ENET_NET_TO_HOST_32 (command -> throttleConfigure.packetThrottleAcceleration); + peer -> packetThrottleDeceleration = ENET_NET_TO_HOST_32 (command -> throttleConfigure.packetThrottleDeceleration); + + return 0; +} + +static int +enet_protocol_handle_disconnect (ENetHost * host, ENetPeer * peer, const ENetProtocol * command) +{ + if (peer -> state == ENET_PEER_STATE_DISCONNECTED || peer -> state == ENET_PEER_STATE_ZOMBIE || peer -> state == ENET_PEER_STATE_ACKNOWLEDGING_DISCONNECT) + return 0; + + enet_peer_reset_queues (peer); + + if (peer -> state == ENET_PEER_STATE_CONNECTION_SUCCEEDED || peer -> state == ENET_PEER_STATE_DISCONNECTING) + enet_protocol_dispatch_state (host, peer, ENET_PEER_STATE_ZOMBIE); + else + if (peer -> state != ENET_PEER_STATE_CONNECTED && peer -> state != ENET_PEER_STATE_DISCONNECT_LATER) + { + if (peer -> state == ENET_PEER_STATE_CONNECTION_PENDING) host -> recalculateBandwidthLimits = 1; + + enet_peer_reset (peer); + } + else + if (command -> header.command & ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE) + enet_protocol_change_state (host, peer, ENET_PEER_STATE_ACKNOWLEDGING_DISCONNECT); + else + enet_protocol_dispatch_state (host, peer, ENET_PEER_STATE_ZOMBIE); + + if (peer -> state != ENET_PEER_STATE_DISCONNECTED) + peer -> eventData = ENET_NET_TO_HOST_32 (command -> disconnect.data); + + return 0; +} + +static int +enet_protocol_handle_acknowledge (ENetHost * host, ENetEvent * event, ENetPeer * peer, const ENetProtocol * command) +{ + enet_uint32 roundTripTime, + receivedSentTime, + receivedReliableSequenceNumber; + ENetProtocolCommand commandNumber; + + if (peer -> state == ENET_PEER_STATE_DISCONNECTED || peer -> state == ENET_PEER_STATE_ZOMBIE) + return 0; + + receivedSentTime = ENET_NET_TO_HOST_16 (command -> acknowledge.receivedSentTime); + receivedSentTime |= host -> serviceTime & 0xFFFF0000; + if ((receivedSentTime & 0x8000) > (host -> serviceTime & 0x8000)) + receivedSentTime -= 0x10000; + + if (ENET_TIME_LESS (host -> serviceTime, receivedSentTime)) + return 0; + + peer -> lastReceiveTime = host -> serviceTime; + peer -> earliestTimeout = 0; + + roundTripTime = ENET_TIME_DIFFERENCE (host -> serviceTime, receivedSentTime); + + enet_peer_throttle (peer, roundTripTime); + + peer -> roundTripTimeVariance -= peer -> roundTripTimeVariance / 4; + + if (roundTripTime >= peer -> roundTripTime) + { + peer -> roundTripTime += (roundTripTime - peer -> roundTripTime) / 8; + peer -> roundTripTimeVariance += (roundTripTime - peer -> roundTripTime) / 4; + } + else + { + peer -> roundTripTime -= (peer -> roundTripTime - roundTripTime) / 8; + peer -> roundTripTimeVariance += (peer -> roundTripTime - roundTripTime) / 4; + } + + if (peer -> roundTripTime < peer -> lowestRoundTripTime) + peer -> lowestRoundTripTime = peer -> roundTripTime; + + if (peer -> roundTripTimeVariance > peer -> highestRoundTripTimeVariance) + peer -> highestRoundTripTimeVariance = peer -> roundTripTimeVariance; + + if (peer -> packetThrottleEpoch == 0 || + ENET_TIME_DIFFERENCE (host -> serviceTime, peer -> packetThrottleEpoch) >= peer -> packetThrottleInterval) + { + peer -> lastRoundTripTime = peer -> lowestRoundTripTime; + peer -> lastRoundTripTimeVariance = peer -> highestRoundTripTimeVariance; + peer -> lowestRoundTripTime = peer -> roundTripTime; + peer -> highestRoundTripTimeVariance = peer -> roundTripTimeVariance; + peer -> packetThrottleEpoch = host -> serviceTime; + } + + receivedReliableSequenceNumber = ENET_NET_TO_HOST_16 (command -> acknowledge.receivedReliableSequenceNumber); + + commandNumber = enet_protocol_remove_sent_reliable_command (peer, receivedReliableSequenceNumber, command -> header.channelID); + + switch (peer -> state) + { + case ENET_PEER_STATE_ACKNOWLEDGING_CONNECT: + if (commandNumber != ENET_PROTOCOL_COMMAND_VERIFY_CONNECT) + return -1; + + enet_protocol_notify_connect (host, peer, event); + break; + + case ENET_PEER_STATE_DISCONNECTING: + if (commandNumber != ENET_PROTOCOL_COMMAND_DISCONNECT) + return -1; + + enet_protocol_notify_disconnect (host, peer, event); + break; + + case ENET_PEER_STATE_DISCONNECT_LATER: + if (enet_list_empty (& peer -> outgoingReliableCommands) && + enet_list_empty (& peer -> outgoingUnreliableCommands) && + enet_list_empty (& peer -> sentReliableCommands)) + enet_peer_disconnect (peer, peer -> eventData); + break; + + default: + break; + } + + return 0; +} + +static int +enet_protocol_handle_verify_connect (ENetHost * host, ENetEvent * event, ENetPeer * peer, const ENetProtocol * command) +{ + enet_uint32 mtu, windowSize; + size_t channelCount; + + if (peer -> state != ENET_PEER_STATE_CONNECTING) + return 0; + + channelCount = ENET_NET_TO_HOST_32 (command -> verifyConnect.channelCount); + + if (channelCount < ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT || channelCount > ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT || + ENET_NET_TO_HOST_32 (command -> verifyConnect.packetThrottleInterval) != peer -> packetThrottleInterval || + ENET_NET_TO_HOST_32 (command -> verifyConnect.packetThrottleAcceleration) != peer -> packetThrottleAcceleration || + ENET_NET_TO_HOST_32 (command -> verifyConnect.packetThrottleDeceleration) != peer -> packetThrottleDeceleration || + command -> verifyConnect.connectID != peer -> connectID) + { + peer -> eventData = 0; + + enet_protocol_dispatch_state (host, peer, ENET_PEER_STATE_ZOMBIE); + + return -1; + } + + enet_protocol_remove_sent_reliable_command (peer, 1, 0xFF); + + if (channelCount < peer -> channelCount) + peer -> channelCount = channelCount; + + peer -> outgoingPeerID = ENET_NET_TO_HOST_16 (command -> verifyConnect.outgoingPeerID); + peer -> incomingSessionID = command -> verifyConnect.incomingSessionID; + peer -> outgoingSessionID = command -> verifyConnect.outgoingSessionID; + + mtu = ENET_NET_TO_HOST_32 (command -> verifyConnect.mtu); + + if (mtu < ENET_PROTOCOL_MINIMUM_MTU) + mtu = ENET_PROTOCOL_MINIMUM_MTU; + else + if (mtu > ENET_PROTOCOL_MAXIMUM_MTU) + mtu = ENET_PROTOCOL_MAXIMUM_MTU; + + if (mtu < peer -> mtu) + peer -> mtu = mtu; + + windowSize = ENET_NET_TO_HOST_32 (command -> verifyConnect.windowSize); + + if (windowSize < ENET_PROTOCOL_MINIMUM_WINDOW_SIZE) + windowSize = ENET_PROTOCOL_MINIMUM_WINDOW_SIZE; + + if (windowSize > ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE) + windowSize = ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE; + + if (windowSize < peer -> windowSize) + peer -> windowSize = windowSize; + + peer -> incomingBandwidth = ENET_NET_TO_HOST_32 (command -> verifyConnect.incomingBandwidth); + peer -> outgoingBandwidth = ENET_NET_TO_HOST_32 (command -> verifyConnect.outgoingBandwidth); + + enet_protocol_notify_connect (host, peer, event); + return 0; +} + +static int +enet_protocol_handle_incoming_commands (ENetHost * host, ENetEvent * event) +{ + ENetProtocolHeader * header; + ENetProtocol * command; + ENetPeer * peer; + enet_uint8 * currentData; + size_t headerSize; + enet_uint16 peerID, flags; + enet_uint8 sessionID; + + if (host -> receivedDataLength < (size_t) & ((ENetProtocolHeader *) 0) -> sentTime) + return 0; + + header = (ENetProtocolHeader *) host -> receivedData; + + peerID = ENET_NET_TO_HOST_16 (header -> peerID); + sessionID = (peerID & ENET_PROTOCOL_HEADER_SESSION_MASK) >> ENET_PROTOCOL_HEADER_SESSION_SHIFT; + flags = peerID & ENET_PROTOCOL_HEADER_FLAG_MASK; + peerID &= ~ (ENET_PROTOCOL_HEADER_FLAG_MASK | ENET_PROTOCOL_HEADER_SESSION_MASK); + + headerSize = (flags & ENET_PROTOCOL_HEADER_FLAG_SENT_TIME ? sizeof (ENetProtocolHeader) : (size_t) & ((ENetProtocolHeader *) 0) -> sentTime); + if (host -> checksum != NULL) + headerSize += sizeof (enet_uint32); + + if (peerID == ENET_PROTOCOL_MAXIMUM_PEER_ID) + peer = NULL; + else + if (peerID >= host -> peerCount) + return 0; + else + { + peer = & host -> peers [peerID]; + + if (peer -> state == ENET_PEER_STATE_DISCONNECTED || + peer -> state == ENET_PEER_STATE_ZOMBIE || + ((host -> receivedAddress.host != peer -> address.host || + host -> receivedAddress.port != peer -> address.port) && + peer -> address.host != ENET_HOST_BROADCAST) || + (peer -> outgoingPeerID < ENET_PROTOCOL_MAXIMUM_PEER_ID && + sessionID != peer -> incomingSessionID)) + return 0; + } + + if (flags & ENET_PROTOCOL_HEADER_FLAG_COMPRESSED) + { + size_t originalSize; + if (host -> compressor.context == NULL || host -> compressor.decompress == NULL) + return 0; + + originalSize = host -> compressor.decompress (host -> compressor.context, + host -> receivedData + headerSize, + host -> receivedDataLength - headerSize, + host -> packetData [1] + headerSize, + sizeof (host -> packetData [1]) - headerSize); + if (originalSize <= 0 || originalSize > sizeof (host -> packetData [1]) - headerSize) + return 0; + + memcpy (host -> packetData [1], header, headerSize); + host -> receivedData = host -> packetData [1]; + host -> receivedDataLength = headerSize + originalSize; + } + + if (host -> checksum != NULL) + { + enet_uint32 * checksum = (enet_uint32 *) & host -> receivedData [headerSize - sizeof (enet_uint32)], + desiredChecksum = * checksum; + ENetBuffer buffer; + + * checksum = peer != NULL ? peer -> connectID : 0; + + buffer.data = host -> receivedData; + buffer.dataLength = host -> receivedDataLength; + + if (host -> checksum (& buffer, 1) != desiredChecksum) + return 0; + } + + if (peer != NULL) + { + peer -> address.host = host -> receivedAddress.host; + peer -> address.port = host -> receivedAddress.port; + peer -> incomingDataTotal += host -> receivedDataLength; + } + + currentData = host -> receivedData + headerSize; + + while (currentData < & host -> receivedData [host -> receivedDataLength]) + { + enet_uint8 commandNumber; + size_t commandSize; + + command = (ENetProtocol *) currentData; + + if (currentData + sizeof (ENetProtocolCommandHeader) > & host -> receivedData [host -> receivedDataLength]) + break; + + commandNumber = command -> header.command & ENET_PROTOCOL_COMMAND_MASK; + if (commandNumber >= ENET_PROTOCOL_COMMAND_COUNT) + break; + + commandSize = commandSizes [commandNumber]; + if (commandSize == 0 || currentData + commandSize > & host -> receivedData [host -> receivedDataLength]) + break; + + currentData += commandSize; + + if (peer == NULL && commandNumber != ENET_PROTOCOL_COMMAND_CONNECT) + break; + + command -> header.reliableSequenceNumber = ENET_NET_TO_HOST_16 (command -> header.reliableSequenceNumber); + + switch (commandNumber) + { + case ENET_PROTOCOL_COMMAND_ACKNOWLEDGE: + if (enet_protocol_handle_acknowledge (host, event, peer, command)) + goto commandError; + break; + + case ENET_PROTOCOL_COMMAND_CONNECT: + if (peer != NULL) + goto commandError; + peer = enet_protocol_handle_connect (host, header, command); + if (peer == NULL) + goto commandError; + break; + + case ENET_PROTOCOL_COMMAND_VERIFY_CONNECT: + if (enet_protocol_handle_verify_connect (host, event, peer, command)) + goto commandError; + break; + + case ENET_PROTOCOL_COMMAND_DISCONNECT: + if (enet_protocol_handle_disconnect (host, peer, command)) + goto commandError; + break; + + case ENET_PROTOCOL_COMMAND_PING: + if (enet_protocol_handle_ping (host, peer, command)) + goto commandError; + break; + + case ENET_PROTOCOL_COMMAND_SEND_RELIABLE: + if (enet_protocol_handle_send_reliable (host, peer, command, & currentData)) + goto commandError; + break; + + case ENET_PROTOCOL_COMMAND_SEND_UNRELIABLE: + if (enet_protocol_handle_send_unreliable (host, peer, command, & currentData)) + goto commandError; + break; + + case ENET_PROTOCOL_COMMAND_SEND_UNSEQUENCED: + if (enet_protocol_handle_send_unsequenced (host, peer, command, & currentData)) + goto commandError; + break; + + case ENET_PROTOCOL_COMMAND_SEND_FRAGMENT: + if (enet_protocol_handle_send_fragment (host, peer, command, & currentData)) + goto commandError; + break; + + case ENET_PROTOCOL_COMMAND_BANDWIDTH_LIMIT: + if (enet_protocol_handle_bandwidth_limit (host, peer, command)) + goto commandError; + break; + + case ENET_PROTOCOL_COMMAND_THROTTLE_CONFIGURE: + if (enet_protocol_handle_throttle_configure (host, peer, command)) + goto commandError; + break; + + case ENET_PROTOCOL_COMMAND_SEND_UNRELIABLE_FRAGMENT: + if (enet_protocol_handle_send_unreliable_fragment (host, peer, command, & currentData)) + goto commandError; + break; + + default: + goto commandError; + } + + if (peer != NULL && + (command -> header.command & ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE) != 0) + { + enet_uint16 sentTime; + + if (! (flags & ENET_PROTOCOL_HEADER_FLAG_SENT_TIME)) + break; + + sentTime = ENET_NET_TO_HOST_16 (header -> sentTime); + + switch (peer -> state) + { + case ENET_PEER_STATE_DISCONNECTING: + case ENET_PEER_STATE_ACKNOWLEDGING_CONNECT: + case ENET_PEER_STATE_DISCONNECTED: + case ENET_PEER_STATE_ZOMBIE: + break; + + case ENET_PEER_STATE_ACKNOWLEDGING_DISCONNECT: + if ((command -> header.command & ENET_PROTOCOL_COMMAND_MASK) == ENET_PROTOCOL_COMMAND_DISCONNECT) + enet_peer_queue_acknowledgement (peer, command, sentTime); + break; + + default: + enet_peer_queue_acknowledgement (peer, command, sentTime); + break; + } + } + } + +commandError: + if (event != NULL && event -> type != ENET_EVENT_TYPE_NONE) + return 1; + + return 0; +} + +static int +enet_protocol_receive_incoming_commands (ENetHost * host, ENetEvent * event) +{ + for (;;) + { + int receivedLength; + ENetBuffer buffer; + + buffer.data = host -> packetData [0]; + buffer.dataLength = sizeof (host -> packetData [0]); + + receivedLength = enet_socket_receive (host -> socket, + & host -> receivedAddress, + & buffer, + 1); + + if (receivedLength < 0) + return -1; + + if (receivedLength == 0) + return 0; + + host -> receivedData = host -> packetData [0]; + host -> receivedDataLength = receivedLength; + + host -> totalReceivedData += receivedLength; + host -> totalReceivedPackets ++; + + if (host -> intercept != NULL) + { + switch (host -> intercept (host, event)) + { + case 1: + if (event != NULL && event -> type != ENET_EVENT_TYPE_NONE) + return 1; + + continue; + + case -1: + return -1; + + default: + break; + } + } + + switch (enet_protocol_handle_incoming_commands (host, event)) + { + case 1: + return 1; + + case -1: + return -1; + + default: + break; + } + } + + return -1; +} + +static void +enet_protocol_send_acknowledgements (ENetHost * host, ENetPeer * peer) +{ + ENetProtocol * command = & host -> commands [host -> commandCount]; + ENetBuffer * buffer = & host -> buffers [host -> bufferCount]; + ENetAcknowledgement * acknowledgement; + ENetListIterator currentAcknowledgement; + enet_uint16 reliableSequenceNumber; + + currentAcknowledgement = enet_list_begin (& peer -> acknowledgements); + + while (currentAcknowledgement != enet_list_end (& peer -> acknowledgements)) + { + if (command >= & host -> commands [sizeof (host -> commands) / sizeof (ENetProtocol)] || + buffer >= & host -> buffers [sizeof (host -> buffers) / sizeof (ENetBuffer)] || + peer -> mtu - host -> packetSize < sizeof (ENetProtocolAcknowledge)) + { + host -> continueSending = 1; + + break; + } + + acknowledgement = (ENetAcknowledgement *) currentAcknowledgement; + + currentAcknowledgement = enet_list_next (currentAcknowledgement); + + buffer -> data = command; + buffer -> dataLength = sizeof (ENetProtocolAcknowledge); + + host -> packetSize += buffer -> dataLength; + + reliableSequenceNumber = ENET_HOST_TO_NET_16 (acknowledgement -> command.header.reliableSequenceNumber); + + command -> header.command = ENET_PROTOCOL_COMMAND_ACKNOWLEDGE; + command -> header.channelID = acknowledgement -> command.header.channelID; + command -> header.reliableSequenceNumber = reliableSequenceNumber; + command -> acknowledge.receivedReliableSequenceNumber = reliableSequenceNumber; + command -> acknowledge.receivedSentTime = ENET_HOST_TO_NET_16 (acknowledgement -> sentTime); + + if ((acknowledgement -> command.header.command & ENET_PROTOCOL_COMMAND_MASK) == ENET_PROTOCOL_COMMAND_DISCONNECT) + enet_protocol_dispatch_state (host, peer, ENET_PEER_STATE_ZOMBIE); + + enet_list_remove (& acknowledgement -> acknowledgementList); + enet_free (acknowledgement); + + ++ command; + ++ buffer; + } + + host -> commandCount = command - host -> commands; + host -> bufferCount = buffer - host -> buffers; +} + +static void +enet_protocol_send_unreliable_outgoing_commands (ENetHost * host, ENetPeer * peer) +{ + ENetProtocol * command = & host -> commands [host -> commandCount]; + ENetBuffer * buffer = & host -> buffers [host -> bufferCount]; + ENetOutgoingCommand * outgoingCommand; + ENetListIterator currentCommand; + + currentCommand = enet_list_begin (& peer -> outgoingUnreliableCommands); + + while (currentCommand != enet_list_end (& peer -> outgoingUnreliableCommands)) + { + size_t commandSize; + + outgoingCommand = (ENetOutgoingCommand *) currentCommand; + commandSize = commandSizes [outgoingCommand -> command.header.command & ENET_PROTOCOL_COMMAND_MASK]; + + if (command >= & host -> commands [sizeof (host -> commands) / sizeof (ENetProtocol)] || + buffer + 1 >= & host -> buffers [sizeof (host -> buffers) / sizeof (ENetBuffer)] || + peer -> mtu - host -> packetSize < commandSize || + (outgoingCommand -> packet != NULL && + peer -> mtu - host -> packetSize < commandSize + outgoingCommand -> fragmentLength)) + { + host -> continueSending = 1; + + break; + } + + currentCommand = enet_list_next (currentCommand); + + if (outgoingCommand -> packet != NULL && outgoingCommand -> fragmentOffset == 0) + { + peer -> packetThrottleCounter += ENET_PEER_PACKET_THROTTLE_COUNTER; + peer -> packetThrottleCounter %= ENET_PEER_PACKET_THROTTLE_SCALE; + + if (peer -> packetThrottleCounter > peer -> packetThrottle) + { + enet_uint16 reliableSequenceNumber = outgoingCommand -> reliableSequenceNumber, + unreliableSequenceNumber = outgoingCommand -> unreliableSequenceNumber; + for (;;) + { + -- outgoingCommand -> packet -> referenceCount; + + if (outgoingCommand -> packet -> referenceCount == 0) + enet_packet_destroy (outgoingCommand -> packet); + + enet_list_remove (& outgoingCommand -> outgoingCommandList); + enet_free (outgoingCommand); + + if (currentCommand == enet_list_end (& peer -> outgoingUnreliableCommands)) + break; + + outgoingCommand = (ENetOutgoingCommand *) currentCommand; + if (outgoingCommand -> reliableSequenceNumber != reliableSequenceNumber || + outgoingCommand -> unreliableSequenceNumber != unreliableSequenceNumber) + break; + + currentCommand = enet_list_next (currentCommand); + } + + continue; + } + } + + buffer -> data = command; + buffer -> dataLength = commandSize; + + host -> packetSize += buffer -> dataLength; + + * command = outgoingCommand -> command; + + enet_list_remove (& outgoingCommand -> outgoingCommandList); + + if (outgoingCommand -> packet != NULL) + { + ++ buffer; + + buffer -> data = outgoingCommand -> packet -> data + outgoingCommand -> fragmentOffset; + buffer -> dataLength = outgoingCommand -> fragmentLength; + + host -> packetSize += buffer -> dataLength; + + enet_list_insert (enet_list_end (& peer -> sentUnreliableCommands), outgoingCommand); + } + else + enet_free (outgoingCommand); + + ++ command; + ++ buffer; + } + + host -> commandCount = command - host -> commands; + host -> bufferCount = buffer - host -> buffers; + + if (peer -> state == ENET_PEER_STATE_DISCONNECT_LATER && + enet_list_empty (& peer -> outgoingReliableCommands) && + enet_list_empty (& peer -> outgoingUnreliableCommands) && + enet_list_empty (& peer -> sentReliableCommands)) + enet_peer_disconnect (peer, peer -> eventData); +} + +static int +enet_protocol_check_timeouts (ENetHost * host, ENetPeer * peer, ENetEvent * event) +{ + ENetOutgoingCommand * outgoingCommand; + ENetListIterator currentCommand, insertPosition; + + currentCommand = enet_list_begin (& peer -> sentReliableCommands); + insertPosition = enet_list_begin (& peer -> outgoingReliableCommands); + + while (currentCommand != enet_list_end (& peer -> sentReliableCommands)) + { + outgoingCommand = (ENetOutgoingCommand *) currentCommand; + + currentCommand = enet_list_next (currentCommand); + + if (ENET_TIME_DIFFERENCE (host -> serviceTime, outgoingCommand -> sentTime) < outgoingCommand -> roundTripTimeout) + continue; + + if (peer -> earliestTimeout == 0 || + ENET_TIME_LESS (outgoingCommand -> sentTime, peer -> earliestTimeout)) + peer -> earliestTimeout = outgoingCommand -> sentTime; + + if (peer -> earliestTimeout != 0 && + (ENET_TIME_DIFFERENCE (host -> serviceTime, peer -> earliestTimeout) >= peer -> timeoutMaximum || + (outgoingCommand -> roundTripTimeout >= outgoingCommand -> roundTripTimeoutLimit && + ENET_TIME_DIFFERENCE (host -> serviceTime, peer -> earliestTimeout) >= peer -> timeoutMinimum))) + { + enet_protocol_notify_disconnect (host, peer, event); + + return 1; + } + + if (outgoingCommand -> packet != NULL) + peer -> reliableDataInTransit -= outgoingCommand -> fragmentLength; + + ++ peer -> packetsLost; + + outgoingCommand -> roundTripTimeout *= 2; + + enet_list_insert (insertPosition, enet_list_remove (& outgoingCommand -> outgoingCommandList)); + + if (currentCommand == enet_list_begin (& peer -> sentReliableCommands) && + ! enet_list_empty (& peer -> sentReliableCommands)) + { + outgoingCommand = (ENetOutgoingCommand *) currentCommand; + + peer -> nextTimeout = outgoingCommand -> sentTime + outgoingCommand -> roundTripTimeout; + } + } + + return 0; +} + +static int +enet_protocol_send_reliable_outgoing_commands (ENetHost * host, ENetPeer * peer) +{ + ENetProtocol * command = & host -> commands [host -> commandCount]; + ENetBuffer * buffer = & host -> buffers [host -> bufferCount]; + ENetOutgoingCommand * outgoingCommand; + ENetListIterator currentCommand; + ENetChannel *channel; + enet_uint16 reliableWindow; + size_t commandSize; + int windowExceeded = 0, windowWrap = 0, canPing = 1; + + currentCommand = enet_list_begin (& peer -> outgoingReliableCommands); + + while (currentCommand != enet_list_end (& peer -> outgoingReliableCommands)) + { + outgoingCommand = (ENetOutgoingCommand *) currentCommand; + + channel = outgoingCommand -> command.header.channelID < peer -> channelCount ? & peer -> channels [outgoingCommand -> command.header.channelID] : NULL; + reliableWindow = outgoingCommand -> reliableSequenceNumber / ENET_PEER_RELIABLE_WINDOW_SIZE; + if (channel != NULL) + { + if (! windowWrap && + outgoingCommand -> sendAttempts < 1 && + ! (outgoingCommand -> reliableSequenceNumber % ENET_PEER_RELIABLE_WINDOW_SIZE) && + (channel -> reliableWindows [(reliableWindow + ENET_PEER_RELIABLE_WINDOWS - 1) % ENET_PEER_RELIABLE_WINDOWS] >= ENET_PEER_RELIABLE_WINDOW_SIZE || + channel -> usedReliableWindows & ((((1 << ENET_PEER_FREE_RELIABLE_WINDOWS) - 1) << reliableWindow) | + (((1 << ENET_PEER_FREE_RELIABLE_WINDOWS) - 1) >> (ENET_PEER_RELIABLE_WINDOW_SIZE - reliableWindow))))) + windowWrap = 1; + if (windowWrap) + { + currentCommand = enet_list_next (currentCommand); + + continue; + } + } + + if (outgoingCommand -> packet != NULL) + { + if (! windowExceeded) + { + enet_uint32 windowSize = (peer -> packetThrottle * peer -> windowSize) / ENET_PEER_PACKET_THROTTLE_SCALE; + + if (peer -> reliableDataInTransit + outgoingCommand -> fragmentLength > ENET_MAX (windowSize, peer -> mtu)) + windowExceeded = 1; + } + if (windowExceeded) + { + currentCommand = enet_list_next (currentCommand); + + continue; + } + } + + canPing = 0; + + commandSize = commandSizes [outgoingCommand -> command.header.command & ENET_PROTOCOL_COMMAND_MASK]; + if (command >= & host -> commands [sizeof (host -> commands) / sizeof (ENetProtocol)] || + buffer + 1 >= & host -> buffers [sizeof (host -> buffers) / sizeof (ENetBuffer)] || + peer -> mtu - host -> packetSize < commandSize || + (outgoingCommand -> packet != NULL && + (enet_uint16) (peer -> mtu - host -> packetSize) < (enet_uint16) (commandSize + outgoingCommand -> fragmentLength))) + { + host -> continueSending = 1; + + break; + } + + currentCommand = enet_list_next (currentCommand); + + if (channel != NULL && outgoingCommand -> sendAttempts < 1) + { + channel -> usedReliableWindows |= 1 << reliableWindow; + ++ channel -> reliableWindows [reliableWindow]; + } + + ++ outgoingCommand -> sendAttempts; + + if (outgoingCommand -> roundTripTimeout == 0) + { + outgoingCommand -> roundTripTimeout = peer -> roundTripTime + 4 * peer -> roundTripTimeVariance; + outgoingCommand -> roundTripTimeoutLimit = peer -> timeoutLimit * outgoingCommand -> roundTripTimeout; + } + + if (enet_list_empty (& peer -> sentReliableCommands)) + peer -> nextTimeout = host -> serviceTime + outgoingCommand -> roundTripTimeout; + + enet_list_insert (enet_list_end (& peer -> sentReliableCommands), + enet_list_remove (& outgoingCommand -> outgoingCommandList)); + + outgoingCommand -> sentTime = host -> serviceTime; + + buffer -> data = command; + buffer -> dataLength = commandSize; + + host -> packetSize += buffer -> dataLength; + host -> headerFlags |= ENET_PROTOCOL_HEADER_FLAG_SENT_TIME; + + * command = outgoingCommand -> command; + + if (outgoingCommand -> packet != NULL) + { + ++ buffer; + + buffer -> data = outgoingCommand -> packet -> data + outgoingCommand -> fragmentOffset; + buffer -> dataLength = outgoingCommand -> fragmentLength; + + host -> packetSize += outgoingCommand -> fragmentLength; + + peer -> reliableDataInTransit += outgoingCommand -> fragmentLength; + } + + ++ peer -> packetsSent; + + ++ command; + ++ buffer; + } + + host -> commandCount = command - host -> commands; + host -> bufferCount = buffer - host -> buffers; + + return canPing; +} + +static int +enet_protocol_send_outgoing_commands (ENetHost * host, ENetEvent * event, int checkForTimeouts) +{ + enet_uint8 headerData [sizeof (ENetProtocolHeader) + sizeof (enet_uint32)]; + ENetProtocolHeader * header = (ENetProtocolHeader *) headerData; + ENetPeer * currentPeer; + int sentLength; + size_t shouldCompress = 0; + + host -> continueSending = 1; + + while (host -> continueSending) + for (host -> continueSending = 0, + currentPeer = host -> peers; + currentPeer < & host -> peers [host -> peerCount]; + ++ currentPeer) + { + if (currentPeer -> state == ENET_PEER_STATE_DISCONNECTED || + currentPeer -> state == ENET_PEER_STATE_ZOMBIE) + continue; + + host -> headerFlags = 0; + host -> commandCount = 0; + host -> bufferCount = 1; + host -> packetSize = sizeof (ENetProtocolHeader); + + if (! enet_list_empty (& currentPeer -> acknowledgements)) + enet_protocol_send_acknowledgements (host, currentPeer); + + if (checkForTimeouts != 0 && + ! enet_list_empty (& currentPeer -> sentReliableCommands) && + ENET_TIME_GREATER_EQUAL (host -> serviceTime, currentPeer -> nextTimeout) && + enet_protocol_check_timeouts (host, currentPeer, event) == 1) + { + if (event != NULL && event -> type != ENET_EVENT_TYPE_NONE) + return 1; + else + continue; + } + + if ((enet_list_empty (& currentPeer -> outgoingReliableCommands) || + enet_protocol_send_reliable_outgoing_commands (host, currentPeer)) && + enet_list_empty (& currentPeer -> sentReliableCommands) && + ENET_TIME_DIFFERENCE (host -> serviceTime, currentPeer -> lastReceiveTime) >= currentPeer -> pingInterval && + currentPeer -> mtu - host -> packetSize >= sizeof (ENetProtocolPing)) + { + enet_peer_ping (currentPeer); + enet_protocol_send_reliable_outgoing_commands (host, currentPeer); + } + + if (! enet_list_empty (& currentPeer -> outgoingUnreliableCommands)) + enet_protocol_send_unreliable_outgoing_commands (host, currentPeer); + + if (host -> commandCount == 0) + continue; + + if (currentPeer -> packetLossEpoch == 0) + currentPeer -> packetLossEpoch = host -> serviceTime; + else + if (ENET_TIME_DIFFERENCE (host -> serviceTime, currentPeer -> packetLossEpoch) >= ENET_PEER_PACKET_LOSS_INTERVAL && + currentPeer -> packetsSent > 0) + { + enet_uint32 packetLoss = currentPeer -> packetsLost * ENET_PEER_PACKET_LOSS_SCALE / currentPeer -> packetsSent; + +#ifdef ENET_DEBUG + printf ("peer %u: %f%%+-%f%% packet loss, %u+-%u ms round trip time, %f%% throttle, %u/%u outgoing, %u/%u incoming\n", currentPeer -> incomingPeerID, currentPeer -> packetLoss / (float) ENET_PEER_PACKET_LOSS_SCALE, currentPeer -> packetLossVariance / (float) ENET_PEER_PACKET_LOSS_SCALE, currentPeer -> roundTripTime, currentPeer -> roundTripTimeVariance, currentPeer -> packetThrottle / (float) ENET_PEER_PACKET_THROTTLE_SCALE, enet_list_size (& currentPeer -> outgoingReliableCommands), enet_list_size (& currentPeer -> outgoingUnreliableCommands), currentPeer -> channels != NULL ? enet_list_size (& currentPeer -> channels -> incomingReliableCommands) : 0, currentPeer -> channels != NULL ? enet_list_size (& currentPeer -> channels -> incomingUnreliableCommands) : 0); +#endif + + currentPeer -> packetLossVariance -= currentPeer -> packetLossVariance / 4; + + if (packetLoss >= currentPeer -> packetLoss) + { + currentPeer -> packetLoss += (packetLoss - currentPeer -> packetLoss) / 8; + currentPeer -> packetLossVariance += (packetLoss - currentPeer -> packetLoss) / 4; + } + else + { + currentPeer -> packetLoss -= (currentPeer -> packetLoss - packetLoss) / 8; + currentPeer -> packetLossVariance += (currentPeer -> packetLoss - packetLoss) / 4; + } + + currentPeer -> packetLossEpoch = host -> serviceTime; + currentPeer -> packetsSent = 0; + currentPeer -> packetsLost = 0; + } + + host -> buffers -> data = headerData; + if (host -> headerFlags & ENET_PROTOCOL_HEADER_FLAG_SENT_TIME) + { + header -> sentTime = ENET_HOST_TO_NET_16 (host -> serviceTime & 0xFFFF); + + host -> buffers -> dataLength = sizeof (ENetProtocolHeader); + } + else + host -> buffers -> dataLength = (size_t) & ((ENetProtocolHeader *) 0) -> sentTime; + + shouldCompress = 0; + if (host -> compressor.context != NULL && host -> compressor.compress != NULL) + { + size_t originalSize = host -> packetSize - sizeof(ENetProtocolHeader), + compressedSize = host -> compressor.compress (host -> compressor.context, + & host -> buffers [1], host -> bufferCount - 1, + originalSize, + host -> packetData [1], + originalSize); + if (compressedSize > 0 && compressedSize < originalSize) + { + host -> headerFlags |= ENET_PROTOCOL_HEADER_FLAG_COMPRESSED; + shouldCompress = compressedSize; +#ifdef ENET_DEBUG_COMPRESS + printf ("peer %u: compressed %u -> %u (%u%%)\n", currentPeer -> incomingPeerID, originalSize, compressedSize, (compressedSize * 100) / originalSize); +#endif + } + } + + if (currentPeer -> outgoingPeerID < ENET_PROTOCOL_MAXIMUM_PEER_ID) + host -> headerFlags |= currentPeer -> outgoingSessionID << ENET_PROTOCOL_HEADER_SESSION_SHIFT; + header -> peerID = ENET_HOST_TO_NET_16 (currentPeer -> outgoingPeerID | host -> headerFlags); + if (host -> checksum != NULL) + { + enet_uint32 * checksum = (enet_uint32 *) & headerData [host -> buffers -> dataLength]; + * checksum = currentPeer -> outgoingPeerID < ENET_PROTOCOL_MAXIMUM_PEER_ID ? currentPeer -> connectID : 0; + host -> buffers -> dataLength += sizeof (enet_uint32); + * checksum = host -> checksum (host -> buffers, host -> bufferCount); + } + + if (shouldCompress > 0) + { + host -> buffers [1].data = host -> packetData [1]; + host -> buffers [1].dataLength = shouldCompress; + host -> bufferCount = 2; + } + + currentPeer -> lastSendTime = host -> serviceTime; + + sentLength = enet_socket_send (host -> socket, & currentPeer -> address, host -> buffers, host -> bufferCount); + + enet_protocol_remove_sent_unreliable_commands (currentPeer); + + if (sentLength < 0) + return -1; + + host -> totalSentData += sentLength; + host -> totalSentPackets ++; + } + + return 0; +} + +/** Sends any queued packets on the host specified to its designated peers. + + @param host host to flush + @remarks this function need only be used in circumstances where one wishes to send queued packets earlier than in a call to enet_host_service(). + @ingroup host +*/ +void +enet_host_flush (ENetHost * host) +{ + host -> serviceTime = enet_time_get (); + + enet_protocol_send_outgoing_commands (host, NULL, 0); +} + +/** Checks for any queued events on the host and dispatches one if available. + + @param host host to check for events + @param event an event structure where event details will be placed if available + @retval > 0 if an event was dispatched + @retval 0 if no events are available + @retval < 0 on failure + @ingroup host +*/ +int +enet_host_check_events (ENetHost * host, ENetEvent * event) +{ + if (event == NULL) return -1; + + event -> type = ENET_EVENT_TYPE_NONE; + event -> peer = NULL; + event -> packet = NULL; + + return enet_protocol_dispatch_incoming_commands (host, event); +} + +/** Waits for events on the host specified and shuttles packets between + the host and its peers. + + @param host host to service + @param event an event structure where event details will be placed if one occurs + if event == NULL then no events will be delivered + @param timeout number of milliseconds that ENet should wait for events + @retval > 0 if an event occurred within the specified time limit + @retval 0 if no event occurred + @retval < 0 on failure + @remarks enet_host_service should be called fairly regularly for adequate performance + @ingroup host +*/ +int +enet_host_service (ENetHost * host, ENetEvent * event, enet_uint32 timeout) +{ + enet_uint32 waitCondition; + + if (event != NULL) + { + event -> type = ENET_EVENT_TYPE_NONE; + event -> peer = NULL; + event -> packet = NULL; + + switch (enet_protocol_dispatch_incoming_commands (host, event)) + { + case 1: + return 1; + + case -1: +#ifdef ENET_DEBUG + perror ("Error dispatching incoming packets"); +#endif + + return -1; + + default: + break; + } + } + + host -> serviceTime = enet_time_get (); + + timeout += host -> serviceTime; + + do + { + if (ENET_TIME_DIFFERENCE (host -> serviceTime, host -> bandwidthThrottleEpoch) >= ENET_HOST_BANDWIDTH_THROTTLE_INTERVAL) + enet_host_bandwidth_throttle (host); + + switch (enet_protocol_send_outgoing_commands (host, event, 1)) + { + case 1: + return 1; + + case -1: +#ifdef ENET_DEBUG + perror ("Error sending outgoing packets"); +#endif + + return -1; + + default: + break; + } + + switch (enet_protocol_receive_incoming_commands (host, event)) + { + case 1: + return 1; + + case -1: +#ifdef ENET_DEBUG + perror ("Error receiving incoming packets"); +#endif + + return -1; + + default: + break; + } + + switch (enet_protocol_send_outgoing_commands (host, event, 1)) + { + case 1: + return 1; + + case -1: +#ifdef ENET_DEBUG + perror ("Error sending outgoing packets"); +#endif + + return -1; + + default: + break; + } + + if (event != NULL) + { + switch (enet_protocol_dispatch_incoming_commands (host, event)) + { + case 1: + return 1; + + case -1: +#ifdef ENET_DEBUG + perror ("Error dispatching incoming packets"); +#endif + + return -1; + + default: + break; + } + } + + do + { + host -> serviceTime = enet_time_get (); + + if (ENET_TIME_GREATER_EQUAL (host -> serviceTime, timeout)) + return 0; + + waitCondition = ENET_SOCKET_WAIT_RECEIVE | ENET_SOCKET_WAIT_INTERRUPT; + + if (enet_socket_wait (host -> socket, & waitCondition, ENET_TIME_DIFFERENCE (timeout, host -> serviceTime)) != 0) + return -1; + } + while (waitCondition & ENET_SOCKET_WAIT_INTERRUPT); + + host -> serviceTime = enet_time_get (); + } while (waitCondition & ENET_SOCKET_WAIT_RECEIVE); + + return 0; +} + diff --git a/Externals/enet/protocol.h b/Externals/enet/protocol.h new file mode 100644 index 000000000000..8e7beabe30ed --- /dev/null +++ b/Externals/enet/protocol.h @@ -0,0 +1,199 @@ +/** + @file protocol.h + @brief ENet protocol +*/ +#ifndef __ENET_PROTOCOL_H__ +#define __ENET_PROTOCOL_H__ + +#include "enet/types.h" + +enum +{ + ENET_PROTOCOL_MINIMUM_MTU = 576, + ENET_PROTOCOL_MAXIMUM_MTU = 4096, + ENET_PROTOCOL_MAXIMUM_PACKET_COMMANDS = 32, + ENET_PROTOCOL_MINIMUM_WINDOW_SIZE = 4096, + ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE = 32768, + ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT = 1, + ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT = 255, + ENET_PROTOCOL_MAXIMUM_PEER_ID = 0xFFF, + ENET_PROTOCOL_MAXIMUM_PACKET_SIZE = 1024 * 1024 * 1024, + ENET_PROTOCOL_MAXIMUM_FRAGMENT_COUNT = 1024 * 1024 +}; + +typedef enum _ENetProtocolCommand +{ + ENET_PROTOCOL_COMMAND_NONE = 0, + ENET_PROTOCOL_COMMAND_ACKNOWLEDGE = 1, + ENET_PROTOCOL_COMMAND_CONNECT = 2, + ENET_PROTOCOL_COMMAND_VERIFY_CONNECT = 3, + ENET_PROTOCOL_COMMAND_DISCONNECT = 4, + ENET_PROTOCOL_COMMAND_PING = 5, + ENET_PROTOCOL_COMMAND_SEND_RELIABLE = 6, + ENET_PROTOCOL_COMMAND_SEND_UNRELIABLE = 7, + ENET_PROTOCOL_COMMAND_SEND_FRAGMENT = 8, + ENET_PROTOCOL_COMMAND_SEND_UNSEQUENCED = 9, + ENET_PROTOCOL_COMMAND_BANDWIDTH_LIMIT = 10, + ENET_PROTOCOL_COMMAND_THROTTLE_CONFIGURE = 11, + ENET_PROTOCOL_COMMAND_SEND_UNRELIABLE_FRAGMENT = 12, + ENET_PROTOCOL_COMMAND_COUNT = 13, + + ENET_PROTOCOL_COMMAND_MASK = 0x0F +} ENetProtocolCommand; + +typedef enum _ENetProtocolFlag +{ + ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE = (1 << 7), + ENET_PROTOCOL_COMMAND_FLAG_UNSEQUENCED = (1 << 6), + + ENET_PROTOCOL_HEADER_FLAG_COMPRESSED = (1 << 14), + ENET_PROTOCOL_HEADER_FLAG_SENT_TIME = (1 << 15), + ENET_PROTOCOL_HEADER_FLAG_MASK = ENET_PROTOCOL_HEADER_FLAG_COMPRESSED | ENET_PROTOCOL_HEADER_FLAG_SENT_TIME, + + ENET_PROTOCOL_HEADER_SESSION_MASK = (3 << 12), + ENET_PROTOCOL_HEADER_SESSION_SHIFT = 12 +} ENetProtocolFlag; + +#ifdef _MSC_VER_ +#pragma pack(push, 1) +#define ENET_PACKED +#elif defined(__GNUC__) || defined(__clang__) +#define ENET_PACKED __attribute__ ((packed)) +#else +#define ENET_PACKED +#endif + +typedef struct _ENetProtocolHeader +{ + enet_uint16 peerID; + enet_uint16 sentTime; +} ENET_PACKED ENetProtocolHeader; + +typedef struct _ENetProtocolCommandHeader +{ + enet_uint8 command; + enet_uint8 channelID; + enet_uint16 reliableSequenceNumber; +} ENET_PACKED ENetProtocolCommandHeader; + +typedef struct _ENetProtocolAcknowledge +{ + ENetProtocolCommandHeader header; + enet_uint16 receivedReliableSequenceNumber; + enet_uint16 receivedSentTime; +} ENET_PACKED ENetProtocolAcknowledge; + +typedef struct _ENetProtocolConnect +{ + ENetProtocolCommandHeader header; + enet_uint16 outgoingPeerID; + enet_uint8 incomingSessionID; + enet_uint8 outgoingSessionID; + enet_uint32 mtu; + enet_uint32 windowSize; + enet_uint32 channelCount; + enet_uint32 incomingBandwidth; + enet_uint32 outgoingBandwidth; + enet_uint32 packetThrottleInterval; + enet_uint32 packetThrottleAcceleration; + enet_uint32 packetThrottleDeceleration; + enet_uint32 connectID; + enet_uint32 data; +} ENET_PACKED ENetProtocolConnect; + +typedef struct _ENetProtocolVerifyConnect +{ + ENetProtocolCommandHeader header; + enet_uint16 outgoingPeerID; + enet_uint8 incomingSessionID; + enet_uint8 outgoingSessionID; + enet_uint32 mtu; + enet_uint32 windowSize; + enet_uint32 channelCount; + enet_uint32 incomingBandwidth; + enet_uint32 outgoingBandwidth; + enet_uint32 packetThrottleInterval; + enet_uint32 packetThrottleAcceleration; + enet_uint32 packetThrottleDeceleration; + enet_uint32 connectID; +} ENET_PACKED ENetProtocolVerifyConnect; + +typedef struct _ENetProtocolBandwidthLimit +{ + ENetProtocolCommandHeader header; + enet_uint32 incomingBandwidth; + enet_uint32 outgoingBandwidth; +} ENET_PACKED ENetProtocolBandwidthLimit; + +typedef struct _ENetProtocolThrottleConfigure +{ + ENetProtocolCommandHeader header; + enet_uint32 packetThrottleInterval; + enet_uint32 packetThrottleAcceleration; + enet_uint32 packetThrottleDeceleration; +} ENET_PACKED ENetProtocolThrottleConfigure; + +typedef struct _ENetProtocolDisconnect +{ + ENetProtocolCommandHeader header; + enet_uint32 data; +} ENET_PACKED ENetProtocolDisconnect; + +typedef struct _ENetProtocolPing +{ + ENetProtocolCommandHeader header; +} ENET_PACKED ENetProtocolPing; + +typedef struct _ENetProtocolSendReliable +{ + ENetProtocolCommandHeader header; + enet_uint16 dataLength; +} ENET_PACKED ENetProtocolSendReliable; + +typedef struct _ENetProtocolSendUnreliable +{ + ENetProtocolCommandHeader header; + enet_uint16 unreliableSequenceNumber; + enet_uint16 dataLength; +} ENET_PACKED ENetProtocolSendUnreliable; + +typedef struct _ENetProtocolSendUnsequenced +{ + ENetProtocolCommandHeader header; + enet_uint16 unsequencedGroup; + enet_uint16 dataLength; +} ENET_PACKED ENetProtocolSendUnsequenced; + +typedef struct _ENetProtocolSendFragment +{ + ENetProtocolCommandHeader header; + enet_uint16 startSequenceNumber; + enet_uint16 dataLength; + enet_uint32 fragmentCount; + enet_uint32 fragmentNumber; + enet_uint32 totalLength; + enet_uint32 fragmentOffset; +} ENET_PACKED ENetProtocolSendFragment; + +typedef union _ENetProtocol +{ + ENetProtocolCommandHeader header; + ENetProtocolAcknowledge acknowledge; + ENetProtocolConnect connect; + ENetProtocolVerifyConnect verifyConnect; + ENetProtocolDisconnect disconnect; + ENetProtocolPing ping; + ENetProtocolSendReliable sendReliable; + ENetProtocolSendUnreliable sendUnreliable; + ENetProtocolSendUnsequenced sendUnsequenced; + ENetProtocolSendFragment sendFragment; + ENetProtocolBandwidthLimit bandwidthLimit; + ENetProtocolThrottleConfigure throttleConfigure; +} ENET_PACKED ENetProtocol; + +#ifdef _MSC_VER_ +#pragma pack(pop) +#endif + +#endif /* __ENET_PROTOCOL_H__ */ + diff --git a/Externals/enet/time.h b/Externals/enet/time.h new file mode 100644 index 000000000000..c82a5460351b --- /dev/null +++ b/Externals/enet/time.h @@ -0,0 +1,18 @@ +/** + @file time.h + @brief ENet time constants and macros +*/ +#ifndef __ENET_TIME_H__ +#define __ENET_TIME_H__ + +#define ENET_TIME_OVERFLOW 86400000 + +#define ENET_TIME_LESS(a, b) ((a) - (b) >= ENET_TIME_OVERFLOW) +#define ENET_TIME_GREATER(a, b) ((b) - (a) >= ENET_TIME_OVERFLOW) +#define ENET_TIME_LESS_EQUAL(a, b) (! ENET_TIME_GREATER (a, b)) +#define ENET_TIME_GREATER_EQUAL(a, b) (! ENET_TIME_LESS (a, b)) + +#define ENET_TIME_DIFFERENCE(a, b) ((a) - (b) >= ENET_TIME_OVERFLOW ? (b) - (a) : (a) - (b)) + +#endif /* __ENET_TIME_H__ */ + diff --git a/Externals/enet/types.h b/Externals/enet/types.h new file mode 100644 index 000000000000..ab010a4b13dd --- /dev/null +++ b/Externals/enet/types.h @@ -0,0 +1,13 @@ +/** + @file types.h + @brief type definitions for ENet +*/ +#ifndef __ENET_TYPES_H__ +#define __ENET_TYPES_H__ + +typedef unsigned char enet_uint8; /**< unsigned 8-bit type */ +typedef unsigned short enet_uint16; /**< unsigned 16-bit type */ +typedef unsigned int enet_uint32; /**< unsigned 32-bit type */ + +#endif /* __ENET_TYPES_H__ */ + diff --git a/Externals/enet/unix.c b/Externals/enet/unix.c new file mode 100644 index 000000000000..3fdce96ca3cd --- /dev/null +++ b/Externals/enet/unix.c @@ -0,0 +1,547 @@ +/** + @file unix.c + @brief ENet Unix system specific functions +*/ +#ifndef WIN32 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ENET_BUILDING_LIB 1 +#include "enet/enet.h" + +#ifdef __APPLE__ +#ifdef HAS_POLL +#undef HAS_POLL +#endif +#ifndef HAS_FCNTL +#define HAS_FCNTL 1 +#endif +#ifndef HAS_INET_PTON +#define HAS_INET_PTON 1 +#endif +#ifndef HAS_INET_NTOP +#define HAS_INET_NTOP 1 +#endif +#ifndef HAS_MSGHDR_FLAGS +#define HAS_MSGHDR_FLAGS 1 +#endif +#ifndef HAS_SOCKLEN_T +#define HAS_SOCKLEN_T 1 +#endif +#endif + +#ifdef HAS_FCNTL +#include +#endif + +#ifdef HAS_POLL +#include +#endif + +#ifndef HAS_SOCKLEN_T +typedef int socklen_t; +#endif + +#ifndef MSG_NOSIGNAL +#define MSG_NOSIGNAL 0 +#endif + +static enet_uint32 timeBase = 0; + +int +enet_initialize (void) +{ + return 0; +} + +void +enet_deinitialize (void) +{ +} + +enet_uint32 +enet_host_random_seed (void) +{ + return (enet_uint32) time (NULL); +} + +enet_uint32 +enet_time_get (void) +{ + struct timeval timeVal; + + gettimeofday (& timeVal, NULL); + + return timeVal.tv_sec * 1000 + timeVal.tv_usec / 1000 - timeBase; +} + +void +enet_time_set (enet_uint32 newTimeBase) +{ + struct timeval timeVal; + + gettimeofday (& timeVal, NULL); + + timeBase = timeVal.tv_sec * 1000 + timeVal.tv_usec / 1000 - newTimeBase; +} + +int +enet_address_set_host (ENetAddress * address, const char * name) +{ + struct hostent * hostEntry = NULL; +#ifdef HAS_GETHOSTBYNAME_R + struct hostent hostData; + char buffer [2048]; + int errnum; + +#if defined(linux) || defined(__linux) || defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) + gethostbyname_r (name, & hostData, buffer, sizeof (buffer), & hostEntry, & errnum); +#else + hostEntry = gethostbyname_r (name, & hostData, buffer, sizeof (buffer), & errnum); +#endif +#else + hostEntry = gethostbyname (name); +#endif + + if (hostEntry == NULL || + hostEntry -> h_addrtype != AF_INET) + { +#ifdef HAS_INET_PTON + if (! inet_pton (AF_INET, name, & address -> host)) +#else + if (! inet_aton (name, (struct in_addr *) & address -> host)) +#endif + return -1; + return 0; + } + + address -> host = * (enet_uint32 *) hostEntry -> h_addr_list [0]; + + return 0; +} + +int +enet_address_get_host_ip (const ENetAddress * address, char * name, size_t nameLength) +{ +#ifdef HAS_INET_NTOP + if (inet_ntop (AF_INET, & address -> host, name, nameLength) == NULL) +#else + char * addr = inet_ntoa (* (struct in_addr *) & address -> host); + if (addr != NULL) + strncpy (name, addr, nameLength); + else +#endif + return -1; + return 0; +} + +int +enet_address_get_host (const ENetAddress * address, char * name, size_t nameLength) +{ + struct in_addr in; + struct hostent * hostEntry = NULL; +#ifdef HAS_GETHOSTBYADDR_R + struct hostent hostData; + char buffer [2048]; + int errnum; + + in.s_addr = address -> host; + +#if defined(linux) || defined(__linux) || defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) + gethostbyaddr_r ((char *) & in, sizeof (struct in_addr), AF_INET, & hostData, buffer, sizeof (buffer), & hostEntry, & errnum); +#else + hostEntry = gethostbyaddr_r ((char *) & in, sizeof (struct in_addr), AF_INET, & hostData, buffer, sizeof (buffer), & errnum); +#endif +#else + in.s_addr = address -> host; + + hostEntry = gethostbyaddr ((char *) & in, sizeof (struct in_addr), AF_INET); +#endif + + if (hostEntry == NULL) + return enet_address_get_host_ip (address, name, nameLength); + + strncpy (name, hostEntry -> h_name, nameLength); + + return 0; +} + +int +enet_socket_bind (ENetSocket socket, const ENetAddress * address) +{ + struct sockaddr_in sin; + + memset (& sin, 0, sizeof (struct sockaddr_in)); + + sin.sin_family = AF_INET; + + if (address != NULL) + { + sin.sin_port = ENET_HOST_TO_NET_16 (address -> port); + sin.sin_addr.s_addr = address -> host; + } + else + { + sin.sin_port = 0; + sin.sin_addr.s_addr = INADDR_ANY; + } + + return bind (socket, + (struct sockaddr *) & sin, + sizeof (struct sockaddr_in)); +} + +int +enet_socket_get_address (ENetSocket socket, ENetAddress * address) +{ + struct sockaddr_in sin; + socklen_t sinLength = sizeof (struct sockaddr_in); + + if (getsockname (socket, (struct sockaddr *) & sin, & sinLength) == -1) + return -1; + + address -> host = (enet_uint32) sin.sin_addr.s_addr; + address -> port = ENET_NET_TO_HOST_16 (sin.sin_port); + + return 0; +} + +int +enet_socket_listen (ENetSocket socket, int backlog) +{ + return listen (socket, backlog < 0 ? SOMAXCONN : backlog); +} + +ENetSocket +enet_socket_create (ENetSocketType type) +{ + return socket (PF_INET, type == ENET_SOCKET_TYPE_DATAGRAM ? SOCK_DGRAM : SOCK_STREAM, 0); +} + +int +enet_socket_set_option (ENetSocket socket, ENetSocketOption option, int value) +{ + int result = -1; + switch (option) + { + case ENET_SOCKOPT_NONBLOCK: +#ifdef HAS_FCNTL + result = fcntl (socket, F_SETFL, O_NONBLOCK | fcntl (socket, F_GETFL)); +#else + result = ioctl (socket, FIONBIO, & value); +#endif + break; + + case ENET_SOCKOPT_BROADCAST: + result = setsockopt (socket, SOL_SOCKET, SO_BROADCAST, (char *) & value, sizeof (int)); + break; + + case ENET_SOCKOPT_REUSEADDR: + result = setsockopt (socket, SOL_SOCKET, SO_REUSEADDR, (char *) & value, sizeof (int)); + break; + + case ENET_SOCKOPT_RCVBUF: + result = setsockopt (socket, SOL_SOCKET, SO_RCVBUF, (char *) & value, sizeof (int)); + break; + + case ENET_SOCKOPT_SNDBUF: + result = setsockopt (socket, SOL_SOCKET, SO_SNDBUF, (char *) & value, sizeof (int)); + break; + + case ENET_SOCKOPT_RCVTIMEO: + { + struct timeval timeVal; + timeVal.tv_sec = value / 1000; + timeVal.tv_usec = (value % 1000) * 1000; + result = setsockopt (socket, SOL_SOCKET, SO_RCVTIMEO, (char *) & timeVal, sizeof (struct timeval)); + break; + } + + case ENET_SOCKOPT_SNDTIMEO: + { + struct timeval timeVal; + timeVal.tv_sec = value / 1000; + timeVal.tv_usec = (value % 1000) * 1000; + result = setsockopt (socket, SOL_SOCKET, SO_SNDTIMEO, (char *) & timeVal, sizeof (struct timeval)); + break; + } + + case ENET_SOCKOPT_NODELAY: + result = setsockopt (socket, IPPROTO_TCP, TCP_NODELAY, (char *) & value, sizeof (int)); + break; + + default: + break; + } + return result == -1 ? -1 : 0; +} + +int +enet_socket_get_option (ENetSocket socket, ENetSocketOption option, int * value) +{ + int result = -1; + socklen_t len; + switch (option) + { + case ENET_SOCKOPT_ERROR: + len = sizeof (int); + result = getsockopt (socket, SOL_SOCKET, SO_ERROR, value, & len); + break; + + default: + break; + } + return result == -1 ? -1 : 0; +} + +int +enet_socket_connect (ENetSocket socket, const ENetAddress * address) +{ + struct sockaddr_in sin; + int result; + + memset (& sin, 0, sizeof (struct sockaddr_in)); + + sin.sin_family = AF_INET; + sin.sin_port = ENET_HOST_TO_NET_16 (address -> port); + sin.sin_addr.s_addr = address -> host; + + result = connect (socket, (struct sockaddr *) & sin, sizeof (struct sockaddr_in)); + if (result == -1 && errno == EINPROGRESS) + return 0; + + return result; +} + +ENetSocket +enet_socket_accept (ENetSocket socket, ENetAddress * address) +{ + int result; + struct sockaddr_in sin; + socklen_t sinLength = sizeof (struct sockaddr_in); + + result = accept (socket, + address != NULL ? (struct sockaddr *) & sin : NULL, + address != NULL ? & sinLength : NULL); + + if (result == -1) + return ENET_SOCKET_NULL; + + if (address != NULL) + { + address -> host = (enet_uint32) sin.sin_addr.s_addr; + address -> port = ENET_NET_TO_HOST_16 (sin.sin_port); + } + + return result; +} + +int +enet_socket_shutdown (ENetSocket socket, ENetSocketShutdown how) +{ + return shutdown (socket, (int) how); +} + +void +enet_socket_destroy (ENetSocket socket) +{ + if (socket != -1) + close (socket); +} + +int +enet_socket_send (ENetSocket socket, + const ENetAddress * address, + const ENetBuffer * buffers, + size_t bufferCount) +{ + struct msghdr msgHdr; + struct sockaddr_in sin; + int sentLength; + + memset (& msgHdr, 0, sizeof (struct msghdr)); + + if (address != NULL) + { + memset (& sin, 0, sizeof (struct sockaddr_in)); + + sin.sin_family = AF_INET; + sin.sin_port = ENET_HOST_TO_NET_16 (address -> port); + sin.sin_addr.s_addr = address -> host; + + msgHdr.msg_name = & sin; + msgHdr.msg_namelen = sizeof (struct sockaddr_in); + } + + msgHdr.msg_iov = (struct iovec *) buffers; + msgHdr.msg_iovlen = bufferCount; + + sentLength = sendmsg (socket, & msgHdr, MSG_NOSIGNAL); + + if (sentLength == -1) + { + if (errno == EWOULDBLOCK) + return 0; + + return -1; + } + + return sentLength; +} + +int +enet_socket_receive (ENetSocket socket, + ENetAddress * address, + ENetBuffer * buffers, + size_t bufferCount) +{ + struct msghdr msgHdr; + struct sockaddr_in sin; + int recvLength; + + memset (& msgHdr, 0, sizeof (struct msghdr)); + + if (address != NULL) + { + msgHdr.msg_name = & sin; + msgHdr.msg_namelen = sizeof (struct sockaddr_in); + } + + msgHdr.msg_iov = (struct iovec *) buffers; + msgHdr.msg_iovlen = bufferCount; + + recvLength = recvmsg (socket, & msgHdr, MSG_NOSIGNAL); + + if (recvLength == -1) + { + if (errno == EWOULDBLOCK) + return 0; + + return -1; + } + +#ifdef HAS_MSGHDR_FLAGS + if (msgHdr.msg_flags & MSG_TRUNC) + return -1; +#endif + + if (address != NULL) + { + address -> host = (enet_uint32) sin.sin_addr.s_addr; + address -> port = ENET_NET_TO_HOST_16 (sin.sin_port); + } + + return recvLength; +} + +int +enet_socketset_select (ENetSocket maxSocket, ENetSocketSet * readSet, ENetSocketSet * writeSet, enet_uint32 timeout) +{ + struct timeval timeVal; + + timeVal.tv_sec = timeout / 1000; + timeVal.tv_usec = (timeout % 1000) * 1000; + + return select (maxSocket + 1, readSet, writeSet, NULL, & timeVal); +} + +int +enet_socket_wait (ENetSocket socket, enet_uint32 * condition, enet_uint32 timeout) +{ +#ifdef HAS_POLL + struct pollfd pollSocket; + int pollCount; + + pollSocket.fd = socket; + pollSocket.events = 0; + + if (* condition & ENET_SOCKET_WAIT_SEND) + pollSocket.events |= POLLOUT; + + if (* condition & ENET_SOCKET_WAIT_RECEIVE) + pollSocket.events |= POLLIN; + + pollCount = poll (& pollSocket, 1, timeout); + + if (pollCount < 0) + { + if (errno == EINTR && * condition & ENET_SOCKET_WAIT_INTERRUPT) + { + * condition = ENET_SOCKET_WAIT_INTERRUPT; + + return 0; + } + + return -1; + } + + * condition = ENET_SOCKET_WAIT_NONE; + + if (pollCount == 0) + return 0; + + if (pollSocket.revents & POLLOUT) + * condition |= ENET_SOCKET_WAIT_SEND; + + if (pollSocket.revents & POLLIN) + * condition |= ENET_SOCKET_WAIT_RECEIVE; + + return 0; +#else + fd_set readSet, writeSet; + struct timeval timeVal; + int selectCount; + + timeVal.tv_sec = timeout / 1000; + timeVal.tv_usec = (timeout % 1000) * 1000; + + FD_ZERO (& readSet); + FD_ZERO (& writeSet); + + if (* condition & ENET_SOCKET_WAIT_SEND) + FD_SET (socket, & writeSet); + + if (* condition & ENET_SOCKET_WAIT_RECEIVE) + FD_SET (socket, & readSet); + + selectCount = select (socket + 1, & readSet, & writeSet, NULL, & timeVal); + + if (selectCount < 0) + { + if (errno == EINTR && * condition & ENET_SOCKET_WAIT_INTERRUPT) + { + * condition = ENET_SOCKET_WAIT_INTERRUPT; + + return 0; + } + + return -1; + } + + * condition = ENET_SOCKET_WAIT_NONE; + + if (selectCount == 0) + return 0; + + if (FD_ISSET (socket, & writeSet)) + * condition |= ENET_SOCKET_WAIT_SEND; + + if (FD_ISSET (socket, & readSet)) + * condition |= ENET_SOCKET_WAIT_RECEIVE; + + return 0; +#endif +} + +#endif + diff --git a/Externals/enet/unix.h b/Externals/enet/unix.h new file mode 100644 index 000000000000..9683c09b4b17 --- /dev/null +++ b/Externals/enet/unix.h @@ -0,0 +1,47 @@ +/** + @file unix.h + @brief ENet Unix header +*/ +#ifndef __ENET_UNIX_H__ +#define __ENET_UNIX_H__ + +#include +#include +#include +#include +#include +#include + +#ifdef MSG_MAXIOVLEN +#define ENET_BUFFER_MAXIMUM MSG_MAXIOVLEN +#endif + +typedef int ENetSocket; + +#define ENET_SOCKET_NULL -1 + +#define ENET_HOST_TO_NET_16(value) (htons (value)) /**< macro that converts host to net byte-order of a 16-bit value */ +#define ENET_HOST_TO_NET_32(value) (htonl (value)) /**< macro that converts host to net byte-order of a 32-bit value */ + +#define ENET_NET_TO_HOST_16(value) (ntohs (value)) /**< macro that converts net to host byte-order of a 16-bit value */ +#define ENET_NET_TO_HOST_32(value) (ntohl (value)) /**< macro that converts net to host byte-order of a 32-bit value */ + +typedef struct +{ + void * data; + size_t dataLength; +} ENetBuffer; + +#define ENET_CALLBACK + +#define ENET_API extern + +typedef fd_set ENetSocketSet; + +#define ENET_SOCKETSET_EMPTY(sockset) FD_ZERO (& (sockset)) +#define ENET_SOCKETSET_ADD(sockset, socket) FD_SET (socket, & (sockset)) +#define ENET_SOCKETSET_REMOVE(sockset, socket) FD_CLEAR (socket, & (sockset)) +#define ENET_SOCKETSET_CHECK(sockset, socket) FD_ISSET (socket, & (sockset)) + +#endif /* __ENET_UNIX_H__ */ + diff --git a/Externals/enet/update-enet.sh b/Externals/enet/update-enet.sh new file mode 100755 index 000000000000..05aad8519fe9 --- /dev/null +++ b/Externals/enet/update-enet.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -xe +rm -rf _enet +git rm -rf --ignore-unmatch *.[ch] +git clone https://github.com/lsalzman/enet.git _enet +cd _enet; git rev-parse HEAD > ../git-revision; cd .. +mv _enet/*.c _enet/include/enet/*.h _enet/LICENSE . +git add *.[ch] LICENSE git-revision +rm -rf _enet +echo 'Make sure to update CMakeLists.txt.' diff --git a/Externals/enet/utility.h b/Externals/enet/utility.h new file mode 100644 index 000000000000..e48a476be3b5 --- /dev/null +++ b/Externals/enet/utility.h @@ -0,0 +1,12 @@ +/** + @file utility.h + @brief ENet utility header +*/ +#ifndef __ENET_UTILITY_H__ +#define __ENET_UTILITY_H__ + +#define ENET_MAX(x, y) ((x) > (y) ? (x) : (y)) +#define ENET_MIN(x, y) ((x) < (y) ? (x) : (y)) + +#endif /* __ENET_UTILITY_H__ */ + diff --git a/Externals/enet/win32.c b/Externals/enet/win32.c new file mode 100644 index 000000000000..c441841d4505 --- /dev/null +++ b/Externals/enet/win32.c @@ -0,0 +1,410 @@ +/** + @file win32.c + @brief ENet Win32 system specific functions +*/ +#ifdef _WIN32 + +#include +#define ENET_BUILDING_LIB 1 +#include "enet/enet.h" + +static enet_uint32 timeBase = 0; + +int +enet_initialize (void) +{ + WORD versionRequested = MAKEWORD (1, 1); + WSADATA wsaData; + + if (WSAStartup (versionRequested, & wsaData)) + return -1; + + if (LOBYTE (wsaData.wVersion) != 1|| + HIBYTE (wsaData.wVersion) != 1) + { + WSACleanup (); + + return -1; + } + + timeBeginPeriod (1); + + return 0; +} + +void +enet_deinitialize (void) +{ + timeEndPeriod (1); + + WSACleanup (); +} + +enet_uint32 +enet_host_random_seed (void) +{ + return (enet_uint32) timeGetTime (); +} + +enet_uint32 +enet_time_get (void) +{ + return (enet_uint32) timeGetTime () - timeBase; +} + +void +enet_time_set (enet_uint32 newTimeBase) +{ + timeBase = (enet_uint32) timeGetTime () - newTimeBase; +} + +int +enet_address_set_host (ENetAddress * address, const char * name) +{ + struct hostent * hostEntry; + + hostEntry = gethostbyname (name); + if (hostEntry == NULL || + hostEntry -> h_addrtype != AF_INET) + { + unsigned long host = inet_addr (name); + if (host == INADDR_NONE) + return -1; + address -> host = host; + return 0; + } + + address -> host = * (enet_uint32 *) hostEntry -> h_addr_list [0]; + + return 0; +} + +int +enet_address_get_host_ip (const ENetAddress * address, char * name, size_t nameLength) +{ + char * addr = inet_ntoa (* (struct in_addr *) & address -> host); + if (addr == NULL) + return -1; + strncpy (name, addr, nameLength); + return 0; +} + +int +enet_address_get_host (const ENetAddress * address, char * name, size_t nameLength) +{ + struct in_addr in; + struct hostent * hostEntry; + + in.s_addr = address -> host; + + hostEntry = gethostbyaddr ((char *) & in, sizeof (struct in_addr), AF_INET); + if (hostEntry == NULL) + return enet_address_get_host_ip (address, name, nameLength); + + strncpy (name, hostEntry -> h_name, nameLength); + + return 0; +} + +int +enet_socket_bind (ENetSocket socket, const ENetAddress * address) +{ + struct sockaddr_in sin; + + memset (& sin, 0, sizeof (struct sockaddr_in)); + + sin.sin_family = AF_INET; + + if (address != NULL) + { + sin.sin_port = ENET_HOST_TO_NET_16 (address -> port); + sin.sin_addr.s_addr = address -> host; + } + else + { + sin.sin_port = 0; + sin.sin_addr.s_addr = INADDR_ANY; + } + + return bind (socket, + (struct sockaddr *) & sin, + sizeof (struct sockaddr_in)) == SOCKET_ERROR ? -1 : 0; +} + +int +enet_socket_get_address (ENetSocket socket, ENetAddress * address) +{ + struct sockaddr_in sin; + int sinLength = sizeof (struct sockaddr_in); + + if (getsockname (socket, (struct sockaddr *) & sin, & sinLength) == -1) + return -1; + + address -> host = (enet_uint32) sin.sin_addr.s_addr; + address -> port = ENET_NET_TO_HOST_16 (sin.sin_port); + + return 0; +} + +int +enet_socket_listen (ENetSocket socket, int backlog) +{ + return listen (socket, backlog < 0 ? SOMAXCONN : backlog) == SOCKET_ERROR ? -1 : 0; +} + +ENetSocket +enet_socket_create (ENetSocketType type) +{ + return socket (PF_INET, type == ENET_SOCKET_TYPE_DATAGRAM ? SOCK_DGRAM : SOCK_STREAM, 0); +} + +int +enet_socket_set_option (ENetSocket socket, ENetSocketOption option, int value) +{ + int result = SOCKET_ERROR; + switch (option) + { + case ENET_SOCKOPT_NONBLOCK: + { + u_long nonBlocking = (u_long) value; + result = ioctlsocket (socket, FIONBIO, & nonBlocking); + break; + } + + case ENET_SOCKOPT_BROADCAST: + result = setsockopt (socket, SOL_SOCKET, SO_BROADCAST, (char *) & value, sizeof (int)); + break; + + case ENET_SOCKOPT_REUSEADDR: + result = setsockopt (socket, SOL_SOCKET, SO_REUSEADDR, (char *) & value, sizeof (int)); + break; + + case ENET_SOCKOPT_RCVBUF: + result = setsockopt (socket, SOL_SOCKET, SO_RCVBUF, (char *) & value, sizeof (int)); + break; + + case ENET_SOCKOPT_SNDBUF: + result = setsockopt (socket, SOL_SOCKET, SO_SNDBUF, (char *) & value, sizeof (int)); + break; + + case ENET_SOCKOPT_RCVTIMEO: + result = setsockopt (socket, SOL_SOCKET, SO_RCVTIMEO, (char *) & value, sizeof (int)); + break; + + case ENET_SOCKOPT_SNDTIMEO: + result = setsockopt (socket, SOL_SOCKET, SO_SNDTIMEO, (char *) & value, sizeof (int)); + break; + + case ENET_SOCKOPT_NODELAY: + result = setsockopt (socket, IPPROTO_TCP, TCP_NODELAY, (char *) & value, sizeof (int)); + break; + + default: + break; + } + return result == SOCKET_ERROR ? -1 : 0; +} + +int +enet_socket_get_option (ENetSocket socket, ENetSocketOption option, int * value) +{ + int result = SOCKET_ERROR, len; + switch (option) + { + case ENET_SOCKOPT_ERROR: + len = sizeof(int); + result = getsockopt (socket, SOL_SOCKET, SO_ERROR, (char *) value, & len); + break; + + default: + break; + } + return result == SOCKET_ERROR ? -1 : 0; +} + +int +enet_socket_connect (ENetSocket socket, const ENetAddress * address) +{ + struct sockaddr_in sin; + int result; + + memset (& sin, 0, sizeof (struct sockaddr_in)); + + sin.sin_family = AF_INET; + sin.sin_port = ENET_HOST_TO_NET_16 (address -> port); + sin.sin_addr.s_addr = address -> host; + + result = connect (socket, (struct sockaddr *) & sin, sizeof (struct sockaddr_in)); + if (result == SOCKET_ERROR && WSAGetLastError () != WSAEWOULDBLOCK) + return -1; + + return 0; +} + +ENetSocket +enet_socket_accept (ENetSocket socket, ENetAddress * address) +{ + SOCKET result; + struct sockaddr_in sin; + int sinLength = sizeof (struct sockaddr_in); + + result = accept (socket, + address != NULL ? (struct sockaddr *) & sin : NULL, + address != NULL ? & sinLength : NULL); + + if (result == INVALID_SOCKET) + return ENET_SOCKET_NULL; + + if (address != NULL) + { + address -> host = (enet_uint32) sin.sin_addr.s_addr; + address -> port = ENET_NET_TO_HOST_16 (sin.sin_port); + } + + return result; +} + +int +enet_socket_shutdown (ENetSocket socket, ENetSocketShutdown how) +{ + return shutdown (socket, (int) how) == SOCKET_ERROR ? -1 : 0; +} + +void +enet_socket_destroy (ENetSocket socket) +{ + if (socket != INVALID_SOCKET) + closesocket (socket); +} + +int +enet_socket_send (ENetSocket socket, + const ENetAddress * address, + const ENetBuffer * buffers, + size_t bufferCount) +{ + struct sockaddr_in sin; + DWORD sentLength; + + if (address != NULL) + { + memset (& sin, 0, sizeof (struct sockaddr_in)); + + sin.sin_family = AF_INET; + sin.sin_port = ENET_HOST_TO_NET_16 (address -> port); + sin.sin_addr.s_addr = address -> host; + } + + if (WSASendTo (socket, + (LPWSABUF) buffers, + (DWORD) bufferCount, + & sentLength, + 0, + address != NULL ? (struct sockaddr *) & sin : NULL, + address != NULL ? sizeof (struct sockaddr_in) : 0, + NULL, + NULL) == SOCKET_ERROR) + { + if (WSAGetLastError () == WSAEWOULDBLOCK) + return 0; + + return -1; + } + + return (int) sentLength; +} + +int +enet_socket_receive (ENetSocket socket, + ENetAddress * address, + ENetBuffer * buffers, + size_t bufferCount) +{ + INT sinLength = sizeof (struct sockaddr_in); + DWORD flags = 0, + recvLength; + struct sockaddr_in sin; + + if (WSARecvFrom (socket, + (LPWSABUF) buffers, + (DWORD) bufferCount, + & recvLength, + & flags, + address != NULL ? (struct sockaddr *) & sin : NULL, + address != NULL ? & sinLength : NULL, + NULL, + NULL) == SOCKET_ERROR) + { + switch (WSAGetLastError ()) + { + case WSAEWOULDBLOCK: + case WSAECONNRESET: + return 0; + } + + return -1; + } + + if (flags & MSG_PARTIAL) + return -1; + + if (address != NULL) + { + address -> host = (enet_uint32) sin.sin_addr.s_addr; + address -> port = ENET_NET_TO_HOST_16 (sin.sin_port); + } + + return (int) recvLength; +} + +int +enet_socketset_select (ENetSocket maxSocket, ENetSocketSet * readSet, ENetSocketSet * writeSet, enet_uint32 timeout) +{ + struct timeval timeVal; + + timeVal.tv_sec = timeout / 1000; + timeVal.tv_usec = (timeout % 1000) * 1000; + + return select (maxSocket + 1, readSet, writeSet, NULL, & timeVal); +} + +int +enet_socket_wait (ENetSocket socket, enet_uint32 * condition, enet_uint32 timeout) +{ + fd_set readSet, writeSet; + struct timeval timeVal; + int selectCount; + + timeVal.tv_sec = timeout / 1000; + timeVal.tv_usec = (timeout % 1000) * 1000; + + FD_ZERO (& readSet); + FD_ZERO (& writeSet); + + if (* condition & ENET_SOCKET_WAIT_SEND) + FD_SET (socket, & writeSet); + + if (* condition & ENET_SOCKET_WAIT_RECEIVE) + FD_SET (socket, & readSet); + + selectCount = select (socket + 1, & readSet, & writeSet, NULL, & timeVal); + + if (selectCount < 0) + return -1; + + * condition = ENET_SOCKET_WAIT_NONE; + + if (selectCount == 0) + return 0; + + if (FD_ISSET (socket, & writeSet)) + * condition |= ENET_SOCKET_WAIT_SEND; + + if (FD_ISSET (socket, & readSet)) + * condition |= ENET_SOCKET_WAIT_RECEIVE; + + return 0; +} + +#endif + diff --git a/Externals/enet/win32.h b/Externals/enet/win32.h new file mode 100644 index 000000000000..40fb803237db --- /dev/null +++ b/Externals/enet/win32.h @@ -0,0 +1,57 @@ +/** + @file win32.h + @brief ENet Win32 header +*/ +#ifndef __ENET_WIN32_H__ +#define __ENET_WIN32_H__ + +#ifdef _MSC_VER +#ifdef ENET_BUILDING_LIB +#pragma warning (disable: 4996) // 'strncpy' was declared deprecated +#pragma warning (disable: 4267) // size_t to int conversion +#pragma warning (disable: 4244) // 64bit to 32bit int +#pragma warning (disable: 4018) // signed/unsigned mismatch +#endif +#endif + +#include +#include + +typedef SOCKET ENetSocket; + +#define ENET_SOCKET_NULL INVALID_SOCKET + +#define ENET_HOST_TO_NET_16(value) (htons (value)) +#define ENET_HOST_TO_NET_32(value) (htonl (value)) + +#define ENET_NET_TO_HOST_16(value) (ntohs (value)) +#define ENET_NET_TO_HOST_32(value) (ntohl (value)) + +typedef struct +{ + size_t dataLength; + void * data; +} ENetBuffer; + +#define ENET_CALLBACK __cdecl + +#ifdef ENET_DLL +#ifdef ENET_BUILDING_LIB +#define ENET_API __declspec( dllexport ) +#else +#define ENET_API __declspec( dllimport ) +#endif /* ENET_BUILDING_LIB */ +#else /* !ENET_DLL */ +#define ENET_API extern +#endif /* ENET_DLL */ + +typedef fd_set ENetSocketSet; + +#define ENET_SOCKETSET_EMPTY(sockset) FD_ZERO (& (sockset)) +#define ENET_SOCKETSET_ADD(sockset, socket) FD_SET (socket, & (sockset)) +#define ENET_SOCKETSET_REMOVE(sockset, socket) FD_CLEAR (socket, & (sockset)) +#define ENET_SOCKETSET_CHECK(sockset, socket) FD_ISSET (socket, & (sockset)) + +#endif /* __ENET_WIN32_H__ */ + + diff --git a/Externals/miniupnpc/CMakeLists.txt b/Externals/miniupnpc/CMakeLists.txt deleted file mode 100644 index 404ef65b94c8..000000000000 --- a/Externals/miniupnpc/CMakeLists.txt +++ /dev/null @@ -1,37 +0,0 @@ -cmake_minimum_required(VERSION 2.6) - -project(miniupnpc C) -set(MINIUPNPC_VERSION 1.7) -set(MINIUPNPC_API_VERSION 9) - -if(UNIX) - add_definitions(-DMINIUPNPC_SET_SOCKET_TIMEOUT) - add_definitions(-D_BSD_SOURCE -D_POSIX_C_SOURCE=1) -elseif(WIN32) - add_definitions(-D_WIN32_WINNT=0x0501) - find_library(WINSOCK2_LIBRARY NAMES ws2_32 WS2_32 Ws2_32) - find_library(IPHLPAPI_LIBRARY NAMES iphlpapi) - set(LDLIBS ${WINSOCK2_LIBRARY} ${IPHLPAPI_LIBRARY} ${LDLIBS}) -endif() - -if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") - add_definitions(-DMACOSX -D_DARWIN_C_SOURCE) -endif() - -include_directories(include) - -set(SRCS src/igd_desc_parse.c - src/miniupnpc.c - src/minixml.c - src/minisoap.c - src/miniwget.c - src/upnpc.c - src/upnpcommands.c - src/upnpreplyparse.c - src/upnperrors.c - src/connecthostport.c - src/portlistingparse.c - src/receivedata.c) - -add_library(miniupnpc STATIC ${SRCS}) - diff --git a/Externals/miniupnpc/Changelog.txt b/Externals/miniupnpc/Changelog.txt deleted file mode 100644 index e3ced295d82f..000000000000 --- a/Externals/miniupnpc/Changelog.txt +++ /dev/null @@ -1,556 +0,0 @@ -$Id: Changelog.txt,v 1.185 2013/05/03 09:05:38 nanard Exp $ -miniUPnP client Changelog. - -2013/05/03: - Fix Solaris build thanks to Maciej Małecki - -2013/04/27: - Fix testminiwget.sh for BSD - -2013/03/23: - Fixed Makefile for *BSD - -2013/03/11: - Update Makefile to use JNAerator version 0.11 - -2013/02/11: - Fix testminiwget.sh for use with dash - Use $(DESTDIR) in Makefile - -VERSION 1.8 : released 2013/02/06 - -2012/10/16: - fix testminiwget with no IPv6 support - -2012/09/27: - Rename all include guards to not clash with C99 - (7.1.3 Reserved identifiers). - -2012/08/30: - Added -e option to upnpc program (set description for port mappings) - -2012/08/29: - Python 3 support (thanks to Christopher Foo) - -2012/08/11: - Fix a memory link in UPNP_GetValidIGD() - Try to handle scope id in link local IPv6 URL under MS Windows - -2012/07/20: - Disable HAS_IP_MREQN on DragonFly BSD - -2012/06/28: - GetUPNPUrls() now inserts scope into link-local IPv6 addresses - -2012/06/23: - More error return checks in upnpc.c - #define MINIUPNPC_GET_SRC_ADDR enables receivedata() to get scope_id - parseURL() now parses IPv6 addresses scope - new parameter for miniwget() : IPv6 address scope - increment API_VERSION to 9 - -2012/06/20: - fixed CMakeLists.txt - -2012/05/29 - Improvements in testminiwget.sh - -VERSION 1.7 : released 2012/05/24 - -2012/05/01: - Cleanup settings of CFLAGS in Makefile - Fix signed/unsigned integer comparaisons - -2012/04/20: - Allow to specify protocol with TCP or UDP for -A option - -2012/04/09: - Only try to fetch XML description once in UPNP_GetValidIGD() - Added -ansi flag to compilation, and fixed C++ comments to ANSI C comments. - -2012/04/05: - minor improvements to minihttptestserver.c - -2012/03/15: - upnperrors.c returns valid error string for unrecognized error codes - -2012/03/08: - make minihttptestserver listen on loopback interface instead of 0.0.0.0 - -2012/01/25: - Maven installation thanks to Alexey Kuznetsov - -2012/01/21: - Replace WIN32 macro by _WIN32 - -2012/01/19: - Fixes in java wrappers thanks to Alexey Kuznetsov : - https://github.com/axet/miniupnp/tree/fix-javatest/miniupnpc - Make and install .deb packages (python) thanks to Alexey Kuznetsov : - https://github.com/axet/miniupnp/tree/feature-debbuild/miniupnpc - -2012/01/07: - The multicast interface can now be specified by name with IPv4. - -2012/01/02: - Install man page - -2011/11/25: - added header to Port Mappings list in upnpc.c - -2011/10/09: - Makefile : make clean now removes jnaerator generated files. - MINIUPNPC_VERSION in miniupnpc.h (updated by make) - -2011/09/12: - added rootdescURL to UPNPUrls structure. - -VERSION 1.6 : released 2011/07/25 - -2011/07/25: - Update doc for version 1.6 release - -2011/06/18: - Fix for windows in miniwget.c - -2011/06/04: - display remote host in port mapping listing - -2011/06/03: - Fix in make install : there were missing headers - -2011/05/26: - Fix the socket leak in miniwget thanks to Richard Marsh. - Permit to add leaseduration in -a command. Display lease duration. - -2011/05/15: - Try both LinkLocal and SiteLocal multicast address for SSDP in IPv6 - -2011/05/09: - add a test in testminiwget.sh. - more error checking in miniwget.c - -2011/05/06: - Adding some tool to test and validate miniwget.c - simplified and debugged miniwget.c - -2011/04/11: - moving ReceiveData() to a receivedata.c file. - parsing presentation url - adding IGD v2 WANIPv6FirewallControl commands - -2011/04/10: - update of miniupnpcmodule.c - comments in miniwget.c, update in testminiwget - Adding errors codes from IGD v2 - new functions in upnpc.c for IGD v2 - -2011/04/09: - Support for litteral ip v6 address in miniwget - -2011/04/08: - Adding support for urn:schemas-upnp-org:service:WANIPv6FirewallControl:1 - Updating APIVERSION - Supporting IPV6 in upnpDiscover() - Adding a -6 option to upnpc command line tool - -2011/03/18: - miniwget/parseURL() : return an error when url param is null. - fixing GetListOfPortMappings() - -2011/03/14: - upnpDiscover() now reporting an error code. - improvements in comments. - -2011/03/11: - adding miniupnpcstrings.h.cmake and CMakeLists.txt files. - -2011/02/15: - Implementation of GetListOfPortMappings() - -2011/02/07: - updates to minixml to support character data starting with spaces - minixml now support CDATA - upnpreplyparse treats specificaly - change in simpleUPnPcommand to return the buffer (simplification) - -2011/02/06: - Added leaseDuration argument to AddPortMapping() - Starting to implement GetListOfPortMappings() - -2011/01/11: - updating wingenminiupnpcstrings.c - -2011/01/04: - improving updateminiupnpcstrings.sh - -VERSION 1.5 : released 2011/01/01 - -2010/12/21: - use NO_GETADDRINFO macro to disable the use of getaddrinfo/freeaddrinfo - -2010/12/11: - Improvements on getHTTPResponse() code. - -2010/12/09: - new code for miniwget that handle Chunked transfer encoding - using getHTTPResponse() in SOAP call code - Adding MANIFEST.in for 'python setup.py bdist_rpm' - -2010/11/25: - changes to minissdpc.c to compile under Win32. - see http://miniupnp.tuxfamily.org/forum/viewtopic.php?t=729 - -2010/09/17: - Various improvement to Makefile from Michał GĂłrny - -2010/08/05: - Adding the script "external-ip.sh" from Reuben Hawkins - -2010/06/09: - update to python module to match modification made on 2010/04/05 - update to Java test code to match modification made on 2010/04/05 - all UPNP_* function now return an error if the SOAP request failed - at HTTP level. - -2010/04/17: - Using GetBestRoute() under win32 in order to find the - right interface to use. - -2010/04/12: - Retrying with HTTP/1.1 if HTTP/1.0 failed. see - http://miniupnp.tuxfamily.org/forum/viewtopic.php?p=1703 - -2010/04/07: - avoid returning duplicates in upnpDiscover() - -2010/04/05: - Create a connecthostport.h/.c with connecthostport() function - and use it in miniwget and miniupnpc. - Use getnameinfo() instead of inet_ntop or inet_ntoa - Work to make miniupnpc IPV6 compatible... - Add java test code. - Big changes in order to support device having both WANIPConnection - and WANPPPConnection. - -2010/04/04: - Use getaddrinfo() instead of gethostbyname() in miniwget. - -2010/01/06: - #define _DARWIN_C_SOURCE for Mac OS X - -2009/12/19: - Improve MinGW32 build - -2009/12/11: - adding a MSVC9 project to build the static library and executable - -2009/12/10: - Fixing some compilation stuff for Windows/MinGW - -2009/12/07: - adaptations in Makefile and updateminiupnpcstring.sh for AmigaOS - some fixes for Windows when using virtual ethernet adapters (it is the - case with VMWare installed). - -2009/12/04: - some fixes for AmigaOS compilation - Changed HTTP version to HTTP/1.0 for Soap too (to prevent chunked - transfer encoding) - -2009/12/03: - updating printIDG and testigddescparse.c for debug. - modifications to compile under AmigaOS - adding a testminiwget program - Changed miniwget to advertise itself as HTTP/1.0 to prevent chunked - transfer encoding - -2009/11/26: - fixing updateminiupnpcstrings.sh to take into account - which command that does not return an error code. - -VERSION 1.4 : released 2009/10/30 - -2009/10/16: - using Py_BEGIN_ALLOW_THREADS and Py_END_ALLOW_THREADS in python module. - -2009/10/10: - Some fixes for compilation under Solaris - compilation fixes : http://miniupnp.tuxfamily.org/forum/viewtopic.php?p=1464 - -2009/09/21: - fixing the code to ignore EINTR during connect() calls. - -2009/08/07: - Set socket timeout for connect() - Some cleanup in miniwget.c - -2009/08/04: - remove multiple redirections with -d in upnpc.c - Print textual error code in upnpc.c - Ignore EINTR during the connect() and poll() calls. - -2009/07/29: - fix in updateminiupnpcstrings.sh if OS name contains "/" - Sending a correct value for MX: field in SSDP request - -2009/07/20: - Change the Makefile to compile under Mac OS X - Fixed a stackoverflow in getDevicesFromMiniSSDPD() - -2009/07/09: - Compile under Haiku - generate miniupnpcstrings.h.in from miniupnpcstrings.h - -2009/06/04: - patching to compile under CygWin and cross compile for minGW - -VERSION 1.3 : - -2009/04/17: - updating python module - Use strtoull() when using C99 - -2009/02/28: - Fixed miniwget.c for compiling under sun - -2008/12/18: - cleanup in Makefile (thanks to Paul de Weerd) - minissdpc.c : win32 compatibility - miniupnpc.c : changed xmlns prefix from 'm' to 'u' - Removed NDEBUG (using DEBUG) - -2008/10/14: - Added the ExternalHost argument to DeletePortMapping() - -2008/10/11: - Added the ExternalHost argument to AddPortMapping() - Put a correct User-Agent: header in HTTP requests. - -VERSION 1.2 : - -2008/10/07: - Update docs - -2008/09/25: - Integrated sameport patch from Dario Meloni : Added a "sameport" - argument to upnpDiscover(). - -2008/07/18: - small modif to make Clang happy :) - -2008/07/17: - #define SOAPPREFIX "s" in miniupnpc.c in order to remove SOAP-ENV... - -2008/07/14: - include declspec.h in installation (to /usr/include/miniupnpc) - -VERSION 1.1 : - -2008/07/04: - standard options for install/ln instead of gnu-specific stuff. - -2008/07/03: - now builds a .dll and .lib with win32. (mingw32) - -2008/04/28: - make install now install the binary of the upnpc tool - -2008/04/27: - added testupnpigd.py - added error strings for miniupnpc "internal" errors - improved python module error/exception reporting. - -2008/04/23: - Completely rewrite igd_desc_parse.c in order to be compatible with - Linksys WAG200G - Added testigddescparse - updated python module - -VERSION 1.0 : - -2008/02/21: - put some #ifdef DEBUG around DisplayNameValueList() - -2008/02/18: - Improved error reporting in upnpcommands.c - UPNP_GetStatusInfo() returns LastConnectionError - -2008/02/16: - better error handling in minisoap.c - improving display of "valid IGD found" in upnpc.c - -2008/02/03: - Fixing UPNP_GetValidIGD() - improved make install :) - -2007/12/22: - Adding upnperrors.c/h to provide a strupnperror() function - used to translate UPnP error codes to string. - -2007/12/19: - Fixing getDevicesFromMiniSSDPD() - improved error reporting of UPnP functions - -2007/12/18: - It is now possible to specify a different location for MiniSSDPd socket. - working with MiniSSDPd is now more efficient. - python module improved. - -2007/12/16: - improving error reporting - -2007/12/13: - Try to improve compatibility by using HTTP/1.0 instead of 1.1 and - XML a bit different for SOAP. - -2007/11/25: - fixed select() call for linux - -2007/11/15: - Added -fPIC to CFLAG for better shared library code. - -2007/11/02: - Fixed a potential socket leak in miniwget2() - -2007/10/16: - added a parameter to upnpDiscover() in order to allow the use of another - interface than the default multicast interface. - -2007/10/12: - Fixed the creation of symbolic link in Makefile - -2007/10/08: - Added man page - -2007/10/02: - fixed memory bug in GetUPNPUrls() - -2007/10/01: - fixes in the Makefile - Added UPNP_GetIGDFromUrl() and adapted the sample program accordingly. - Added SONAME in the shared library to please debian :) - fixed MS Windows compilation (minissdpd is not available under MS Windows). - -2007/09/25: - small change to Makefile to be able to install in a different location - (default is /usr) - -2007/09/24: - now compiling both shared and static library - -2007/09/19: - Cosmetic changes on upnpc.c - -2007/09/02: - adapting to new miniSSDPd (release version ?) - -2007/08/31: - Usage of miniSSDPd to skip discovery process. - -2007/08/27: - fixed python module to allow compilation with Python older than Python 2.4 - -2007/06/12: - Added a python module. - -2007/05/19: - Fixed compilation under MinGW - -2007/05/15: - fixed a memory leak in AddPortMapping() - Added testupnpreplyparse executable to check the parsing of - upnp soap messages - minixml now ignore namespace prefixes. - -2007/04/26: - upnpc now displays external ip address with -s or -l - -2007/04/11: - changed MINIUPNPC_URL_MAXSIZE to 128 to accomodate the "BT Voyager 210" - -2007/03/19: - cleanup in miniwget.c - -2007/03/01: - Small typo fix... - -2007/01/30: - Now parsing the HTTP header from SOAP responses in order to - get content-length value. - -2007/01/29: - Fixed the Soap Query to speedup the HTTP request. - added some Win32 DLL stuff... - -2007/01/27: - Fixed some WIN32 compatibility issues - -2006/12/14: - Added UPNPIGD_IsConnected() function in miniupnp.c/.h - Added UPNP_GetValidIGD() in miniupnp.c/.h - cleaned upnpc.c main(). now using UPNP_GetValidIGD() - -2006/12/07: - Version 1.0-RC1 released - -2006/12/03: - Minor changes to compile under SunOS/Solaris - -2006/11/30: - made a minixml parser validator program - updated minixml to handle attributes correctly - -2006/11/22: - Added a -r option to the upnpc sample thanks to Alexander Hubmann. - -2006/11/19: - Cleanup code to make it more ANSI C compliant - -2006/11/10: - detect and display local lan address. - -2006/11/04: - Packets and Bytes Sent/Received are now unsigned int. - -2006/11/01: - Bug fix thanks to Giuseppe D'Angelo - -2006/10/31: - C++ compatibility for .h files. - Added a way to get ip Address on the LAN used to reach the IGD. - -2006/10/25: - Added M-SEARCH to the services in the discovery process. - -2006/10/22: - updated the Makefile to use makedepend, added a "make install" - update Makefile - -2006/10/20: - fixing the description url parsing thanks to patch sent by - Wayne Dawe. - Fixed/translated some comments. - Implemented a better discover process, first looking - for IGD then for root devices (as some devices only reply to - M-SEARCH for root devices). - -2006/09/02: - added freeUPNPDevlist() function. - -2006/08/04: - More command line arguments checking - -2006/08/01: - Added the .bat file to compile under Win32 with minGW32 - -2006/07/31: - Fixed the rootdesc parser (igd_desc_parse.c) - -2006/07/20: - parseMSEARCHReply() is now returning the ST: line as well - starting changes to detect several UPnP devices on the network - -2006/07/19: - using GetCommonLinkProperties to get down/upload bitrate - diff --git a/Externals/miniupnpc/LICENSE b/Externals/miniupnpc/LICENSE deleted file mode 100644 index ac89a7516a36..000000000000 --- a/Externals/miniupnpc/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -MiniUPnPc -Copyright (c) 2005-2011, Thomas BERNARD -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - diff --git a/Externals/miniupnpc/README b/Externals/miniupnpc/README deleted file mode 100644 index b23478de9756..000000000000 --- a/Externals/miniupnpc/README +++ /dev/null @@ -1,66 +0,0 @@ -Project: miniupnp -Project web page: http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ -github: https://github.com/miniupnp/miniupnp -freecode: http://freecode.com/projects/miniupnp -Author: Thomas Bernard -Copyright (c) 2005-2012 Thomas Bernard -This software is subject to the conditions detailed in the -LICENSE file provided within this distribution. - - -For the comfort of Win32 users, bsdqueue.h is included in the distribution. -Its licence is included in the header of the file. -bsdqueue.h is a copy of the sys/queue.h of an OpenBSD system. - - -* miniUPnP Client - miniUPnPc * - -To compile, simply run 'gmake' (could be 'make' on your system). -Under win32, to compile with MinGW, type "mingw32make.bat". -MS Visual C solution and project files are supplied in the msvc/ subdirectory. - -The compilation is known to work under linux, FreeBSD, -OpenBSD, MacOS X, AmigaOS and cygwin. -The official AmigaOS4.1 SDK was used for AmigaOS4 and GeekGadgets for AmigaOS3. -upx (http://upx.sourceforge.net) is used to compress the win32 .exe files. - -To install the library and headers on the system use : -> su -> make install -> exit - -alternatively, to install into a specific location, use : -> INSTALLPREFIX=/usr/local make install - -upnpc.c is a sample client using the libminiupnpc. -To use the libminiupnpc in your application, link it with -libminiupnpc.a (or .so) and use the following functions found in miniupnpc.h, -upnpcommands.h and miniwget.h : -- upnpDiscover() -- miniwget() -- parserootdesc() -- GetUPNPUrls() -- UPNP_* (calling UPNP methods) - -Note : use #include etc... for the includes -and -lminiupnpc for the link - -Discovery process is speeded up when MiniSSDPd is running on the machine. - - -* Python module * - -you can build a python module with 'make pythonmodule' -and install it with 'make installpythonmodule'. -setup.py (and setupmingw32.py) are included in the distribution. - - -Feel free to contact me if you have any problem : -e-mail : miniupnp@free.fr - -If you are using libminiupnpc in your application, please -send me an email ! - -For any question, you can use the web forum : -http://miniupnp.tuxfamily.org/forum/ - diff --git a/Externals/miniupnpc/VERSION b/Externals/miniupnpc/VERSION deleted file mode 100644 index 6259340971be..000000000000 --- a/Externals/miniupnpc/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.8 diff --git a/Externals/miniupnpc/apiversions.txt b/Externals/miniupnpc/apiversions.txt deleted file mode 100644 index bec2941be38f..000000000000 --- a/Externals/miniupnpc/apiversions.txt +++ /dev/null @@ -1,117 +0,0 @@ -$Id: apiversions.txt,v 1.2 2013/03/29 14:45:09 nanard Exp $ - -Differences in API between miniUPnPc versions - - -====================== miniUPnPc version 1.8 ====================== -API version 9 - -miniupnpc.h: - updated macros : - #define MINIUPNPC_VERSION "1.8" - #define MINIUPNPC_API_VERSION 9 - added "unsigned int scope_id;" to struct UPNPDev - added scope_id argument to GetUPNPUrls() - - - -====================== miniUPnPc version 1.7 ====================== -API version 8 - -miniupnpc.h : - add new macros : - #define MINIUPNPC_VERSION "1.7" - #define MINIUPNPC_API_VERSION 8 - add rootdescURL to struct UPNPUrls - - - -====================== miniUPnPc version 1.6 ====================== -API version 8 - -Adding support for IPv6. -igd_desc_parse.h : - struct IGDdatas_service : - add char presentationurl[MINIUPNPC_URL_MAXSIZE]; - struct IGDdatas : - add struct IGDdatas_service IPv6FC; -miniupnpc.h : - new macros : - #define UPNPDISCOVER_SUCCESS (0) - #define UPNPDISCOVER_UNKNOWN_ERROR (-1) - #define UPNPDISCOVER_SOCKET_ERROR (-101) - #define UPNPDISCOVER_MEMORY_ERROR (-102) - simpleUPnPcommand() prototype changed (but is normaly not used by API users) - add arguments ipv6 and error to upnpDiscover() : - struct UPNPDev * - upnpDiscover(int delay, const char * multicastif, - const char * minissdpdsock, int sameport, - int ipv6, - int * error); - add controlURL_6FC member to struct UPNPUrls : - struct UPNPUrls { - char * controlURL; - char * ipcondescURL; - char * controlURL_CIF; - char * controlURL_6FC; - }; - -upnpcommands.h : - add leaseDuration argument to UPNP_AddPortMapping() - add desc, enabled and leaseDuration arguments to UPNP_GetSpecificPortMappingEntry() - add UPNP_GetListOfPortMappings() function (IGDv2) - add IGDv2 IPv6 related functions : - UPNP_GetFirewallStatus() - UPNP_GetOutboundPinholeTimeout() - UPNP_AddPinhole() - UPNP_UpdatePinhole() - UPNP_DeletePinhole() - UPNP_CheckPinholeWorking() - UPNP_GetPinholePackets() - - - -====================== miniUPnPc version 1.5 ====================== -API version 5 - -new function : -int UPNPIGD_IsConnected(struct UPNPUrls *, struct IGDdatas *); -new macro in upnpcommands.h : -#define UPNPCOMMAND_HTTP_ERROR - -====================== miniUPnPc version 1.4 ====================== -Same API as version 1.3 - -====================== miniUPnPc version 1.3 ====================== -API version 4 - -Use UNSIGNED_INTEGER type for -UPNP_GetTotalBytesSent(), UPNP_GetTotalBytesReceived(), -UPNP_GetTotalPacketsSent(), UPNP_GetTotalPacketsReceived() -Add remoteHost argument to UPNP_AddPortMapping() and UPNP_DeletePortMapping() - -====================== miniUPnPc version 1.2 ====================== -API version 3 - -added sameport argument to upnpDiscover() -struct UPNPDev * -upnpDiscover(int delay, const char * multicastif, - const char * minissdpdsock, int sameport); - -====================== miniUPnPc Version 1.1 ====================== -Same API as 1.0 - - -====================== miniUPnPc Version 1.0 ====================== -API version 2 - - -struct UPNPDev { - struct UPNPDev * pNext; - char * descURL; - char * st; - char buffer[2]; -}; -struct UPNPDev * upnpDiscover(int delay, const char * multicastif, - const char * minissdpdsock); - diff --git a/Externals/miniupnpc/miniupnpc.vcxproj.filters b/Externals/miniupnpc/miniupnpc.vcxproj.filters deleted file mode 100644 index 1b4bfa312a4e..000000000000 --- a/Externals/miniupnpc/miniupnpc.vcxproj.filters +++ /dev/null @@ -1,108 +0,0 @@ -ďťż - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hpp;hxx;hm;inl;inc;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - \ No newline at end of file diff --git a/Externals/miniupnpc/src/bsdqueue.h b/Externals/miniupnpc/src/bsdqueue.h deleted file mode 100644 index c6afe1f7c445..000000000000 --- a/Externals/miniupnpc/src/bsdqueue.h +++ /dev/null @@ -1,531 +0,0 @@ -/* $OpenBSD: queue.h,v 1.31 2005/11/25 08:06:25 otto Exp $ */ -/* $NetBSD: queue.h,v 1.11 1996/05/16 05:17:14 mycroft Exp $ */ - -/* - * Copyright (c) 1991, 1993 - * The Regents of the University of California. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the University nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY - * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - * - * @(#)queue.h 8.5 (Berkeley) 8/20/94 - */ - -#ifndef _SYS_QUEUE_H_ -#define _SYS_QUEUE_H_ - -/* - * This file defines five types of data structures: singly-linked lists, - * lists, simple queues, tail queues, and circular queues. - * - * - * A singly-linked list is headed by a single forward pointer. The elements - * are singly linked for minimum space and pointer manipulation overhead at - * the expense of O(n) removal for arbitrary elements. New elements can be - * added to the list after an existing element or at the head of the list. - * Elements being removed from the head of the list should use the explicit - * macro for this purpose for optimum efficiency. A singly-linked list may - * only be traversed in the forward direction. Singly-linked lists are ideal - * for applications with large datasets and few or no removals or for - * implementing a LIFO queue. - * - * A list is headed by a single forward pointer (or an array of forward - * pointers for a hash table header). The elements are doubly linked - * so that an arbitrary element can be removed without a need to - * traverse the list. New elements can be added to the list before - * or after an existing element or at the head of the list. A list - * may only be traversed in the forward direction. - * - * A simple queue is headed by a pair of pointers, one the head of the - * list and the other to the tail of the list. The elements are singly - * linked to save space, so elements can only be removed from the - * head of the list. New elements can be added to the list before or after - * an existing element, at the head of the list, or at the end of the - * list. A simple queue may only be traversed in the forward direction. - * - * A tail queue is headed by a pair of pointers, one to the head of the - * list and the other to the tail of the list. The elements are doubly - * linked so that an arbitrary element can be removed without a need to - * traverse the list. New elements can be added to the list before or - * after an existing element, at the head of the list, or at the end of - * the list. A tail queue may be traversed in either direction. - * - * A circle queue is headed by a pair of pointers, one to the head of the - * list and the other to the tail of the list. The elements are doubly - * linked so that an arbitrary element can be removed without a need to - * traverse the list. New elements can be added to the list before or after - * an existing element, at the head of the list, or at the end of the list. - * A circle queue may be traversed in either direction, but has a more - * complex end of list detection. - * - * For details on the use of these macros, see the queue(3) manual page. - */ - -#ifdef QUEUE_MACRO_DEBUG -#define _Q_INVALIDATE(a) (a) = ((void *)-1) -#else -#define _Q_INVALIDATE(a) -#endif - -/* - * Singly-linked List definitions. - */ -#define SLIST_HEAD(name, type) \ -struct name { \ - struct type *slh_first; /* first element */ \ -} - -#define SLIST_HEAD_INITIALIZER(head) \ - { NULL } - -#ifdef SLIST_ENTRY -#undef SLIST_ENTRY -#endif - -#define SLIST_ENTRY(type) \ -struct { \ - struct type *sle_next; /* next element */ \ -} - -/* - * Singly-linked List access methods. - */ -#define SLIST_FIRST(head) ((head)->slh_first) -#define SLIST_END(head) NULL -#define SLIST_EMPTY(head) (SLIST_FIRST(head) == SLIST_END(head)) -#define SLIST_NEXT(elm, field) ((elm)->field.sle_next) - -#define SLIST_FOREACH(var, head, field) \ - for((var) = SLIST_FIRST(head); \ - (var) != SLIST_END(head); \ - (var) = SLIST_NEXT(var, field)) - -#define SLIST_FOREACH_PREVPTR(var, varp, head, field) \ - for ((varp) = &SLIST_FIRST((head)); \ - ((var) = *(varp)) != SLIST_END(head); \ - (varp) = &SLIST_NEXT((var), field)) - -/* - * Singly-linked List functions. - */ -#define SLIST_INIT(head) { \ - SLIST_FIRST(head) = SLIST_END(head); \ -} - -#define SLIST_INSERT_AFTER(slistelm, elm, field) do { \ - (elm)->field.sle_next = (slistelm)->field.sle_next; \ - (slistelm)->field.sle_next = (elm); \ -} while (0) - -#define SLIST_INSERT_HEAD(head, elm, field) do { \ - (elm)->field.sle_next = (head)->slh_first; \ - (head)->slh_first = (elm); \ -} while (0) - -#define SLIST_REMOVE_NEXT(head, elm, field) do { \ - (elm)->field.sle_next = (elm)->field.sle_next->field.sle_next; \ -} while (0) - -#define SLIST_REMOVE_HEAD(head, field) do { \ - (head)->slh_first = (head)->slh_first->field.sle_next; \ -} while (0) - -#define SLIST_REMOVE(head, elm, type, field) do { \ - if ((head)->slh_first == (elm)) { \ - SLIST_REMOVE_HEAD((head), field); \ - } else { \ - struct type *curelm = (head)->slh_first; \ - \ - while (curelm->field.sle_next != (elm)) \ - curelm = curelm->field.sle_next; \ - curelm->field.sle_next = \ - curelm->field.sle_next->field.sle_next; \ - _Q_INVALIDATE((elm)->field.sle_next); \ - } \ -} while (0) - -/* - * List definitions. - */ -#define LIST_HEAD(name, type) \ -struct name { \ - struct type *lh_first; /* first element */ \ -} - -#define LIST_HEAD_INITIALIZER(head) \ - { NULL } - -#define LIST_ENTRY(type) \ -struct { \ - struct type *le_next; /* next element */ \ - struct type **le_prev; /* address of previous next element */ \ -} - -/* - * List access methods - */ -#define LIST_FIRST(head) ((head)->lh_first) -#define LIST_END(head) NULL -#define LIST_EMPTY(head) (LIST_FIRST(head) == LIST_END(head)) -#define LIST_NEXT(elm, field) ((elm)->field.le_next) - -#define LIST_FOREACH(var, head, field) \ - for((var) = LIST_FIRST(head); \ - (var)!= LIST_END(head); \ - (var) = LIST_NEXT(var, field)) - -/* - * List functions. - */ -#define LIST_INIT(head) do { \ - LIST_FIRST(head) = LIST_END(head); \ -} while (0) - -#define LIST_INSERT_AFTER(listelm, elm, field) do { \ - if (((elm)->field.le_next = (listelm)->field.le_next) != NULL) \ - (listelm)->field.le_next->field.le_prev = \ - &(elm)->field.le_next; \ - (listelm)->field.le_next = (elm); \ - (elm)->field.le_prev = &(listelm)->field.le_next; \ -} while (0) - -#define LIST_INSERT_BEFORE(listelm, elm, field) do { \ - (elm)->field.le_prev = (listelm)->field.le_prev; \ - (elm)->field.le_next = (listelm); \ - *(listelm)->field.le_prev = (elm); \ - (listelm)->field.le_prev = &(elm)->field.le_next; \ -} while (0) - -#define LIST_INSERT_HEAD(head, elm, field) do { \ - if (((elm)->field.le_next = (head)->lh_first) != NULL) \ - (head)->lh_first->field.le_prev = &(elm)->field.le_next;\ - (head)->lh_first = (elm); \ - (elm)->field.le_prev = &(head)->lh_first; \ -} while (0) - -#define LIST_REMOVE(elm, field) do { \ - if ((elm)->field.le_next != NULL) \ - (elm)->field.le_next->field.le_prev = \ - (elm)->field.le_prev; \ - *(elm)->field.le_prev = (elm)->field.le_next; \ - _Q_INVALIDATE((elm)->field.le_prev); \ - _Q_INVALIDATE((elm)->field.le_next); \ -} while (0) - -#define LIST_REPLACE(elm, elm2, field) do { \ - if (((elm2)->field.le_next = (elm)->field.le_next) != NULL) \ - (elm2)->field.le_next->field.le_prev = \ - &(elm2)->field.le_next; \ - (elm2)->field.le_prev = (elm)->field.le_prev; \ - *(elm2)->field.le_prev = (elm2); \ - _Q_INVALIDATE((elm)->field.le_prev); \ - _Q_INVALIDATE((elm)->field.le_next); \ -} while (0) - -/* - * Simple queue definitions. - */ -#define SIMPLEQ_HEAD(name, type) \ -struct name { \ - struct type *sqh_first; /* first element */ \ - struct type **sqh_last; /* addr of last next element */ \ -} - -#define SIMPLEQ_HEAD_INITIALIZER(head) \ - { NULL, &(head).sqh_first } - -#define SIMPLEQ_ENTRY(type) \ -struct { \ - struct type *sqe_next; /* next element */ \ -} - -/* - * Simple queue access methods. - */ -#define SIMPLEQ_FIRST(head) ((head)->sqh_first) -#define SIMPLEQ_END(head) NULL -#define SIMPLEQ_EMPTY(head) (SIMPLEQ_FIRST(head) == SIMPLEQ_END(head)) -#define SIMPLEQ_NEXT(elm, field) ((elm)->field.sqe_next) - -#define SIMPLEQ_FOREACH(var, head, field) \ - for((var) = SIMPLEQ_FIRST(head); \ - (var) != SIMPLEQ_END(head); \ - (var) = SIMPLEQ_NEXT(var, field)) - -/* - * Simple queue functions. - */ -#define SIMPLEQ_INIT(head) do { \ - (head)->sqh_first = NULL; \ - (head)->sqh_last = &(head)->sqh_first; \ -} while (0) - -#define SIMPLEQ_INSERT_HEAD(head, elm, field) do { \ - if (((elm)->field.sqe_next = (head)->sqh_first) == NULL) \ - (head)->sqh_last = &(elm)->field.sqe_next; \ - (head)->sqh_first = (elm); \ -} while (0) - -#define SIMPLEQ_INSERT_TAIL(head, elm, field) do { \ - (elm)->field.sqe_next = NULL; \ - *(head)->sqh_last = (elm); \ - (head)->sqh_last = &(elm)->field.sqe_next; \ -} while (0) - -#define SIMPLEQ_INSERT_AFTER(head, listelm, elm, field) do { \ - if (((elm)->field.sqe_next = (listelm)->field.sqe_next) == NULL)\ - (head)->sqh_last = &(elm)->field.sqe_next; \ - (listelm)->field.sqe_next = (elm); \ -} while (0) - -#define SIMPLEQ_REMOVE_HEAD(head, field) do { \ - if (((head)->sqh_first = (head)->sqh_first->field.sqe_next) == NULL) \ - (head)->sqh_last = &(head)->sqh_first; \ -} while (0) - -/* - * Tail queue definitions. - */ -#define TAILQ_HEAD(name, type) \ -struct name { \ - struct type *tqh_first; /* first element */ \ - struct type **tqh_last; /* addr of last next element */ \ -} - -#define TAILQ_HEAD_INITIALIZER(head) \ - { NULL, &(head).tqh_first } - -#define TAILQ_ENTRY(type) \ -struct { \ - struct type *tqe_next; /* next element */ \ - struct type **tqe_prev; /* address of previous next element */ \ -} - -/* - * tail queue access methods - */ -#define TAILQ_FIRST(head) ((head)->tqh_first) -#define TAILQ_END(head) NULL -#define TAILQ_NEXT(elm, field) ((elm)->field.tqe_next) -#define TAILQ_LAST(head, headname) \ - (*(((struct headname *)((head)->tqh_last))->tqh_last)) -/* XXX */ -#define TAILQ_PREV(elm, headname, field) \ - (*(((struct headname *)((elm)->field.tqe_prev))->tqh_last)) -#define TAILQ_EMPTY(head) \ - (TAILQ_FIRST(head) == TAILQ_END(head)) - -#define TAILQ_FOREACH(var, head, field) \ - for((var) = TAILQ_FIRST(head); \ - (var) != TAILQ_END(head); \ - (var) = TAILQ_NEXT(var, field)) - -#define TAILQ_FOREACH_REVERSE(var, head, headname, field) \ - for((var) = TAILQ_LAST(head, headname); \ - (var) != TAILQ_END(head); \ - (var) = TAILQ_PREV(var, headname, field)) - -/* - * Tail queue functions. - */ -#define TAILQ_INIT(head) do { \ - (head)->tqh_first = NULL; \ - (head)->tqh_last = &(head)->tqh_first; \ -} while (0) - -#define TAILQ_INSERT_HEAD(head, elm, field) do { \ - if (((elm)->field.tqe_next = (head)->tqh_first) != NULL) \ - (head)->tqh_first->field.tqe_prev = \ - &(elm)->field.tqe_next; \ - else \ - (head)->tqh_last = &(elm)->field.tqe_next; \ - (head)->tqh_first = (elm); \ - (elm)->field.tqe_prev = &(head)->tqh_first; \ -} while (0) - -#define TAILQ_INSERT_TAIL(head, elm, field) do { \ - (elm)->field.tqe_next = NULL; \ - (elm)->field.tqe_prev = (head)->tqh_last; \ - *(head)->tqh_last = (elm); \ - (head)->tqh_last = &(elm)->field.tqe_next; \ -} while (0) - -#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do { \ - if (((elm)->field.tqe_next = (listelm)->field.tqe_next) != NULL)\ - (elm)->field.tqe_next->field.tqe_prev = \ - &(elm)->field.tqe_next; \ - else \ - (head)->tqh_last = &(elm)->field.tqe_next; \ - (listelm)->field.tqe_next = (elm); \ - (elm)->field.tqe_prev = &(listelm)->field.tqe_next; \ -} while (0) - -#define TAILQ_INSERT_BEFORE(listelm, elm, field) do { \ - (elm)->field.tqe_prev = (listelm)->field.tqe_prev; \ - (elm)->field.tqe_next = (listelm); \ - *(listelm)->field.tqe_prev = (elm); \ - (listelm)->field.tqe_prev = &(elm)->field.tqe_next; \ -} while (0) - -#define TAILQ_REMOVE(head, elm, field) do { \ - if (((elm)->field.tqe_next) != NULL) \ - (elm)->field.tqe_next->field.tqe_prev = \ - (elm)->field.tqe_prev; \ - else \ - (head)->tqh_last = (elm)->field.tqe_prev; \ - *(elm)->field.tqe_prev = (elm)->field.tqe_next; \ - _Q_INVALIDATE((elm)->field.tqe_prev); \ - _Q_INVALIDATE((elm)->field.tqe_next); \ -} while (0) - -#define TAILQ_REPLACE(head, elm, elm2, field) do { \ - if (((elm2)->field.tqe_next = (elm)->field.tqe_next) != NULL) \ - (elm2)->field.tqe_next->field.tqe_prev = \ - &(elm2)->field.tqe_next; \ - else \ - (head)->tqh_last = &(elm2)->field.tqe_next; \ - (elm2)->field.tqe_prev = (elm)->field.tqe_prev; \ - *(elm2)->field.tqe_prev = (elm2); \ - _Q_INVALIDATE((elm)->field.tqe_prev); \ - _Q_INVALIDATE((elm)->field.tqe_next); \ -} while (0) - -/* - * Circular queue definitions. - */ -#define CIRCLEQ_HEAD(name, type) \ -struct name { \ - struct type *cqh_first; /* first element */ \ - struct type *cqh_last; /* last element */ \ -} - -#define CIRCLEQ_HEAD_INITIALIZER(head) \ - { CIRCLEQ_END(&head), CIRCLEQ_END(&head) } - -#define CIRCLEQ_ENTRY(type) \ -struct { \ - struct type *cqe_next; /* next element */ \ - struct type *cqe_prev; /* previous element */ \ -} - -/* - * Circular queue access methods - */ -#define CIRCLEQ_FIRST(head) ((head)->cqh_first) -#define CIRCLEQ_LAST(head) ((head)->cqh_last) -#define CIRCLEQ_END(head) ((void *)(head)) -#define CIRCLEQ_NEXT(elm, field) ((elm)->field.cqe_next) -#define CIRCLEQ_PREV(elm, field) ((elm)->field.cqe_prev) -#define CIRCLEQ_EMPTY(head) \ - (CIRCLEQ_FIRST(head) == CIRCLEQ_END(head)) - -#define CIRCLEQ_FOREACH(var, head, field) \ - for((var) = CIRCLEQ_FIRST(head); \ - (var) != CIRCLEQ_END(head); \ - (var) = CIRCLEQ_NEXT(var, field)) - -#define CIRCLEQ_FOREACH_REVERSE(var, head, field) \ - for((var) = CIRCLEQ_LAST(head); \ - (var) != CIRCLEQ_END(head); \ - (var) = CIRCLEQ_PREV(var, field)) - -/* - * Circular queue functions. - */ -#define CIRCLEQ_INIT(head) do { \ - (head)->cqh_first = CIRCLEQ_END(head); \ - (head)->cqh_last = CIRCLEQ_END(head); \ -} while (0) - -#define CIRCLEQ_INSERT_AFTER(head, listelm, elm, field) do { \ - (elm)->field.cqe_next = (listelm)->field.cqe_next; \ - (elm)->field.cqe_prev = (listelm); \ - if ((listelm)->field.cqe_next == CIRCLEQ_END(head)) \ - (head)->cqh_last = (elm); \ - else \ - (listelm)->field.cqe_next->field.cqe_prev = (elm); \ - (listelm)->field.cqe_next = (elm); \ -} while (0) - -#define CIRCLEQ_INSERT_BEFORE(head, listelm, elm, field) do { \ - (elm)->field.cqe_next = (listelm); \ - (elm)->field.cqe_prev = (listelm)->field.cqe_prev; \ - if ((listelm)->field.cqe_prev == CIRCLEQ_END(head)) \ - (head)->cqh_first = (elm); \ - else \ - (listelm)->field.cqe_prev->field.cqe_next = (elm); \ - (listelm)->field.cqe_prev = (elm); \ -} while (0) - -#define CIRCLEQ_INSERT_HEAD(head, elm, field) do { \ - (elm)->field.cqe_next = (head)->cqh_first; \ - (elm)->field.cqe_prev = CIRCLEQ_END(head); \ - if ((head)->cqh_last == CIRCLEQ_END(head)) \ - (head)->cqh_last = (elm); \ - else \ - (head)->cqh_first->field.cqe_prev = (elm); \ - (head)->cqh_first = (elm); \ -} while (0) - -#define CIRCLEQ_INSERT_TAIL(head, elm, field) do { \ - (elm)->field.cqe_next = CIRCLEQ_END(head); \ - (elm)->field.cqe_prev = (head)->cqh_last; \ - if ((head)->cqh_first == CIRCLEQ_END(head)) \ - (head)->cqh_first = (elm); \ - else \ - (head)->cqh_last->field.cqe_next = (elm); \ - (head)->cqh_last = (elm); \ -} while (0) - -#define CIRCLEQ_REMOVE(head, elm, field) do { \ - if ((elm)->field.cqe_next == CIRCLEQ_END(head)) \ - (head)->cqh_last = (elm)->field.cqe_prev; \ - else \ - (elm)->field.cqe_next->field.cqe_prev = \ - (elm)->field.cqe_prev; \ - if ((elm)->field.cqe_prev == CIRCLEQ_END(head)) \ - (head)->cqh_first = (elm)->field.cqe_next; \ - else \ - (elm)->field.cqe_prev->field.cqe_next = \ - (elm)->field.cqe_next; \ - _Q_INVALIDATE((elm)->field.cqe_prev); \ - _Q_INVALIDATE((elm)->field.cqe_next); \ -} while (0) - -#define CIRCLEQ_REPLACE(head, elm, elm2, field) do { \ - if (((elm2)->field.cqe_next = (elm)->field.cqe_next) == \ - CIRCLEQ_END(head)) \ - (head).cqh_last = (elm2); \ - else \ - (elm2)->field.cqe_next->field.cqe_prev = (elm2); \ - if (((elm2)->field.cqe_prev = (elm)->field.cqe_prev) == \ - CIRCLEQ_END(head)) \ - (head).cqh_first = (elm2); \ - else \ - (elm2)->field.cqe_prev->field.cqe_next = (elm2); \ - _Q_INVALIDATE((elm)->field.cqe_prev); \ - _Q_INVALIDATE((elm)->field.cqe_next); \ -} while (0) - -#endif /* !_SYS_QUEUE_H_ */ diff --git a/Externals/miniupnpc/src/codelength.h b/Externals/miniupnpc/src/codelength.h deleted file mode 100644 index d342bd141944..000000000000 --- a/Externals/miniupnpc/src/codelength.h +++ /dev/null @@ -1,31 +0,0 @@ -/* $Id: codelength.h,v 1.4 2012/09/27 15:40:29 nanard Exp $ */ -/* Project : miniupnp - * Author : Thomas BERNARD - * copyright (c) 2005-2011 Thomas Bernard - * This software is subjet to the conditions detailed in the - * provided LICENCE file. */ -#ifndef CODELENGTH_H_INCLUDED -#define CODELENGTH_H_INCLUDED - -/* Encode length by using 7bit per Byte : - * Most significant bit of each byte specifies that the - * following byte is part of the code */ -#define DECODELENGTH(n, p) n = 0; \ - do { n = (n << 7) | (*p & 0x7f); } \ - while((*(p++)&0x80) && (n<(1<<25))); - -#define DECODELENGTH_CHECKLIMIT(n, p, p_limit) \ - n = 0; \ - do { \ - if((p) >= (p_limit)) break; \ - n = (n << 7) | (*(p) & 0x7f); \ - } while((*((p)++)&0x80) && (n<(1<<25))); - -#define CODELENGTH(n, p) if(n>=268435456) *(p++) = (n >> 28) | 0x80; \ - if(n>=2097152) *(p++) = (n >> 21) | 0x80; \ - if(n>=16384) *(p++) = (n >> 14) | 0x80; \ - if(n>=128) *(p++) = (n >> 7) | 0x80; \ - *(p++) = n & 0x7f; - -#endif - diff --git a/Externals/miniupnpc/src/connecthostport.c b/Externals/miniupnpc/src/connecthostport.c deleted file mode 100644 index dcf37f41906f..000000000000 --- a/Externals/miniupnpc/src/connecthostport.c +++ /dev/null @@ -1,250 +0,0 @@ -/* $Id: connecthostport.c,v 1.10 2013/05/03 09:05:38 nanard Exp $ */ -/* Project : miniupnp - * Author : Thomas Bernard - * Copyright (c) 2010-2012 Thomas Bernard - * This software is subject to the conditions detailed in the - * LICENCE file provided in this distribution. */ - -/* use getaddrinfo() or gethostbyname() - * uncomment the following line in order to use gethostbyname() */ -#ifdef NO_GETADDRINFO -#define USE_GETHOSTBYNAME -#endif - -#include -#include -#ifdef _WIN32 -#include -#include -#include -#define MAXHOSTNAMELEN 64 -#define snprintf _snprintf -#define herror -#define socklen_t int -#else /* #ifdef _WIN32 */ -#include -#include -#include -#include -#define closesocket close -#include -#include -/* defining MINIUPNPC_IGNORE_EINTR enable the ignore of interruptions - * during the connect() call */ -#define MINIUPNPC_IGNORE_EINTR -#ifndef USE_GETHOSTBYNAME -#include -#include -#endif /* #ifndef USE_GETHOSTBYNAME */ -#endif /* #else _WIN32 */ - -/* definition of PRINT_SOCKET_ERROR */ -#ifdef _WIN32 -#define PRINT_SOCKET_ERROR(x) printf("Socket error: %s, %d\n", x, WSAGetLastError()); -#else -#define PRINT_SOCKET_ERROR(x) perror(x) -#endif - -#if defined(__amigaos__) || defined(__amigaos4__) -#define herror(A) printf("%s\n", A) -#endif - -#include "connecthostport.h" - -/* connecthostport() - * return a socket connected (TCP) to the host and port - * or -1 in case of error */ -int connecthostport(const char * host, unsigned short port, - unsigned int scope_id) -{ - int s, n; -#ifdef USE_GETHOSTBYNAME - struct sockaddr_in dest; - struct hostent *hp; -#else /* #ifdef USE_GETHOSTBYNAME */ - char tmp_host[MAXHOSTNAMELEN+1]; - char port_str[8]; - struct addrinfo *ai, *p; - struct addrinfo hints; -#endif /* #ifdef USE_GETHOSTBYNAME */ -#ifdef MINIUPNPC_SET_SOCKET_TIMEOUT - struct timeval timeout; -#endif /* #ifdef MINIUPNPC_SET_SOCKET_TIMEOUT */ - -#ifdef USE_GETHOSTBYNAME - hp = gethostbyname(host); - if(hp == NULL) - { - herror(host); - return -1; - } - memcpy(&dest.sin_addr, hp->h_addr, sizeof(dest.sin_addr)); - memset(dest.sin_zero, 0, sizeof(dest.sin_zero)); - s = socket(PF_INET, SOCK_STREAM, 0); - if(s < 0) - { - PRINT_SOCKET_ERROR("socket"); - return -1; - } -#ifdef MINIUPNPC_SET_SOCKET_TIMEOUT - /* setting a 3 seconds timeout for the connect() call */ - timeout.tv_sec = 3; - timeout.tv_usec = 0; - if(setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(struct timeval)) < 0) - { - PRINT_SOCKET_ERROR("setsockopt"); - } - timeout.tv_sec = 3; - timeout.tv_usec = 0; - if(setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(struct timeval)) < 0) - { - PRINT_SOCKET_ERROR("setsockopt"); - } -#endif /* #ifdef MINIUPNPC_SET_SOCKET_TIMEOUT */ - dest.sin_family = AF_INET; - dest.sin_port = htons(port); - n = connect(s, (struct sockaddr *)&dest, sizeof(struct sockaddr_in)); -#ifdef MINIUPNPC_IGNORE_EINTR - while(n < 0 && errno == EINTR) - { - socklen_t len; - fd_set wset; - int err; - FD_ZERO(&wset); - FD_SET(s, &wset); - if((n = select(s + 1, NULL, &wset, NULL, NULL)) == -1 && errno == EINTR) - continue; - /*len = 0;*/ - /*n = getpeername(s, NULL, &len);*/ - len = sizeof(err); - if(getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len) < 0) { - PRINT_SOCKET_ERROR("getsockopt"); - closesocket(s); - return -1; - } - if(err != 0) { - errno = err; - n = -1; - } - } -#endif /* #ifdef MINIUPNPC_IGNORE_EINTR */ - if(n<0) - { - PRINT_SOCKET_ERROR("connect"); - closesocket(s); - return -1; - } -#else /* #ifdef USE_GETHOSTBYNAME */ - /* use getaddrinfo() instead of gethostbyname() */ - memset(&hints, 0, sizeof(hints)); - /* hints.ai_flags = AI_ADDRCONFIG; */ -#ifdef AI_NUMERICSERV - hints.ai_flags = AI_NUMERICSERV; -#endif - hints.ai_socktype = SOCK_STREAM; - hints.ai_family = AF_UNSPEC; /* AF_INET, AF_INET6 or AF_UNSPEC */ - /* hints.ai_protocol = IPPROTO_TCP; */ - snprintf(port_str, sizeof(port_str), "%hu", port); - if(host[0] == '[') - { - /* literal ip v6 address */ - int i, j; - for(i = 0, j = 1; host[j] && (host[j] != ']') && i < MAXHOSTNAMELEN; i++, j++) - { - tmp_host[i] = host[j]; - if(0 == memcmp(host+j, "%25", 3)) /* %25 is just url encoding for '%' */ - j+=2; /* skip "25" */ - } - tmp_host[i] = '\0'; - } - else - { - strncpy(tmp_host, host, MAXHOSTNAMELEN); - } - tmp_host[MAXHOSTNAMELEN] = '\0'; - n = getaddrinfo(tmp_host, port_str, &hints, &ai); - if(n != 0) - { -#ifdef _WIN32 - fprintf(stderr, "getaddrinfo() error : %d\n", n); -#else - fprintf(stderr, "getaddrinfo() error : %s\n", gai_strerror(n)); -#endif - return -1; - } - s = -1; - for(p = ai; p; p = p->ai_next) - { - s = socket(p->ai_family, p->ai_socktype, p->ai_protocol); - if(s < 0) - continue; - if(p->ai_addr->sa_family == AF_INET6 && scope_id > 0) { - struct sockaddr_in6 * addr6 = (struct sockaddr_in6 *)p->ai_addr; - addr6->sin6_scope_id = scope_id; - } -#ifdef MINIUPNPC_SET_SOCKET_TIMEOUT - /* setting a 3 seconds timeout for the connect() call */ - timeout.tv_sec = 3; - timeout.tv_usec = 0; - if(setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(struct timeval)) < 0) - { - PRINT_SOCKET_ERROR("setsockopt"); - } - timeout.tv_sec = 3; - timeout.tv_usec = 0; - if(setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(struct timeval)) < 0) - { - PRINT_SOCKET_ERROR("setsockopt"); - } -#endif /* #ifdef MINIUPNPC_SET_SOCKET_TIMEOUT */ - n = connect(s, p->ai_addr, p->ai_addrlen); -#ifdef MINIUPNPC_IGNORE_EINTR - while(n < 0 && errno == EINTR) - { - socklen_t len; - fd_set wset; - int err; - FD_ZERO(&wset); - FD_SET(s, &wset); - if((n = select(s + 1, NULL, &wset, NULL, NULL)) == -1 && errno == EINTR) - continue; - /*len = 0;*/ - /*n = getpeername(s, NULL, &len);*/ - len = sizeof(err); - if(getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len) < 0) { - PRINT_SOCKET_ERROR("getsockopt"); - closesocket(s); - freeaddrinfo(ai); - return -1; - } - if(err != 0) { - errno = err; - n = -1; - } - } -#endif /* #ifdef MINIUPNPC_IGNORE_EINTR */ - if(n < 0) - { - closesocket(s); - continue; - } - else - { - break; - } - } - freeaddrinfo(ai); - if(s < 0) - { - PRINT_SOCKET_ERROR("socket"); - return -1; - } - if(n < 0) - { - PRINT_SOCKET_ERROR("connect"); - return -1; - } -#endif /* #ifdef USE_GETHOSTBYNAME */ - return s; -} - diff --git a/Externals/miniupnpc/src/connecthostport.h b/Externals/miniupnpc/src/connecthostport.h deleted file mode 100644 index 56941d6faeb5..000000000000 --- a/Externals/miniupnpc/src/connecthostport.h +++ /dev/null @@ -1,18 +0,0 @@ -/* $Id: connecthostport.h,v 1.3 2012/09/27 15:42:10 nanard Exp $ */ -/* Project: miniupnp - * http://miniupnp.free.fr/ - * Author: Thomas Bernard - * Copyright (c) 2010-2012 Thomas Bernard - * This software is subjects to the conditions detailed - * in the LICENCE file provided within this distribution */ -#ifndef CONNECTHOSTPORT_H_INCLUDED -#define CONNECTHOSTPORT_H_INCLUDED - -/* connecthostport() - * return a socket connected (TCP) to the host and port - * or -1 in case of error */ -int connecthostport(const char * host, unsigned short port, - unsigned int scope_id); - -#endif - diff --git a/Externals/miniupnpc/src/declspec.h b/Externals/miniupnpc/src/declspec.h deleted file mode 100644 index b9bc07ef72b8..000000000000 --- a/Externals/miniupnpc/src/declspec.h +++ /dev/null @@ -1,5 +0,0 @@ -#ifndef DECLSPEC_H_INCLUDED -#define DECLSPEC_H_INCLUDED -#define LIBSPEC -#endif - diff --git a/Externals/miniupnpc/src/igd_desc_parse.c b/Externals/miniupnpc/src/igd_desc_parse.c deleted file mode 100644 index 6c3e65677d70..000000000000 --- a/Externals/miniupnpc/src/igd_desc_parse.c +++ /dev/null @@ -1,125 +0,0 @@ -/* $Id: igd_desc_parse.c,v 1.14 2011/04/11 09:19:24 nanard Exp $ */ -/* Project : miniupnp - * http://miniupnp.free.fr/ - * Author : Thomas Bernard - * Copyright (c) 2005-2010 Thomas Bernard - * This software is subject to the conditions detailed in the - * LICENCE file provided in this distribution. */ - -#include "igd_desc_parse.h" -#include -#include - -/* Start element handler : - * update nesting level counter and copy element name */ -void IGDstartelt(void * d, const char * name, int l) -{ - struct IGDdatas * datas = (struct IGDdatas *)d; - memcpy( datas->cureltname, name, l); - datas->cureltname[l] = '\0'; - datas->level++; - if( (l==7) && !memcmp(name, "service", l) ) { - datas->tmp.controlurl[0] = '\0'; - datas->tmp.eventsuburl[0] = '\0'; - datas->tmp.scpdurl[0] = '\0'; - datas->tmp.servicetype[0] = '\0'; - } -} - -/* End element handler : - * update nesting level counter and update parser state if - * service element is parsed */ -void IGDendelt(void * d, const char * name, int l) -{ - struct IGDdatas * datas = (struct IGDdatas *)d; - datas->level--; - /*printf("endelt %2d %.*s\n", datas->level, l, name);*/ - if( (l==7) && !memcmp(name, "service", l) ) - { - /* - if( datas->state < 1 - && !strcmp(datas->servicetype, - // "urn:schemas-upnp-org:service:WANIPConnection:1") ) - "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1")) - datas->state ++; - */ - if(0==strcmp(datas->tmp.servicetype, - "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1")) { - memcpy(&datas->CIF, &datas->tmp, sizeof(struct IGDdatas_service)); - } else if(0==strcmp(datas->tmp.servicetype, - "urn:schemas-upnp-org:service:WANIPv6FirewallControl:1")) { - memcpy(&datas->IPv6FC, &datas->tmp, sizeof(struct IGDdatas_service)); - } else if(0==strcmp(datas->tmp.servicetype, - "urn:schemas-upnp-org:service:WANIPConnection:1") - || 0==strcmp(datas->tmp.servicetype, - "urn:schemas-upnp-org:service:WANPPPConnection:1") ) { - if(datas->first.servicetype[0] == '\0') { - memcpy(&datas->first, &datas->tmp, sizeof(struct IGDdatas_service)); - } else { - memcpy(&datas->second, &datas->tmp, sizeof(struct IGDdatas_service)); - } - } - } -} - -/* Data handler : - * copy data depending on the current element name and state */ -void IGDdata(void * d, const char * data, int l) -{ - struct IGDdatas * datas = (struct IGDdatas *)d; - char * dstmember = 0; - /*printf("%2d %s : %.*s\n", - datas->level, datas->cureltname, l, data); */ - if( !strcmp(datas->cureltname, "URLBase") ) - dstmember = datas->urlbase; - else if( !strcmp(datas->cureltname, "presentationURL") ) - dstmember = datas->presentationurl; - else if( !strcmp(datas->cureltname, "serviceType") ) - dstmember = datas->tmp.servicetype; - else if( !strcmp(datas->cureltname, "controlURL") ) - dstmember = datas->tmp.controlurl; - else if( !strcmp(datas->cureltname, "eventSubURL") ) - dstmember = datas->tmp.eventsuburl; - else if( !strcmp(datas->cureltname, "SCPDURL") ) - dstmember = datas->tmp.scpdurl; -/* else if( !strcmp(datas->cureltname, "deviceType") ) - dstmember = datas->devicetype_tmp;*/ - if(dstmember) - { - if(l>=MINIUPNPC_URL_MAXSIZE) - l = MINIUPNPC_URL_MAXSIZE-1; - memcpy(dstmember, data, l); - dstmember[l] = '\0'; - } -} - -void printIGD(struct IGDdatas * d) -{ - printf("urlbase = '%s'\n", d->urlbase); - printf("WAN Device (Common interface config) :\n"); - /*printf(" deviceType = '%s'\n", d->CIF.devicetype);*/ - printf(" serviceType = '%s'\n", d->CIF.servicetype); - printf(" controlURL = '%s'\n", d->CIF.controlurl); - printf(" eventSubURL = '%s'\n", d->CIF.eventsuburl); - printf(" SCPDURL = '%s'\n", d->CIF.scpdurl); - printf("primary WAN Connection Device (IP or PPP Connection):\n"); - /*printf(" deviceType = '%s'\n", d->first.devicetype);*/ - printf(" servicetype = '%s'\n", d->first.servicetype); - printf(" controlURL = '%s'\n", d->first.controlurl); - printf(" eventSubURL = '%s'\n", d->first.eventsuburl); - printf(" SCPDURL = '%s'\n", d->first.scpdurl); - printf("secondary WAN Connection Device (IP or PPP Connection):\n"); - /*printf(" deviceType = '%s'\n", d->second.devicetype);*/ - printf(" servicetype = '%s'\n", d->second.servicetype); - printf(" controlURL = '%s'\n", d->second.controlurl); - printf(" eventSubURL = '%s'\n", d->second.eventsuburl); - printf(" SCPDURL = '%s'\n", d->second.scpdurl); - printf("WAN IPv6 Firewall Control :\n"); - /*printf(" deviceType = '%s'\n", d->IPv6FC.devicetype);*/ - printf(" servicetype = '%s'\n", d->IPv6FC.servicetype); - printf(" controlURL = '%s'\n", d->IPv6FC.controlurl); - printf(" eventSubURL = '%s'\n", d->IPv6FC.eventsuburl); - printf(" SCPDURL = '%s'\n", d->IPv6FC.scpdurl); -} - - diff --git a/Externals/miniupnpc/src/igd_desc_parse.h b/Externals/miniupnpc/src/igd_desc_parse.h deleted file mode 100644 index 0a49b019d03d..000000000000 --- a/Externals/miniupnpc/src/igd_desc_parse.h +++ /dev/null @@ -1,48 +0,0 @@ -/* $Id: igd_desc_parse.h,v 1.11 2012/10/16 16:49:02 nanard Exp $ */ -/* Project : miniupnp - * http://miniupnp.free.fr/ - * Author : Thomas Bernard - * Copyright (c) 2005-2010 Thomas Bernard - * This software is subject to the conditions detailed in the - * LICENCE file provided in this distribution. - * */ -#ifndef IGD_DESC_PARSE_H_INCLUDED -#define IGD_DESC_PARSE_H_INCLUDED - -/* Structure to store the result of the parsing of UPnP - * descriptions of Internet Gateway Devices */ -#define MINIUPNPC_URL_MAXSIZE (128) -struct IGDdatas_service { - char controlurl[MINIUPNPC_URL_MAXSIZE]; - char eventsuburl[MINIUPNPC_URL_MAXSIZE]; - char scpdurl[MINIUPNPC_URL_MAXSIZE]; - char servicetype[MINIUPNPC_URL_MAXSIZE]; - /*char devicetype[MINIUPNPC_URL_MAXSIZE];*/ -}; - -struct IGDdatas { - char cureltname[MINIUPNPC_URL_MAXSIZE]; - char urlbase[MINIUPNPC_URL_MAXSIZE]; - char presentationurl[MINIUPNPC_URL_MAXSIZE]; - int level; - /*int state;*/ - /* "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1" */ - struct IGDdatas_service CIF; - /* "urn:schemas-upnp-org:service:WANIPConnection:1" - * "urn:schemas-upnp-org:service:WANPPPConnection:1" */ - struct IGDdatas_service first; - /* if both WANIPConnection and WANPPPConnection are present */ - struct IGDdatas_service second; - /* "urn:schemas-upnp-org:service:WANIPv6FirewallControl:1" */ - struct IGDdatas_service IPv6FC; - /* tmp */ - struct IGDdatas_service tmp; -}; - -void IGDstartelt(void *, const char *, int); -void IGDendelt(void *, const char *, int); -void IGDdata(void *, const char *, int); -void printIGD(struct IGDdatas *); - -#endif - diff --git a/Externals/miniupnpc/src/minisoap.c b/Externals/miniupnpc/src/minisoap.c deleted file mode 100644 index e45a481acb24..000000000000 --- a/Externals/miniupnpc/src/minisoap.c +++ /dev/null @@ -1,121 +0,0 @@ -/* $Id: minisoap.c,v 1.22 2012/01/21 13:30:31 nanard Exp $ */ -/* Project : miniupnp - * Author : Thomas Bernard - * Copyright (c) 2005-2012 Thomas Bernard - * This software is subject to the conditions detailed in the - * LICENCE file provided in this distribution. - * - * Minimal SOAP implementation for UPnP protocol. - */ -#include -#include -#ifdef _WIN32 -#include -#include -#define snprintf _snprintf -#else -#include -#include -#include -#endif -#include "minisoap.h" -#include "miniupnpcstrings.h" - -/* only for malloc */ -#include - -#ifdef _WIN32 -#define PRINT_SOCKET_ERROR(x) printf("Socket error: %s, %d\n", x, WSAGetLastError()); -#else -#define PRINT_SOCKET_ERROR(x) perror(x) -#endif - -/* httpWrite sends the headers and the body to the socket - * and returns the number of bytes sent */ -static int -httpWrite(int fd, const char * body, int bodysize, - const char * headers, int headerssize) -{ - int n = 0; - /*n = write(fd, headers, headerssize);*/ - /*if(bodysize>0) - n += write(fd, body, bodysize);*/ - /* Note : my old linksys router only took into account - * soap request that are sent into only one packet */ - char * p; - /* TODO: AVOID MALLOC */ - p = malloc(headerssize+bodysize); - if(!p) - return 0; - memcpy(p, headers, headerssize); - memcpy(p+headerssize, body, bodysize); - /*n = write(fd, p, headerssize+bodysize);*/ - n = send(fd, p, headerssize+bodysize, 0); - if(n<0) { - PRINT_SOCKET_ERROR("send"); - } - /* disable send on the socket */ - /* draytek routers dont seems to like that... */ -#if 0 -#ifdef _WIN32 - if(shutdown(fd, SD_SEND)<0) { -#else - if(shutdown(fd, SHUT_WR)<0) { /*SD_SEND*/ -#endif - PRINT_SOCKET_ERROR("shutdown"); - } -#endif - free(p); - return n; -} - -/* self explanatory */ -int soapPostSubmit(int fd, - const char * url, - const char * host, - unsigned short port, - const char * action, - const char * body, - const char * httpversion) -{ - int bodysize; - char headerbuf[512]; - int headerssize; - char portstr[8]; - bodysize = (int)strlen(body); - /* We are not using keep-alive HTTP connections. - * HTTP/1.1 needs the header Connection: close to do that. - * This is the default with HTTP/1.0 - * Using HTTP/1.1 means we need to support chunked transfer-encoding : - * When using HTTP/1.1, the router "BiPAC 7404VNOX" always use chunked - * transfer encoding. */ - /* Connection: Close is normally there only in HTTP/1.1 but who knows */ - portstr[0] = '\0'; - if(port != 80) - snprintf(portstr, sizeof(portstr), ":%hu", port); - headerssize = snprintf(headerbuf, sizeof(headerbuf), - "POST %s HTTP/%s\r\n" - "Host: %s%s\r\n" - "User-Agent: " OS_STRING ", UPnP/1.0, MiniUPnPc/" MINIUPNPC_VERSION_STRING "\r\n" - "Content-Length: %d\r\n" - "Content-Type: text/xml\r\n" - "SOAPAction: \"%s\"\r\n" - "Connection: Close\r\n" - "Cache-Control: no-cache\r\n" /* ??? */ - "Pragma: no-cache\r\n" - "\r\n", - url, httpversion, host, portstr, bodysize, action); -#ifdef DEBUG - /*printf("SOAP request : headersize=%d bodysize=%d\n", - headerssize, bodysize); - */ - printf("SOAP request : POST %s HTTP/%s - Host: %s%s\n", - url, httpversion, host, portstr); - printf("SOAPAction: \"%s\" - Content-Length: %d\n", action, bodysize); - printf("Headers :\n%s", headerbuf); - printf("Body :\n%s\n", body); -#endif - return httpWrite(fd, body, bodysize, headerbuf, headerssize); -} - - diff --git a/Externals/miniupnpc/src/minisoap.h b/Externals/miniupnpc/src/minisoap.h deleted file mode 100644 index 14c859d1eb75..000000000000 --- a/Externals/miniupnpc/src/minisoap.h +++ /dev/null @@ -1,15 +0,0 @@ -/* $Id: minisoap.h,v 1.5 2012/09/27 15:42:10 nanard Exp $ */ -/* Project : miniupnp - * Author : Thomas Bernard - * Copyright (c) 2005 Thomas Bernard - * This software is subject to the conditions detailed in the - * LICENCE file provided in this distribution. */ -#ifndef MINISOAP_H_INCLUDED -#define MINISOAP_H_INCLUDED - -/*int httpWrite(int, const char *, int, const char *);*/ -int soapPostSubmit(int, const char *, const char *, unsigned short, - const char *, const char *, const char *); - -#endif - diff --git a/Externals/miniupnpc/src/minissdpc.c b/Externals/miniupnpc/src/minissdpc.c deleted file mode 100644 index c4913fb89a97..000000000000 --- a/Externals/miniupnpc/src/minissdpc.c +++ /dev/null @@ -1,133 +0,0 @@ -/* $Id: minissdpc.c,v 1.16 2012/03/05 19:42:46 nanard Exp $ */ -/* Project : miniupnp - * Web : http://miniupnp.free.fr/ - * Author : Thomas BERNARD - * copyright (c) 2005-2012 Thomas Bernard - * This software is subjet to the conditions detailed in the - * provided LICENCE file. */ -/*#include */ -#include -#include -#include -#include -#include -#if defined(_WIN32) || defined(__amigaos__) || defined(__amigaos4__) -#ifdef _WIN32 -#include -#include -#include -#include -#include -#endif -#if defined(__amigaos__) || defined(__amigaos4__) -#include -#endif -#if defined(__amigaos__) -#define uint16_t unsigned short -#endif -/* Hack */ -#define UNIX_PATH_LEN 108 -struct sockaddr_un { - uint16_t sun_family; - char sun_path[UNIX_PATH_LEN]; -}; -#else -#include -#include -#endif - -#include "minissdpc.h" -#include "miniupnpc.h" - -#include "codelength.h" - -struct UPNPDev * -getDevicesFromMiniSSDPD(const char * devtype, const char * socketpath) -{ - struct UPNPDev * tmp; - struct UPNPDev * devlist = NULL; - unsigned char buffer[2048]; - ssize_t n; - unsigned char * p; - unsigned char * url; - unsigned int i; - unsigned int urlsize, stsize, usnsize, l; - int s; - struct sockaddr_un addr; - - s = socket(AF_UNIX, SOCK_STREAM, 0); - if(s < 0) - { - /*syslog(LOG_ERR, "socket(unix): %m");*/ - perror("socket(unix)"); - return NULL; - } - addr.sun_family = AF_UNIX; - strncpy(addr.sun_path, socketpath, sizeof(addr.sun_path)); - /* TODO : check if we need to handle the EINTR */ - if(connect(s, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) - { - /*syslog(LOG_WARNING, "connect(\"%s\"): %m", socketpath);*/ - close(s); - return NULL; - } - stsize = strlen(devtype); - buffer[0] = 1; /* request type 1 : request devices/services by type */ - p = buffer + 1; - l = stsize; CODELENGTH(l, p); - if(p + stsize > buffer + sizeof(buffer)) - { - /* devtype is too long ! */ - close(s); - return NULL; - } - memcpy(p, devtype, stsize); - p += stsize; - if(write(s, buffer, p - buffer) < 0) - { - /*syslog(LOG_ERR, "write(): %m");*/ - perror("minissdpc.c: write()"); - close(s); - return NULL; - } - n = read(s, buffer, sizeof(buffer)); - if(n<=0) - { - perror("minissdpc.c: read()"); - close(s); - return NULL; - } - p = buffer + 1; - for(i = 0; i < buffer[0]; i++) - { - if(p+2>=buffer+sizeof(buffer)) - break; - DECODELENGTH(urlsize, p); - if(p+urlsize+2>=buffer+sizeof(buffer)) - break; - url = p; - p += urlsize; - DECODELENGTH(stsize, p); - if(p+stsize+2>=buffer+sizeof(buffer)) - break; - tmp = (struct UPNPDev *)malloc(sizeof(struct UPNPDev)+urlsize+stsize); - tmp->pNext = devlist; - tmp->descURL = tmp->buffer; - tmp->st = tmp->buffer + 1 + urlsize; - memcpy(tmp->buffer, url, urlsize); - tmp->buffer[urlsize] = '\0'; - memcpy(tmp->buffer + urlsize + 1, p, stsize); - p += stsize; - tmp->buffer[urlsize+1+stsize] = '\0'; - devlist = tmp; - /* added for compatibility with recent versions of MiniSSDPd - * >= 2007/12/19 */ - DECODELENGTH(usnsize, p); - p += usnsize; - if(p>buffer + sizeof(buffer)) - break; - } - close(s); - return devlist; -} - diff --git a/Externals/miniupnpc/src/minissdpc.h b/Externals/miniupnpc/src/minissdpc.h deleted file mode 100644 index 915b0026fb6e..000000000000 --- a/Externals/miniupnpc/src/minissdpc.h +++ /dev/null @@ -1,15 +0,0 @@ -/* $Id: minissdpc.h,v 1.2 2012/09/27 15:42:10 nanard Exp $ */ -/* Project: miniupnp - * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ - * Author: Thomas Bernard - * Copyright (c) 2005-2007 Thomas Bernard - * This software is subjects to the conditions detailed - * in the LICENCE file provided within this distribution */ -#ifndef MINISSDPC_H_INCLUDED -#define MINISSDPC_H_INCLUDED - -struct UPNPDev * -getDevicesFromMiniSSDPD(const char * devtype, const char * socketpath); - -#endif - diff --git a/Externals/miniupnpc/src/miniupnpc.c b/Externals/miniupnpc/src/miniupnpc.c deleted file mode 100644 index f6b0cc23ffbd..000000000000 --- a/Externals/miniupnpc/src/miniupnpc.c +++ /dev/null @@ -1,987 +0,0 @@ -/* $Id: miniupnpc.c,v 1.111 2012/10/09 17:53:14 nanard Exp $ */ -/* Project : miniupnp - * Web : http://miniupnp.free.fr/ - * Author : Thomas BERNARD - * copyright (c) 2005-2012 Thomas Bernard - * This software is subjet to the conditions detailed in the - * provided LICENSE file. */ -#define __EXTENSIONS__ 1 -#if !defined(MACOSX) && !defined(__sun) -#if !defined(_XOPEN_SOURCE) && !defined(__OpenBSD__) && !defined(__NetBSD__) -#ifndef __cplusplus -#define _XOPEN_SOURCE 600 -#endif -#endif -#ifndef __BSD_VISIBLE -#define __BSD_VISIBLE 1 -#endif -#endif - -#if !defined(__DragonFly__) && !defined(__OpenBSD__) && !defined(__NetBSD__) && !defined(MACOSX) && !defined(_WIN32) && !defined(__CYGWIN__) && !defined(__sun) -#define HAS_IP_MREQN -#endif - -#include -#include -#include -#ifdef _WIN32 -/* Win32 Specific includes and defines */ -#include -#include -#include -#include -#define snprintf _snprintf -#define strdup _strdup -#ifndef strncasecmp -#if defined(_MSC_VER) && (_MSC_VER >= 1400) -#define strncasecmp _memicmp -#else /* defined(_MSC_VER) && (_MSC_VER >= 1400) */ -#define strncasecmp memicmp -#endif /* defined(_MSC_VER) && (_MSC_VER >= 1400) */ -#endif /* #ifndef strncasecmp */ -#define MAXHOSTNAMELEN 64 -#else /* #ifdef _WIN32 */ -/* Standard POSIX includes */ -#include -#if defined(__amigaos__) && !defined(__amigaos4__) -/* Amiga OS 3 specific stuff */ -#define socklen_t int -#else -#include -#endif -#include -#include -#include -#include -#include -#include -#include -#if !defined(__amigaos__) && !defined(__amigaos4__) -#include -#endif -#include -#include -#define closesocket close -#endif /* #else _WIN32 */ -#ifdef MINIUPNPC_SET_SOCKET_TIMEOUT -#include -#endif -#if defined(__amigaos__) || defined(__amigaos4__) -/* Amiga OS specific stuff */ -#define TIMEVAL struct timeval -#endif - -#include "miniupnpc.h" -#include "minissdpc.h" -#include "miniwget.h" -#include "minisoap.h" -#include "minixml.h" -#include "upnpcommands.h" -#include "connecthostport.h" -#include "receivedata.h" - -#ifdef _WIN32 -#define PRINT_SOCKET_ERROR(x) printf("Socket error: %s, %d\n", x, WSAGetLastError()); -#else -#define PRINT_SOCKET_ERROR(x) perror(x) -#endif - -#define SOAPPREFIX "s" -#define SERVICEPREFIX "u" -#define SERVICEPREFIX2 'u' - -/* root description parsing */ -LIBSPEC void parserootdesc(const char * buffer, int bufsize, struct IGDdatas * data) -{ - struct xmlparser parser; - /* xmlparser object */ - parser.xmlstart = buffer; - parser.xmlsize = bufsize; - parser.data = data; - parser.starteltfunc = IGDstartelt; - parser.endeltfunc = IGDendelt; - parser.datafunc = IGDdata; - parser.attfunc = 0; - parsexml(&parser); -#ifdef DEBUG - printIGD(data); -#endif -} - -/* simpleUPnPcommand2 : - * not so simple ! - * return values : - * pointer - OK - * NULL - error */ -char * simpleUPnPcommand2(int s, const char * url, const char * service, - const char * action, struct UPNParg * args, - int * bufsize, const char * httpversion) -{ - char hostname[MAXHOSTNAMELEN+1]; - unsigned short port = 0; - char * path; - char soapact[128]; - char soapbody[2048]; - char * buf; - int n; - - *bufsize = 0; - snprintf(soapact, sizeof(soapact), "%s#%s", service, action); - if(args==NULL) - { - /*soapbodylen = */snprintf(soapbody, sizeof(soapbody), - "\r\n" - "<" SOAPPREFIX ":Envelope " - "xmlns:" SOAPPREFIX "=\"http://schemas.xmlsoap.org/soap/envelope/\" " - SOAPPREFIX ":encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" - "<" SOAPPREFIX ":Body>" - "<" SERVICEPREFIX ":%s xmlns:" SERVICEPREFIX "=\"%s\">" - "" - "" - "\r\n", action, service, action); - } - else - { - char * p; - const char * pe, * pv; - int soapbodylen; - soapbodylen = snprintf(soapbody, sizeof(soapbody), - "\r\n" - "<" SOAPPREFIX ":Envelope " - "xmlns:" SOAPPREFIX "=\"http://schemas.xmlsoap.org/soap/envelope/\" " - SOAPPREFIX ":encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" - "<" SOAPPREFIX ":Body>" - "<" SERVICEPREFIX ":%s xmlns:" SERVICEPREFIX "=\"%s\">", - action, service); - p = soapbody + soapbodylen; - while(args->elt) - { - /* check that we are never overflowing the string... */ - if(soapbody + sizeof(soapbody) <= p + 100) - { - /* we keep a margin of at least 100 bytes */ - return NULL; - } - *(p++) = '<'; - pe = args->elt; - while(*pe) - *(p++) = *(pe++); - *(p++) = '>'; - if((pv = args->val)) - { - while(*pv) - *(p++) = *(pv++); - } - *(p++) = '<'; - *(p++) = '/'; - pe = args->elt; - while(*pe) - *(p++) = *(pe++); - *(p++) = '>'; - args++; - } - *(p++) = '<'; - *(p++) = '/'; - *(p++) = SERVICEPREFIX2; - *(p++) = ':'; - pe = action; - while(*pe) - *(p++) = *(pe++); - strncpy(p, ">\r\n", - soapbody + sizeof(soapbody) - p); - } - if(!parseURL(url, hostname, &port, &path, NULL)) return NULL; - if(s < 0) { - s = connecthostport(hostname, port, 0); - if(s < 0) { - /* failed to connect */ - return NULL; - } - } - - n = soapPostSubmit(s, path, hostname, port, soapact, soapbody, httpversion); - if(n<=0) { -#ifdef DEBUG - printf("Error sending SOAP request\n"); -#endif - closesocket(s); - return NULL; - } - - buf = getHTTPResponse(s, bufsize); -#ifdef DEBUG - if(*bufsize > 0 && buf) - { - printf("SOAP Response :\n%.*s\n", *bufsize, buf); - } -#endif - closesocket(s); - return buf; -} - -/* simpleUPnPcommand : - * not so simple ! - * return values : - * pointer - OK - * NULL - error */ -char * simpleUPnPcommand(int s, const char * url, const char * service, - const char * action, struct UPNParg * args, - int * bufsize) -{ - char * buf; - -#if 1 - buf = simpleUPnPcommand2(s, url, service, action, args, bufsize, "1.1"); -#else - buf = simpleUPnPcommand2(s, url, service, action, args, bufsize, "1.0"); - if (!buf || *bufsize == 0) - { -#if DEBUG - printf("Error or no result from SOAP request; retrying with HTTP/1.1\n"); -#endif - buf = simpleUPnPcommand2(s, url, service, action, args, bufsize, "1.1"); - } -#endif - return buf; -} - -/* parseMSEARCHReply() - * the last 4 arguments are filled during the parsing : - * - location/locationsize : "location:" field of the SSDP reply packet - * - st/stsize : "st:" field of the SSDP reply packet. - * The strings are NOT null terminated */ -static void -parseMSEARCHReply(const char * reply, int size, - const char * * location, int * locationsize, - const char * * st, int * stsize) -{ - int a, b, i; - i = 0; - a = i; /* start of the line */ - b = 0; /* end of the "header" (position of the colon) */ - while(isin6_family = AF_INET6; - if(sameport) - p->sin6_port = htons(PORT); - p->sin6_addr = in6addr_any; /* in6addr_any is not available with MinGW32 3.4.2 */ - } else { - struct sockaddr_in * p = (struct sockaddr_in *)&sockudp_r; - p->sin_family = AF_INET; - if(sameport) - p->sin_port = htons(PORT); - p->sin_addr.s_addr = INADDR_ANY; - } -#ifdef _WIN32 -/* This code could help us to use the right Network interface for - * SSDP multicast traffic */ -/* Get IP associated with the index given in the ip_forward struct - * in order to give this ip to setsockopt(sudp, IPPROTO_IP, IP_MULTICAST_IF) */ - if(!ipv6 - && (GetBestRoute(inet_addr("223.255.255.255"), 0, &ip_forward) == NO_ERROR)) { - DWORD dwRetVal = 0; - PMIB_IPADDRTABLE pIPAddrTable; - DWORD dwSize = 0; -#ifdef DEBUG - IN_ADDR IPAddr; -#endif - int i; -#ifdef DEBUG - printf("ifIndex=%lu nextHop=%lx \n", ip_forward.dwForwardIfIndex, ip_forward.dwForwardNextHop); -#endif - pIPAddrTable = (MIB_IPADDRTABLE *) malloc(sizeof (MIB_IPADDRTABLE)); - if (GetIpAddrTable(pIPAddrTable, &dwSize, 0) == ERROR_INSUFFICIENT_BUFFER) { - free(pIPAddrTable); - pIPAddrTable = (MIB_IPADDRTABLE *) malloc(dwSize); - } - if(pIPAddrTable) { - dwRetVal = GetIpAddrTable( pIPAddrTable, &dwSize, 0 ); -#ifdef DEBUG - printf("\tNum Entries: %ld\n", pIPAddrTable->dwNumEntries); -#endif - for (i=0; i < (int) pIPAddrTable->dwNumEntries; i++) { -#ifdef DEBUG - printf("\n\tInterface Index[%d]:\t%ld\n", i, pIPAddrTable->table[i].dwIndex); - IPAddr.S_un.S_addr = (u_long) pIPAddrTable->table[i].dwAddr; - printf("\tIP Address[%d]: \t%s\n", i, inet_ntoa(IPAddr) ); - IPAddr.S_un.S_addr = (u_long) pIPAddrTable->table[i].dwMask; - printf("\tSubnet Mask[%d]: \t%s\n", i, inet_ntoa(IPAddr) ); - IPAddr.S_un.S_addr = (u_long) pIPAddrTable->table[i].dwBCastAddr; - printf("\tBroadCast[%d]: \t%s (%ld)\n", i, inet_ntoa(IPAddr), pIPAddrTable->table[i].dwBCastAddr); - printf("\tReassembly size[%d]:\t%ld\n", i, pIPAddrTable->table[i].dwReasmSize); - printf("\tType and State[%d]:", i); - printf("\n"); -#endif - if (pIPAddrTable->table[i].dwIndex == ip_forward.dwForwardIfIndex) { - /* Set the address of this interface to be used */ - struct in_addr mc_if; - memset(&mc_if, 0, sizeof(mc_if)); - mc_if.s_addr = pIPAddrTable->table[i].dwAddr; - if(setsockopt(sudp, IPPROTO_IP, IP_MULTICAST_IF, (const char *)&mc_if, sizeof(mc_if)) < 0) { - PRINT_SOCKET_ERROR("setsockopt"); - } - ((struct sockaddr_in *)&sockudp_r)->sin_addr.s_addr = pIPAddrTable->table[i].dwAddr; -#ifndef DEBUG - break; -#endif - } - } - free(pIPAddrTable); - pIPAddrTable = NULL; - } - } -#endif - -#ifdef _WIN32 - if (setsockopt(sudp, SOL_SOCKET, SO_REUSEADDR, (const char *)&opt, sizeof (opt)) < 0) -#else - if (setsockopt(sudp, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)) < 0) -#endif - { - if(error) - *error = UPNPDISCOVER_SOCKET_ERROR; - PRINT_SOCKET_ERROR("setsockopt"); - return NULL; - } - - if(multicastif) - { - if(ipv6) { -#if !defined(_WIN32) - /* according to MSDN, if_nametoindex() is supported since - * MS Windows Vista and MS Windows Server 2008. - * http://msdn.microsoft.com/en-us/library/bb408409%28v=vs.85%29.aspx */ - unsigned int ifindex = if_nametoindex(multicastif); /* eth0, etc. */ - if(setsockopt(sudp, IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifindex, sizeof(&ifindex)) < 0) - { - PRINT_SOCKET_ERROR("setsockopt"); - } -#else -#ifdef DEBUG - printf("Setting of multicast interface not supported in IPv6 under Windows.\n"); -#endif -#endif - } else { - struct in_addr mc_if; - mc_if.s_addr = inet_addr(multicastif); /* ex: 192.168.x.x */ - if(mc_if.s_addr != INADDR_NONE) - { - ((struct sockaddr_in *)&sockudp_r)->sin_addr.s_addr = mc_if.s_addr; - if(setsockopt(sudp, IPPROTO_IP, IP_MULTICAST_IF, (const char *)&mc_if, sizeof(mc_if)) < 0) - { - PRINT_SOCKET_ERROR("setsockopt"); - } - } else { -#ifdef HAS_IP_MREQN - /* was not an ip address, try with an interface name */ - struct ip_mreqn reqn; /* only defined with -D_BSD_SOURCE or -D_GNU_SOURCE */ - memset(&reqn, 0, sizeof(struct ip_mreqn)); - reqn.imr_ifindex = if_nametoindex(multicastif); - if(setsockopt(sudp, IPPROTO_IP, IP_MULTICAST_IF, (const char *)&reqn, sizeof(reqn)) < 0) - { - PRINT_SOCKET_ERROR("setsockopt"); - } -#else -#ifdef DEBUG - printf("Setting of multicast interface not supported with interface name.\n"); -#endif -#endif - } - } - } - - /* Avant d'envoyer le paquet on bind pour recevoir la reponse */ - if (bind(sudp, (const struct sockaddr *)&sockudp_r, - ipv6 ? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in)) != 0) - { - if(error) - *error = UPNPDISCOVER_SOCKET_ERROR; - PRINT_SOCKET_ERROR("bind"); - closesocket(sudp); - return NULL; - } - - if(error) - *error = UPNPDISCOVER_SUCCESS; - /* Calculating maximum response time in seconds */ - mx = ((unsigned int)delay) / 1000u; - /* receiving SSDP response packet */ - for(n = 0; deviceList[deviceIndex]; deviceIndex++) - { - if(n == 0) - { - /* sending the SSDP M-SEARCH packet */ - n = snprintf(bufr, sizeof(bufr), - MSearchMsgFmt, - ipv6 ? - (linklocal ? "[" UPNP_MCAST_LL_ADDR "]" : "[" UPNP_MCAST_SL_ADDR "]") - : UPNP_MCAST_ADDR, - deviceList[deviceIndex], mx); -#ifdef DEBUG - printf("Sending %s", bufr); -#endif -#ifdef NO_GETADDRINFO - /* the following code is not using getaddrinfo */ - /* emission */ - memset(&sockudp_w, 0, sizeof(struct sockaddr_storage)); - if(ipv6) { - struct sockaddr_in6 * p = (struct sockaddr_in6 *)&sockudp_w; - p->sin6_family = AF_INET6; - p->sin6_port = htons(PORT); - inet_pton(AF_INET6, - linklocal ? UPNP_MCAST_LL_ADDR : UPNP_MCAST_SL_ADDR, - &(p->sin6_addr)); - } else { - struct sockaddr_in * p = (struct sockaddr_in *)&sockudp_w; - p->sin_family = AF_INET; - p->sin_port = htons(PORT); - p->sin_addr.s_addr = inet_addr(UPNP_MCAST_ADDR); - } - n = sendto(sudp, bufr, n, 0, - &sockudp_w, - ipv6 ? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in)); - if (n < 0) { - if(error) - *error = UPNPDISCOVER_SOCKET_ERROR; - PRINT_SOCKET_ERROR("sendto"); - break; - } -#else /* #ifdef NO_GETADDRINFO */ - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_UNSPEC; /* AF_INET6 or AF_INET */ - hints.ai_socktype = SOCK_DGRAM; - /*hints.ai_flags = */ - if ((rv = getaddrinfo(ipv6 - ? (linklocal ? UPNP_MCAST_LL_ADDR : UPNP_MCAST_SL_ADDR) - : UPNP_MCAST_ADDR, - XSTR(PORT), &hints, &servinfo)) != 0) { - if(error) - *error = UPNPDISCOVER_SOCKET_ERROR; -#ifdef _WIN32 - fprintf(stderr, "getaddrinfo() failed: %d\n", rv); -#else - fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); -#endif - break; - } - for(p = servinfo; p; p = p->ai_next) { - n = sendto(sudp, bufr, n, 0, p->ai_addr, p->ai_addrlen); - if (n < 0) { -#ifdef DEBUG - char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV]; - if (getnameinfo(p->ai_addr, p->ai_addrlen, hbuf, sizeof(hbuf), sbuf, - sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV) == 0) { - fprintf(stderr, "host:%s port:%s\n", hbuf, sbuf); - } -#endif - PRINT_SOCKET_ERROR("sendto"); - continue; - } - } - freeaddrinfo(servinfo); - if(n < 0) { - if(error) - *error = UPNPDISCOVER_SOCKET_ERROR; - break; - } -#endif /* #ifdef NO_GETADDRINFO */ - } - /* Waiting for SSDP REPLY packet to M-SEARCH */ - n = receivedata(sudp, bufr, sizeof(bufr), delay, &scope_id); - if (n < 0) { - /* error */ - if(error) - *error = UPNPDISCOVER_SOCKET_ERROR; - break; - } else if (n == 0) { - /* no data or Time Out */ - if (devlist) { - /* no more device type to look for... */ - if(error) - *error = UPNPDISCOVER_SUCCESS; - break; - } - if(ipv6) { - if(linklocal) { - linklocal = 0; - --deviceIndex; - } else { - linklocal = 1; - } - } - } else { - const char * descURL=NULL; - int urlsize=0; - const char * st=NULL; - int stsize=0; - /*printf("%d byte(s) :\n%s\n", n, bufr);*/ /* affichage du message */ - parseMSEARCHReply(bufr, n, &descURL, &urlsize, &st, &stsize); - if(st&&descURL) - { -#ifdef DEBUG - printf("M-SEARCH Reply:\nST: %.*s\nLocation: %.*s\n", - stsize, st, urlsize, descURL); -#endif - for(tmp=devlist; tmp; tmp = tmp->pNext) { - if(memcmp(tmp->descURL, descURL, urlsize) == 0 && - tmp->descURL[urlsize] == '\0' && - memcmp(tmp->st, st, stsize) == 0 && - tmp->st[stsize] == '\0') - break; - } - /* at the exit of the loop above, tmp is null if - * no duplicate device was found */ - if(tmp) - continue; - tmp = (struct UPNPDev *)malloc(sizeof(struct UPNPDev)+urlsize+stsize); - if(!tmp) { - /* memory allocation error */ - if(error) - *error = UPNPDISCOVER_MEMORY_ERROR; - break; - } - tmp->pNext = devlist; - tmp->descURL = tmp->buffer; - tmp->st = tmp->buffer + 1 + urlsize; - memcpy(tmp->buffer, descURL, urlsize); - tmp->buffer[urlsize] = '\0'; - memcpy(tmp->buffer + urlsize + 1, st, stsize); - tmp->buffer[urlsize+1+stsize] = '\0'; - tmp->scope_id = scope_id; - devlist = tmp; - } - } - } - closesocket(sudp); - return devlist; -} - -/* freeUPNPDevlist() should be used to - * free the chained list returned by upnpDiscover() */ -LIBSPEC void freeUPNPDevlist(struct UPNPDev * devlist) -{ - struct UPNPDev * next; - while(devlist) - { - next = devlist->pNext; - free(devlist); - devlist = next; - } -} - -static void -url_cpy_or_cat(char * dst, const char * src, int n) -{ - if( (src[0] == 'h') - &&(src[1] == 't') - &&(src[2] == 't') - &&(src[3] == 'p') - &&(src[4] == ':') - &&(src[5] == '/') - &&(src[6] == '/')) - { - strncpy(dst, src, n); - } - else - { - int l = strlen(dst); - if(src[0] != '/') - dst[l++] = '/'; - if(l<=n) - strncpy(dst + l, src, n - l); - } -} - -/* Prepare the Urls for usage... - */ -LIBSPEC void -GetUPNPUrls(struct UPNPUrls * urls, struct IGDdatas * data, - const char * descURL, unsigned int scope_id) -{ - char * p; - int n1, n2, n3, n4; -#ifdef IF_NAMESIZE - char ifname[IF_NAMESIZE]; -#else - char scope_str[8]; -#endif - - n1 = strlen(data->urlbase); - if(n1==0) - n1 = strlen(descURL); - if(scope_id != 0) { -#ifdef IF_NAMESIZE - if(if_indextoname(scope_id, ifname)) { - n1 += 3 + strlen(ifname); /* 3 == strlen(%25) */ - } -#else - /* under windows, scope is numerical */ - snprintf(scope_str, sizeof(scope_str), "%u", scope_id); -#endif - } - n1 += 2; /* 1 byte more for Null terminator, 1 byte for '/' if needed */ - n2 = n1; n3 = n1; n4 = n1; - n1 += strlen(data->first.scpdurl); - n2 += strlen(data->first.controlurl); - n3 += strlen(data->CIF.controlurl); - n4 += strlen(data->IPv6FC.controlurl); - - /* allocate memory to store URLs */ - urls->ipcondescURL = (char *)malloc(n1); - urls->controlURL = (char *)malloc(n2); - urls->controlURL_CIF = (char *)malloc(n3); - urls->controlURL_6FC = (char *)malloc(n4); - - /* strdup descURL */ - urls->rootdescURL = strdup(descURL); - - /* get description of WANIPConnection */ - if(data->urlbase[0] != '\0') - strncpy(urls->ipcondescURL, data->urlbase, n1); - else - strncpy(urls->ipcondescURL, descURL, n1); - p = strchr(urls->ipcondescURL+7, '/'); - if(p) p[0] = '\0'; - if(scope_id != 0) { - if(0 == memcmp(urls->ipcondescURL, "http://[fe80:", 13)) { - /* this is a linklocal IPv6 address */ - p = strchr(urls->ipcondescURL, ']'); - if(p) { - /* insert %25 into URL */ -#ifdef IF_NAMESIZE - memmove(p + 3 + strlen(ifname), p, strlen(p) + 1); - memcpy(p, "%25", 3); - memcpy(p + 3, ifname, strlen(ifname)); -#else - memmove(p + 3 + strlen(scope_str), p, strlen(p) + 1); - memcpy(p, "%25", 3); - memcpy(p + 3, scope_str, strlen(scope_str)); -#endif - } - } - } - strncpy(urls->controlURL, urls->ipcondescURL, n2); - strncpy(urls->controlURL_CIF, urls->ipcondescURL, n3); - strncpy(urls->controlURL_6FC, urls->ipcondescURL, n4); - - url_cpy_or_cat(urls->ipcondescURL, data->first.scpdurl, n1); - - url_cpy_or_cat(urls->controlURL, data->first.controlurl, n2); - - url_cpy_or_cat(urls->controlURL_CIF, data->CIF.controlurl, n3); - - url_cpy_or_cat(urls->controlURL_6FC, data->IPv6FC.controlurl, n4); - -#ifdef DEBUG - printf("urls->ipcondescURL='%s' %u n1=%d\n", urls->ipcondescURL, - (unsigned)strlen(urls->ipcondescURL), n1); - printf("urls->controlURL='%s' %u n2=%d\n", urls->controlURL, - (unsigned)strlen(urls->controlURL), n2); - printf("urls->controlURL_CIF='%s' %u n3=%d\n", urls->controlURL_CIF, - (unsigned)strlen(urls->controlURL_CIF), n3); - printf("urls->controlURL_6FC='%s' %u n4=%d\n", urls->controlURL_6FC, - (unsigned)strlen(urls->controlURL_6FC), n4); -#endif -} - -LIBSPEC void -FreeUPNPUrls(struct UPNPUrls * urls) -{ - if(!urls) - return; - free(urls->controlURL); - urls->controlURL = 0; - free(urls->ipcondescURL); - urls->ipcondescURL = 0; - free(urls->controlURL_CIF); - urls->controlURL_CIF = 0; - free(urls->controlURL_6FC); - urls->controlURL_6FC = 0; - free(urls->rootdescURL); - urls->rootdescURL = 0; -} - -int -UPNPIGD_IsConnected(struct UPNPUrls * urls, struct IGDdatas * data) -{ - char status[64]; - unsigned int uptime; - status[0] = '\0'; - UPNP_GetStatusInfo(urls->controlURL, data->first.servicetype, - status, &uptime, NULL); - if(0 == strcmp("Connected", status)) - { - return 1; - } - else - return 0; -} - - -/* UPNP_GetValidIGD() : - * return values : - * -1 = Internal error - * 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 - * - * In any non zero return case, the urls and data structures - * passed as parameters are set. Donc forget to call FreeUPNPUrls(urls) to - * free allocated memory. - */ -LIBSPEC int -UPNP_GetValidIGD(struct UPNPDev * devlist, - struct UPNPUrls * urls, - struct IGDdatas * data, - char * lanaddr, int lanaddrlen) -{ - struct xml_desc { - char * xml; - int size; - } * desc = NULL; - struct UPNPDev * dev; - int ndev = 0; - int i; - int state = -1; /* state 1 : IGD connected. State 2 : IGD. State 3 : anything */ - if(!devlist) - { -#ifdef DEBUG - printf("Empty devlist\n"); -#endif - return 0; - } - for(dev = devlist; dev; dev = dev->pNext) - ndev++; - if(ndev > 0) - { - desc = calloc(ndev, sizeof(struct xml_desc)); - if(!desc) - return -1; /* memory allocation error */ - } - for(state = 1; state <= 3; state++) - { - for(dev = devlist, i = 0; dev; dev = dev->pNext, i++) - { - /* we should choose an internet gateway device. - * with st == urn:schemas-upnp-org:device:InternetGatewayDevice:1 */ - if(state == 1) - { - desc[i].xml = miniwget_getaddr(dev->descURL, &(desc[i].size), - lanaddr, lanaddrlen, - dev->scope_id); -#ifdef DEBUG - if(!desc[i].xml) - { - printf("error getting XML description %s\n", dev->descURL); - } -#endif - } - if(desc[i].xml) - { - memset(data, 0, sizeof(struct IGDdatas)); - memset(urls, 0, sizeof(struct UPNPUrls)); - parserootdesc(desc[i].xml, desc[i].size, data); - if(0==strcmp(data->CIF.servicetype, - "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1") - || state >= 3 ) - { - GetUPNPUrls(urls, data, dev->descURL, dev->scope_id); - -#ifdef DEBUG - printf("UPNPIGD_IsConnected(%s) = %d\n", - urls->controlURL, - UPNPIGD_IsConnected(urls, data)); -#endif - if((state >= 2) || UPNPIGD_IsConnected(urls, data)) - goto free_and_return; - FreeUPNPUrls(urls); - if(data->second.servicetype[0] != '\0') { -#ifdef DEBUG - printf("We tried %s, now we try %s !\n", - data->first.servicetype, data->second.servicetype); -#endif - /* swaping WANPPPConnection and WANIPConnection ! */ - memcpy(&data->tmp, &data->first, sizeof(struct IGDdatas_service)); - memcpy(&data->first, &data->second, sizeof(struct IGDdatas_service)); - memcpy(&data->second, &data->tmp, sizeof(struct IGDdatas_service)); - GetUPNPUrls(urls, data, dev->descURL, dev->scope_id); -#ifdef DEBUG - printf("UPNPIGD_IsConnected(%s) = %d\n", - urls->controlURL, - UPNPIGD_IsConnected(urls, data)); -#endif - if((state >= 2) || UPNPIGD_IsConnected(urls, data)) - goto free_and_return; - FreeUPNPUrls(urls); - } - } - memset(data, 0, sizeof(struct IGDdatas)); - } - } - } - state = 0; -free_and_return: - if(desc) { - for(i = 0; i < ndev; i++) { - if(desc[i].xml) { - free(desc[i].xml); - } - } - free(desc); - } - return state; -} - -/* UPNP_GetIGDFromUrl() - * Used when skipping the discovery process. - * return value : - * 0 - Not ok - * 1 - OK */ -int -UPNP_GetIGDFromUrl(const char * rootdescurl, - struct UPNPUrls * urls, - struct IGDdatas * data, - char * lanaddr, int lanaddrlen) -{ - char * descXML; - int descXMLsize = 0; - descXML = miniwget_getaddr(rootdescurl, &descXMLsize, - lanaddr, lanaddrlen, 0); - if(descXML) { - memset(data, 0, sizeof(struct IGDdatas)); - memset(urls, 0, sizeof(struct UPNPUrls)); - parserootdesc(descXML, descXMLsize, data); - free(descXML); - descXML = NULL; - GetUPNPUrls(urls, data, rootdescurl, 0); - return 1; - } else { - return 0; - } -} - diff --git a/Externals/miniupnpc/src/miniupnpc.def b/Externals/miniupnpc/src/miniupnpc.def deleted file mode 100644 index 10b9f5800295..000000000000 --- a/Externals/miniupnpc/src/miniupnpc.def +++ /dev/null @@ -1,42 +0,0 @@ -LIBRARY -; miniupnpc library - -EXPORTS -; miniupnpc - upnpDiscover - freeUPNPDevlist - parserootdesc - UPNP_GetValidIGD - UPNP_GetIGDFromUrl - GetUPNPUrls - FreeUPNPUrls -; miniwget - miniwget - miniwget_getaddr -; upnpcommands - UPNP_GetTotalBytesSent - UPNP_GetTotalBytesReceived - UPNP_GetTotalPacketsSent - UPNP_GetTotalPacketsReceived - UPNP_GetStatusInfo - UPNP_GetConnectionTypeInfo - UPNP_GetExternalIPAddress - UPNP_GetLinkLayerMaxBitRates - UPNP_AddPortMapping - UPNP_DeletePortMapping - UPNP_GetPortMappingNumberOfEntries - UPNP_GetSpecificPortMappingEntry - UPNP_GetGenericPortMappingEntry - UPNP_GetListOfPortMappings - UPNP_AddPinhole - UPNP_CheckPinholeWorking - UPNP_UpdatePinhole - UPNP_GetPinholePackets - UPNP_DeletePinhole - UPNP_GetFirewallStatus - UPNP_GetOutboundPinholeTimeout -; upnperrors - strupnperror -; portlistingparse - ParsePortListing - FreePortListing diff --git a/Externals/miniupnpc/src/miniupnpc.h b/Externals/miniupnpc/src/miniupnpc.h deleted file mode 100644 index ba0deb03f289..000000000000 --- a/Externals/miniupnpc/src/miniupnpc.h +++ /dev/null @@ -1,130 +0,0 @@ -/* $Id: miniupnpc.h,v 1.32 2013/02/06 14:44:42 nanard Exp $ */ -/* Project: miniupnp - * http://miniupnp.free.fr/ - * Author: Thomas Bernard - * Copyright (c) 2005-2012 Thomas Bernard - * This software is subjects to the conditions detailed - * in the LICENCE file provided within this distribution */ -#ifndef MINIUPNPC_H_INCLUDED -#define MINIUPNPC_H_INCLUDED - -#include "declspec.h" -#include "igd_desc_parse.h" - -/* error codes : */ -#define UPNPDISCOVER_SUCCESS (0) -#define UPNPDISCOVER_UNKNOWN_ERROR (-1) -#define UPNPDISCOVER_SOCKET_ERROR (-101) -#define UPNPDISCOVER_MEMORY_ERROR (-102) - -/* versions : */ -#define MINIUPNPC_VERSION "1.8.20130503" -#define MINIUPNPC_API_VERSION 9 - -#ifdef __cplusplus -extern "C" { -#endif - -/* Structures definitions : */ -struct UPNParg { const char * elt; const char * val; }; - -char * -simpleUPnPcommand(int, const char *, const char *, - const char *, struct UPNParg *, - int *); - -struct UPNPDev { - struct UPNPDev * pNext; - char * descURL; - char * st; - unsigned int scope_id; - char buffer[2]; -}; - -/* upnpDiscover() - * discover UPnP devices on the network. - * The discovered devices are returned as a chained list. - * It is up to the caller to free the list with freeUPNPDevlist(). - * delay (in millisecond) is the maximum time for waiting any device - * response. - * If available, device list will be obtained from MiniSSDPd. - * Default path for minissdpd socket will be used if minissdpdsock argument - * is NULL. - * If multicastif is not NULL, it will be used instead of the default - * multicast interface for sending SSDP discover packets. - * If sameport is not null, SSDP packets will be sent from the source port - * 1900 (same as destination port) otherwise system assign a source port. */ -LIBSPEC struct UPNPDev * -upnpDiscover(int delay, const char * multicastif, - const char * minissdpdsock, int sameport, - int ipv6, - int * error); -/* freeUPNPDevlist() - * free list returned by upnpDiscover() */ -LIBSPEC void freeUPNPDevlist(struct UPNPDev * devlist); - -/* parserootdesc() : - * parse root XML description of a UPnP device and fill the IGDdatas - * structure. */ -LIBSPEC void parserootdesc(const char *, int, struct IGDdatas *); - -/* structure used to get fast access to urls - * controlURL: controlURL of the WANIPConnection - * ipcondescURL: url of the description of the WANIPConnection - * controlURL_CIF: controlURL of the WANCommonInterfaceConfig - * controlURL_6FC: controlURL of the WANIPv6FirewallControl - */ -struct UPNPUrls { - char * controlURL; - char * ipcondescURL; - char * controlURL_CIF; - char * controlURL_6FC; - char * rootdescURL; -}; - -/* UPNP_GetValidIGD() : - * return 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 - * - * In any non zero return case, the urls and data structures - * passed as parameters are set. Donc forget to call FreeUPNPUrls(urls) to - * free allocated memory. - */ -LIBSPEC int -UPNP_GetValidIGD(struct UPNPDev * devlist, - struct UPNPUrls * urls, - struct IGDdatas * data, - char * lanaddr, int lanaddrlen); - -/* UPNP_GetIGDFromUrl() - * Used when skipping the discovery process. - * return value : - * 0 - Not ok - * 1 - OK */ -LIBSPEC int -UPNP_GetIGDFromUrl(const char * rootdescurl, - struct UPNPUrls * urls, - struct IGDdatas * data, - char * lanaddr, int lanaddrlen); - -LIBSPEC void -GetUPNPUrls(struct UPNPUrls *, struct IGDdatas *, - const char *, unsigned int); - -LIBSPEC void -FreeUPNPUrls(struct UPNPUrls *); - -/* return 0 or 1 */ -LIBSPEC int UPNPIGD_IsConnected(struct UPNPUrls *, struct IGDdatas *); - - -#ifdef __cplusplus -} -#endif - -#endif - diff --git a/Externals/miniupnpc/src/miniupnpcmodule.c b/Externals/miniupnpc/src/miniupnpcmodule.c deleted file mode 100644 index f3d27d6e5962..000000000000 --- a/Externals/miniupnpc/src/miniupnpcmodule.c +++ /dev/null @@ -1,544 +0,0 @@ -/* $Id: miniupnpcmodule.c,v 1.21 2012/08/29 07:51:30 nanard Exp $*/ -/* Project : miniupnp - * Author : Thomas BERNARD - * website : http://miniupnp.tuxfamily.org/ - * copyright (c) 2007-2012 Thomas Bernard - * This software is subjet to the conditions detailed in the - * provided LICENCE file. */ -#include -#define STATICLIB -#include "structmember.h" -#include "miniupnpc.h" -#include "upnpcommands.h" -#include "upnperrors.h" - -/* for compatibility with Python < 2.4 */ -#ifndef Py_RETURN_NONE -#define Py_RETURN_NONE return Py_INCREF(Py_None), Py_None -#endif - -#ifndef Py_RETURN_TRUE -#define Py_RETURN_TRUE return Py_INCREF(Py_True), Py_True -#endif - -#ifndef Py_RETURN_FALSE -#define Py_RETURN_FALSE return Py_INCREF(Py_False), Py_False -#endif - -/* for compatibility with Python < 3.0 */ -#ifndef PyVarObject_HEAD_INIT -#define PyVarObject_HEAD_INIT(type, size) \ - PyObject_HEAD_INIT(type) size, -#endif - -#ifndef Py_TYPE -#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) -#endif - -typedef struct { - PyObject_HEAD - /* Type-specific fields go here. */ - struct UPNPDev * devlist; - struct UPNPUrls urls; - struct IGDdatas data; - unsigned int discoverdelay; /* value passed to upnpDiscover() */ - char lanaddr[40]; /* our ip address on the LAN */ - char * multicastif; - char * minissdpdsocket; -} UPnPObject; - -static PyMemberDef UPnP_members[] = { - {"lanaddr", T_STRING_INPLACE, offsetof(UPnPObject, lanaddr), - READONLY, "ip address on the LAN" - }, - {"discoverdelay", T_UINT, offsetof(UPnPObject, discoverdelay), - 0/*READWRITE*/, "value in ms used to wait for SSDP responses" - }, - /* T_STRING is allways readonly :( */ - {"multicastif", T_STRING, offsetof(UPnPObject, multicastif), - 0, "IP of the network interface to be used for multicast operations" - }, - {"minissdpdsocket", T_STRING, offsetof(UPnPObject, multicastif), - 0, "path of the MiniSSDPd unix socket" - }, - {NULL} -}; - -static void -UPnPObject_dealloc(UPnPObject *self) -{ - freeUPNPDevlist(self->devlist); - FreeUPNPUrls(&self->urls); - Py_TYPE(self)->tp_free((PyObject*)self); -} - -static PyObject * -UPnP_discover(UPnPObject *self) -{ - struct UPNPDev * dev; - int i; - PyObject *res = NULL; - if(self->devlist) - { - freeUPNPDevlist(self->devlist); - self->devlist = 0; - } - Py_BEGIN_ALLOW_THREADS - self->devlist = upnpDiscover((int)self->discoverdelay/*timeout in ms*/, - 0/* multicast if*/, - 0/*minissdpd socket*/, - 0/*sameport flag*/, - 0/*ip v6*/, - 0/*error */); - Py_END_ALLOW_THREADS - /* Py_RETURN_NONE ??? */ - for(dev = self->devlist, i = 0; dev; dev = dev->pNext) - i++; - res = Py_BuildValue("i", i); - return res; -} - -static PyObject * -UPnP_selectigd(UPnPObject *self) -{ - int r; -Py_BEGIN_ALLOW_THREADS - r = UPNP_GetValidIGD(self->devlist, &self->urls, &self->data, - self->lanaddr, sizeof(self->lanaddr)); -Py_END_ALLOW_THREADS - if(r) - { - return Py_BuildValue("s", self->urls.controlURL); - } - else - { - /* TODO: have our own exception type ! */ - PyErr_SetString(PyExc_Exception, "No UPnP device discovered"); - return NULL; - } -} - -static PyObject * -UPnP_totalbytesent(UPnPObject *self) -{ - UNSIGNED_INTEGER i; -Py_BEGIN_ALLOW_THREADS - i = UPNP_GetTotalBytesSent(self->urls.controlURL_CIF, - self->data.CIF.servicetype); -Py_END_ALLOW_THREADS - return Py_BuildValue("I", i); -} - -static PyObject * -UPnP_totalbytereceived(UPnPObject *self) -{ - UNSIGNED_INTEGER i; -Py_BEGIN_ALLOW_THREADS - i = UPNP_GetTotalBytesReceived(self->urls.controlURL_CIF, - self->data.CIF.servicetype); -Py_END_ALLOW_THREADS - return Py_BuildValue("I", i); -} - -static PyObject * -UPnP_totalpacketsent(UPnPObject *self) -{ - UNSIGNED_INTEGER i; -Py_BEGIN_ALLOW_THREADS - i = UPNP_GetTotalPacketsSent(self->urls.controlURL_CIF, - self->data.CIF.servicetype); -Py_END_ALLOW_THREADS - return Py_BuildValue("I", i); -} - -static PyObject * -UPnP_totalpacketreceived(UPnPObject *self) -{ - UNSIGNED_INTEGER i; -Py_BEGIN_ALLOW_THREADS - i = UPNP_GetTotalPacketsReceived(self->urls.controlURL_CIF, - self->data.CIF.servicetype); -Py_END_ALLOW_THREADS - return Py_BuildValue("I", i); -} - -static PyObject * -UPnP_statusinfo(UPnPObject *self) -{ - char status[64]; - char lastconnerror[64]; - unsigned int uptime = 0; - int r; - status[0] = '\0'; - lastconnerror[0] = '\0'; -Py_BEGIN_ALLOW_THREADS - r = UPNP_GetStatusInfo(self->urls.controlURL, self->data.first.servicetype, - status, &uptime, lastconnerror); -Py_END_ALLOW_THREADS - if(r==UPNPCOMMAND_SUCCESS) { - return Py_BuildValue("(s,I,s)", status, uptime, lastconnerror); - } else { - /* TODO: have our own exception type ! */ - PyErr_SetString(PyExc_Exception, strupnperror(r)); - return NULL; - } -} - -static PyObject * -UPnP_connectiontype(UPnPObject *self) -{ - char connectionType[64]; - int r; - connectionType[0] = '\0'; -Py_BEGIN_ALLOW_THREADS - r = UPNP_GetConnectionTypeInfo(self->urls.controlURL, - self->data.first.servicetype, - connectionType); -Py_END_ALLOW_THREADS - if(r==UPNPCOMMAND_SUCCESS) { - return Py_BuildValue("s", connectionType); - } else { - /* TODO: have our own exception type ! */ - PyErr_SetString(PyExc_Exception, strupnperror(r)); - return NULL; - } -} - -static PyObject * -UPnP_externalipaddress(UPnPObject *self) -{ - char externalIPAddress[40]; - int r; - externalIPAddress[0] = '\0'; -Py_BEGIN_ALLOW_THREADS - r = UPNP_GetExternalIPAddress(self->urls.controlURL, - self->data.first.servicetype, - externalIPAddress); -Py_END_ALLOW_THREADS - if(r==UPNPCOMMAND_SUCCESS) { - return Py_BuildValue("s", externalIPAddress); - } else { - /* TODO: have our own exception type ! */ - PyErr_SetString(PyExc_Exception, strupnperror(r)); - return NULL; - } -} - -/* AddPortMapping(externalPort, protocol, internalHost, internalPort, desc, - * remoteHost) - * protocol is 'UDP' or 'TCP' */ -static PyObject * -UPnP_addportmapping(UPnPObject *self, PyObject *args) -{ - char extPort[6]; - unsigned short ePort; - char inPort[6]; - unsigned short iPort; - const char * proto; - const char * host; - const char * desc; - const char * remoteHost; - const char * leaseDuration = "0"; - int r; - if (!PyArg_ParseTuple(args, "HssHss", &ePort, &proto, - &host, &iPort, &desc, &remoteHost)) - return NULL; -Py_BEGIN_ALLOW_THREADS - sprintf(extPort, "%hu", ePort); - sprintf(inPort, "%hu", iPort); - r = UPNP_AddPortMapping(self->urls.controlURL, self->data.first.servicetype, - extPort, inPort, host, desc, proto, - remoteHost, leaseDuration); -Py_END_ALLOW_THREADS - if(r==UPNPCOMMAND_SUCCESS) - { - Py_RETURN_TRUE; - } - else - { - // TODO: RAISE an Exception. See upnpcommands.h for errors codes. - // upnperrors.c - //Py_RETURN_FALSE; - /* TODO: have our own exception type ! */ - PyErr_SetString(PyExc_Exception, strupnperror(r)); - return NULL; - } -} - -/* DeletePortMapping(extPort, proto, removeHost='') - * proto = 'UDP', 'TCP' */ -static PyObject * -UPnP_deleteportmapping(UPnPObject *self, PyObject *args) -{ - char extPort[6]; - unsigned short ePort; - const char * proto; - const char * remoteHost = ""; - int r; - if(!PyArg_ParseTuple(args, "Hs|z", &ePort, &proto, &remoteHost)) - return NULL; -Py_BEGIN_ALLOW_THREADS - sprintf(extPort, "%hu", ePort); - r = UPNP_DeletePortMapping(self->urls.controlURL, self->data.first.servicetype, - extPort, proto, remoteHost); -Py_END_ALLOW_THREADS - if(r==UPNPCOMMAND_SUCCESS) { - Py_RETURN_TRUE; - } else { - /* TODO: have our own exception type ! */ - PyErr_SetString(PyExc_Exception, strupnperror(r)); - return NULL; - } -} - -static PyObject * -UPnP_getportmappingnumberofentries(UPnPObject *self) -{ - unsigned int n = 0; - int r; -Py_BEGIN_ALLOW_THREADS - r = UPNP_GetPortMappingNumberOfEntries(self->urls.controlURL, - self->data.first.servicetype, - &n); -Py_END_ALLOW_THREADS - if(r==UPNPCOMMAND_SUCCESS) { - return Py_BuildValue("I", n); - } else { - /* TODO: have our own exception type ! */ - PyErr_SetString(PyExc_Exception, strupnperror(r)); - return NULL; - } -} - -/* GetSpecificPortMapping(ePort, proto) - * proto = 'UDP' or 'TCP' */ -static PyObject * -UPnP_getspecificportmapping(UPnPObject *self, PyObject *args) -{ - char extPort[6]; - unsigned short ePort; - const char * proto; - char intClient[40]; - char intPort[6]; - unsigned short iPort; - char desc[80]; - char enabled[4]; - char leaseDuration[16]; - if(!PyArg_ParseTuple(args, "Hs", &ePort, &proto)) - return NULL; - extPort[0] = '\0'; intClient[0] = '\0'; intPort[0] = '\0'; - desc[0] = '\0'; enabled[0] = '\0'; leaseDuration[0] = '\0'; -Py_BEGIN_ALLOW_THREADS - sprintf(extPort, "%hu", ePort); - UPNP_GetSpecificPortMappingEntry(self->urls.controlURL, - self->data.first.servicetype, - extPort, proto, - intClient, intPort, - desc, enabled, leaseDuration); -Py_END_ALLOW_THREADS - if(intClient[0]) - { - iPort = (unsigned short)atoi(intPort); - return Py_BuildValue("(s,H,s,O,i)", - intClient, iPort, desc, - PyBool_FromLong(atoi(enabled)), - atoi(leaseDuration)); - } - else - { - Py_RETURN_NONE; - } -} - -/* GetGenericPortMapping(index) */ -static PyObject * -UPnP_getgenericportmapping(UPnPObject *self, PyObject *args) -{ - int i, r; - char index[8]; - char intClient[40]; - char intPort[6]; - unsigned short iPort; - char extPort[6]; - unsigned short ePort; - char protocol[4]; - char desc[80]; - char enabled[6]; - char rHost[64]; - char duration[16]; /* lease duration */ - unsigned int dur; - if(!PyArg_ParseTuple(args, "i", &i)) - return NULL; -Py_BEGIN_ALLOW_THREADS - snprintf(index, sizeof(index), "%d", i); - rHost[0] = '\0'; enabled[0] = '\0'; - duration[0] = '\0'; desc[0] = '\0'; - extPort[0] = '\0'; intPort[0] = '\0'; intClient[0] = '\0'; - r = UPNP_GetGenericPortMappingEntry(self->urls.controlURL, - self->data.first.servicetype, - index, - extPort, intClient, intPort, - protocol, desc, enabled, rHost, - duration); -Py_END_ALLOW_THREADS - if(r==UPNPCOMMAND_SUCCESS) - { - ePort = (unsigned short)atoi(extPort); - iPort = (unsigned short)atoi(intPort); - dur = (unsigned int)strtoul(duration, 0, 0); - return Py_BuildValue("(H,s,(s,H),s,s,s,I)", - ePort, protocol, intClient, iPort, - desc, enabled, rHost, dur); - } - else - { - Py_RETURN_NONE; - } -} - -/* miniupnpc.UPnP object Method Table */ -static PyMethodDef UPnP_methods[] = { - {"discover", (PyCFunction)UPnP_discover, METH_NOARGS, - "discover UPnP IGD devices on the network" - }, - {"selectigd", (PyCFunction)UPnP_selectigd, METH_NOARGS, - "select a valid UPnP IGD among discovered devices" - }, - {"totalbytesent", (PyCFunction)UPnP_totalbytesent, METH_NOARGS, - "return the total number of bytes sent by UPnP IGD" - }, - {"totalbytereceived", (PyCFunction)UPnP_totalbytereceived, METH_NOARGS, - "return the total number of bytes received by UPnP IGD" - }, - {"totalpacketsent", (PyCFunction)UPnP_totalpacketsent, METH_NOARGS, - "return the total number of packets sent by UPnP IGD" - }, - {"totalpacketreceived", (PyCFunction)UPnP_totalpacketreceived, METH_NOARGS, - "return the total number of packets received by UPnP IGD" - }, - {"statusinfo", (PyCFunction)UPnP_statusinfo, METH_NOARGS, - "return status and uptime" - }, - {"connectiontype", (PyCFunction)UPnP_connectiontype, METH_NOARGS, - "return IGD WAN connection type" - }, - {"externalipaddress", (PyCFunction)UPnP_externalipaddress, METH_NOARGS, - "return external IP address" - }, - {"addportmapping", (PyCFunction)UPnP_addportmapping, METH_VARARGS, - "add a port mapping" - }, - {"deleteportmapping", (PyCFunction)UPnP_deleteportmapping, METH_VARARGS, - "delete a port mapping" - }, - {"getportmappingnumberofentries", (PyCFunction)UPnP_getportmappingnumberofentries, METH_NOARGS, - "-- non standard --" - }, - {"getspecificportmapping", (PyCFunction)UPnP_getspecificportmapping, METH_VARARGS, - "get details about a specific port mapping entry" - }, - {"getgenericportmapping", (PyCFunction)UPnP_getgenericportmapping, METH_VARARGS, - "get all details about the port mapping at index" - }, - {NULL} /* Sentinel */ -}; - -static PyTypeObject UPnPType = { - PyVarObject_HEAD_INIT(NULL, - 0) /*ob_size*/ - "miniupnpc.UPnP", /*tp_name*/ - sizeof(UPnPObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - (destructor)UPnPObject_dealloc,/*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - "UPnP objects", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - UPnP_methods, /* tp_methods */ - UPnP_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0,/*(initproc)UPnP_init,*/ /* tp_init */ - 0, /* tp_alloc */ -#ifndef _WIN32 - PyType_GenericNew,/*UPnP_new,*/ /* tp_new */ -#else - 0, -#endif -}; - -/* module methods */ -static PyMethodDef miniupnpc_methods[] = { - {NULL} /* Sentinel */ -}; - -#if PY_MAJOR_VERSION >= 3 -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "miniupnpc", /* m_name */ - "miniupnpc module.", /* m_doc */ - -1, /* m_size */ - miniupnpc_methods, /* m_methods */ - NULL, /* m_reload */ - NULL, /* m_traverse */ - NULL, /* m_clear */ - NULL, /* m_free */ -}; -#endif - -#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ -#define PyMODINIT_FUNC void -#endif - -PyMODINIT_FUNC -#if PY_MAJOR_VERSION >= 3 -PyInit_miniupnpc(void) -#else -initminiupnpc(void) -#endif -{ - PyObject* m; - -#ifdef _WIN32 - UPnPType.tp_new = PyType_GenericNew; -#endif - if (PyType_Ready(&UPnPType) < 0) - return; - -#if PY_MAJOR_VERSION >= 3 - m = PyModule_Create(&moduledef); -#else - m = Py_InitModule3("miniupnpc", miniupnpc_methods, - "miniupnpc module."); -#endif - - Py_INCREF(&UPnPType); - PyModule_AddObject(m, "UPnP", (PyObject *)&UPnPType); - -#if PY_MAJOR_VERSION >= 3 - return m; -#endif -} - diff --git a/Externals/miniupnpc/src/miniupnpcstrings.h b/Externals/miniupnpc/src/miniupnpcstrings.h deleted file mode 100644 index 426ecc2d33cb..000000000000 --- a/Externals/miniupnpc/src/miniupnpcstrings.h +++ /dev/null @@ -1,27 +0,0 @@ -/* $Id: miniupnpcstrings.h.in,v 1.5 2012/10/16 16:48:26 nanard Exp $ */ -/* Project: miniupnp - * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ - * Author: Thomas Bernard - * Copyright (c) 2005-2011 Thomas Bernard - * This software is subjects to the conditions detailed - * in the LICENCE file provided within this distribution */ -#ifndef MINIUPNPCSTRINGS_H_INCLUDED -#define MINIUPNPCSTRINGS_H_INCLUDED - -#if defined(_WIN32) -#define OS_STRING "Windows" -#elif defined(__linux__) -#define OS_STRING "Linux" -#elif defined(__OSX__) -#define OS_STRING "Mac OS X" -#elif defined(__APPLE__) -#define OS_STRING "Mac OS X" -#elif defined(__DARWIN__) -#define OS_STRING "Darwin" -#else -#define OS_STRING "Generic" -#endif -#define MINIUPNPC_VERSION_STRING "1.7" - -#endif - diff --git a/Externals/miniupnpc/src/miniupnpctypes.h b/Externals/miniupnpc/src/miniupnpctypes.h deleted file mode 100644 index 591c32fb60cf..000000000000 --- a/Externals/miniupnpc/src/miniupnpctypes.h +++ /dev/null @@ -1,19 +0,0 @@ -/* $Id: miniupnpctypes.h,v 1.2 2012/09/27 15:42:10 nanard Exp $ */ -/* Miniupnp project : http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org - * Author : Thomas Bernard - * Copyright (c) 2011 Thomas Bernard - * This software is subject to the conditions detailed in the - * LICENCE file provided within this distribution */ -#ifndef MINIUPNPCTYPES_H_INCLUDED -#define MINIUPNPCTYPES_H_INCLUDED - -#if (defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L) -#define UNSIGNED_INTEGER unsigned long long -#define STRTOUI strtoull -#else -#define UNSIGNED_INTEGER unsigned int -#define STRTOUI strtoul -#endif - -#endif - diff --git a/Externals/miniupnpc/src/miniwget.c b/Externals/miniupnpc/src/miniwget.c deleted file mode 100644 index 31f3af070e28..000000000000 --- a/Externals/miniupnpc/src/miniwget.c +++ /dev/null @@ -1,574 +0,0 @@ -/* $Id: miniwget.c,v 1.58 2012/08/11 05:52:49 nanard Exp $ */ -/* Project : miniupnp - * Website : http://miniupnp.free.fr/ - * Author : Thomas Bernard - * Copyright (c) 2005-2012 Thomas Bernard - * This software is subject to the conditions detailed in the - * LICENCE file provided in this distribution. */ - -#include -#include -#include -#include -#ifdef _WIN32 -#include -#include -#include -#define MAXHOSTNAMELEN 64 -#define MIN(x,y) (((x)<(y))?(x):(y)) -#define snprintf _snprintf -#define socklen_t int -#ifndef strncasecmp -#if defined(_MSC_VER) && (_MSC_VER >= 1400) -#define strncasecmp _memicmp -#else /* defined(_MSC_VER) && (_MSC_VER >= 1400) */ -#define strncasecmp memicmp -#endif /* defined(_MSC_VER) && (_MSC_VER >= 1400) */ -#endif /* #ifndef strncasecmp */ -#else /* #ifdef _WIN32 */ -#include -#include -#if defined(__amigaos__) && !defined(__amigaos4__) -#define socklen_t int -#else /* #if defined(__amigaos__) && !defined(__amigaos4__) */ -#include -#endif /* #else defined(__amigaos__) && !defined(__amigaos4__) */ -#include -#include -#include -#include -#include -#define closesocket close -/* defining MINIUPNPC_IGNORE_EINTR enable the ignore of interruptions - * during the connect() call */ -#define MINIUPNPC_IGNORE_EINTR -#endif /* #else _WIN32 */ -#if defined(__sun) || defined(sun) -#define MIN(x,y) (((x)<(y))?(x):(y)) -#endif - -#include "miniupnpcstrings.h" -#include "miniwget.h" -#include "connecthostport.h" -#include "receivedata.h" - -/* - * Read a HTTP response from a socket. - * Process Content-Length and Transfer-encoding headers. - * return a pointer to the content buffer, which length is saved - * to the length parameter. - */ -void * -getHTTPResponse(int s, int * size) -{ - char buf[2048]; - int n; - int endofheaders = 0; - int chunked = 0; - int content_length = -1; - unsigned int chunksize = 0; - unsigned int bytestocopy = 0; - /* buffers : */ - char * header_buf; - unsigned int header_buf_len = 2048; - unsigned int header_buf_used = 0; - char * content_buf; - unsigned int content_buf_len = 2048; - unsigned int content_buf_used = 0; - char chunksize_buf[32]; - unsigned int chunksize_buf_index; - - header_buf = malloc(header_buf_len); - content_buf = malloc(content_buf_len); - chunksize_buf[0] = '\0'; - chunksize_buf_index = 0; - - while((n = receivedata(s, buf, 2048, 5000, NULL)) > 0) - { - if(endofheaders == 0) - { - int i; - int linestart=0; - int colon=0; - int valuestart=0; - if(header_buf_used + n > header_buf_len) { - header_buf = realloc(header_buf, header_buf_used + n); - header_buf_len = header_buf_used + n; - } - memcpy(header_buf + header_buf_used, buf, n); - header_buf_used += n; - /* search for CR LF CR LF (end of headers) - * recognize also LF LF */ - i = 0; - while(i < ((int)header_buf_used-1) && (endofheaders == 0)) { - if(header_buf[i] == '\r') { - i++; - if(header_buf[i] == '\n') { - i++; - if(i < (int)header_buf_used && header_buf[i] == '\r') { - i++; - if(i < (int)header_buf_used && header_buf[i] == '\n') { - endofheaders = i+1; - } - } - } - } else if(header_buf[i] == '\n') { - i++; - if(header_buf[i] == '\n') { - endofheaders = i+1; - } - } - i++; - } - if(endofheaders == 0) - continue; - /* parse header lines */ - for(i = 0; i < endofheaders - 1; i++) { - if(colon <= linestart && header_buf[i]==':') - { - colon = i; - while(i < (endofheaders-1) - && (header_buf[i+1] == ' ' || header_buf[i+1] == '\t')) - i++; - valuestart = i + 1; - } - /* detecting end of line */ - else if(header_buf[i]=='\r' || header_buf[i]=='\n') - { - if(colon > linestart && valuestart > colon) - { -#ifdef DEBUG - printf("header='%.*s', value='%.*s'\n", - colon-linestart, header_buf+linestart, - i-valuestart, header_buf+valuestart); -#endif - if(0==strncasecmp(header_buf+linestart, "content-length", colon-linestart)) - { - content_length = atoi(header_buf+valuestart); -#ifdef DEBUG - printf("Content-Length: %d\n", content_length); -#endif - } - else if(0==strncasecmp(header_buf+linestart, "transfer-encoding", colon-linestart) - && 0==strncasecmp(header_buf+valuestart, "chunked", 7)) - { -#ifdef DEBUG - printf("chunked transfer-encoding!\n"); -#endif - chunked = 1; - } - } - while(header_buf[i]=='\r' || header_buf[i] == '\n') - i++; - linestart = i; - colon = linestart; - valuestart = 0; - } - } - /* copy the remaining of the received data back to buf */ - n = header_buf_used - endofheaders; - memcpy(buf, header_buf + endofheaders, n); - /* if(headers) */ - } - if(endofheaders) - { - /* content */ - if(chunked) - { - int i = 0; - while(i < n) - { - if(chunksize == 0) - { - /* reading chunk size */ - if(chunksize_buf_index == 0) { - /* skipping any leading CR LF */ - if(i= '0' - && chunksize_buf[j] <= '9') - chunksize = (chunksize << 4) + (chunksize_buf[j] - '0'); - else - chunksize = (chunksize << 4) + ((chunksize_buf[j] | 32) - 'a' + 10); - } - chunksize_buf[0] = '\0'; - chunksize_buf_index = 0; - i++; - } else { - /* not finished to get chunksize */ - continue; - } -#ifdef DEBUG - printf("chunksize = %u (%x)\n", chunksize, chunksize); -#endif - if(chunksize == 0) - { -#ifdef DEBUG - printf("end of HTTP content - %d %d\n", i, n); - /*printf("'%.*s'\n", n-i, buf+i);*/ -#endif - goto end_of_stream; - } - } - bytestocopy = ((int)chunksize < (n - i))?chunksize:(unsigned int)(n - i); - if((content_buf_used + bytestocopy) > content_buf_len) - { - if(content_length >= (int)(content_buf_used + bytestocopy)) { - content_buf_len = content_length; - } else { - content_buf_len = content_buf_used + bytestocopy; - } - content_buf = (char *)realloc((void *)content_buf, - content_buf_len); - } - memcpy(content_buf + content_buf_used, buf + i, bytestocopy); - content_buf_used += bytestocopy; - i += bytestocopy; - chunksize -= bytestocopy; - } - } - else - { - /* not chunked */ - if(content_length > 0 - && (int)(content_buf_used + n) > content_length) { - /* skipping additional bytes */ - n = content_length - content_buf_used; - } - if(content_buf_used + n > content_buf_len) - { - if(content_length >= (int)(content_buf_used + n)) { - content_buf_len = content_length; - } else { - content_buf_len = content_buf_used + n; - } - content_buf = (char *)realloc((void *)content_buf, - content_buf_len); - } - memcpy(content_buf + content_buf_used, buf, n); - content_buf_used += n; - } - } - /* use the Content-Length header value if available */ - if(content_length > 0 && (int)content_buf_used >= content_length) - { -#ifdef DEBUG - printf("End of HTTP content\n"); -#endif - break; - } - } -end_of_stream: - free(header_buf); header_buf = NULL; - *size = content_buf_used; - if(content_buf_used == 0) - { - free(content_buf); - content_buf = NULL; - } - return content_buf; -} - -/* miniwget3() : - * do all the work. - * Return NULL if something failed. */ -static void * -miniwget3(const char * host, - unsigned short port, const char * path, - int * size, char * addr_str, int addr_str_len, - const char * httpversion, unsigned int scope_id) -{ - char buf[2048]; - int s; - int n; - int len; - int sent; - void * content; - - *size = 0; - s = connecthostport(host, port, scope_id); - if(s < 0) - return NULL; - - /* get address for caller ! */ - if(addr_str) - { - struct sockaddr_storage saddr; - socklen_t saddrlen; - - saddrlen = sizeof(saddr); - if(getsockname(s, (struct sockaddr *)&saddr, &saddrlen) < 0) - { - perror("getsockname"); - } - else - { -#if defined(__amigaos__) && !defined(__amigaos4__) - /* using INT WINAPI WSAAddressToStringA(LPSOCKADDR, DWORD, LPWSAPROTOCOL_INFOA, LPSTR, LPDWORD); - * But his function make a string with the port : nn.nn.nn.nn:port */ -/* if(WSAAddressToStringA((SOCKADDR *)&saddr, sizeof(saddr), - NULL, addr_str, (DWORD *)&addr_str_len)) - { - printf("WSAAddressToStringA() failed : %d\n", WSAGetLastError()); - }*/ - /* the following code is only compatible with ip v4 addresses */ - strncpy(addr_str, inet_ntoa(((struct sockaddr_in *)&saddr)->sin_addr), addr_str_len); -#else -#if 0 - if(saddr.sa_family == AF_INET6) { - inet_ntop(AF_INET6, - &(((struct sockaddr_in6 *)&saddr)->sin6_addr), - addr_str, addr_str_len); - } else { - inet_ntop(AF_INET, - &(((struct sockaddr_in *)&saddr)->sin_addr), - addr_str, addr_str_len); - } -#endif - /* getnameinfo return ip v6 address with the scope identifier - * such as : 2a01:e35:8b2b:7330::%4281128194 */ - n = getnameinfo((const struct sockaddr *)&saddr, saddrlen, - addr_str, addr_str_len, - NULL, 0, - NI_NUMERICHOST | NI_NUMERICSERV); - if(n != 0) { -#ifdef _WIN32 - fprintf(stderr, "getnameinfo() failed : %d\n", n); -#else - fprintf(stderr, "getnameinfo() failed : %s\n", gai_strerror(n)); -#endif - } -#endif - } -#ifdef DEBUG - printf("address miniwget : %s\n", addr_str); -#endif - } - - len = snprintf(buf, sizeof(buf), - "GET %s HTTP/%s\r\n" - "Host: %s:%d\r\n" - "Connection: Close\r\n" - "User-Agent: " OS_STRING ", UPnP/1.0, MiniUPnPc/" MINIUPNPC_VERSION_STRING "\r\n" - - "\r\n", - path, httpversion, host, port); - sent = 0; - /* sending the HTTP request */ - while(sent < len) - { - n = send(s, buf+sent, len-sent, 0); - if(n < 0) - { - perror("send"); - closesocket(s); - return NULL; - } - else - { - sent += n; - } - } - content = getHTTPResponse(s, size); - closesocket(s); - return content; -} - -/* miniwget2() : - * Call miniwget3(); retry with HTTP/1.1 if 1.0 fails. */ -static void * -miniwget2(const char * host, - unsigned short port, const char * path, - int * size, char * addr_str, int addr_str_len, - unsigned int scope_id) -{ - char * respbuffer; - -#if 1 - respbuffer = miniwget3(host, port, path, size, - addr_str, addr_str_len, "1.1", scope_id); -#else - respbuffer = miniwget3(host, port, path, size, - addr_str, addr_str_len, "1.0", scope_id); - if (*size == 0) - { -#ifdef DEBUG - printf("Retrying with HTTP/1.1\n"); -#endif - free(respbuffer); - respbuffer = miniwget3(host, port, path, size, - addr_str, addr_str_len, "1.1", scope_id); - } -#endif - return respbuffer; -} - - - - -/* parseURL() - * arguments : - * url : source string not modified - * hostname : hostname destination string (size of MAXHOSTNAMELEN+1) - * port : port (destination) - * path : pointer to the path part of the URL - * - * Return values : - * 0 - Failure - * 1 - Success */ -int -parseURL(const char * url, - char * hostname, unsigned short * port, - char * * path, unsigned int * scope_id) -{ - char * p1, *p2, *p3; - if(!url) - return 0; - p1 = strstr(url, "://"); - if(!p1) - return 0; - p1 += 3; - if( (url[0]!='h') || (url[1]!='t') - ||(url[2]!='t') || (url[3]!='p')) - return 0; - memset(hostname, 0, MAXHOSTNAMELEN + 1); - if(*p1 == '[') - { - /* IP v6 : http://[2a00:1450:8002::6a]/path/abc */ - char * scope; - scope = strchr(p1, '%'); - p2 = strchr(p1, ']'); - if(p2 && scope && scope < p2 && scope_id) { - /* parse scope */ -#ifdef IF_NAMESIZE - char tmp[IF_NAMESIZE]; - int l; - scope++; - /* "%25" is just '%' in URL encoding */ - if(scope[0] == '2' && scope[1] == '5') - scope += 2; /* skip "25" */ - l = p2 - scope; - if(l >= IF_NAMESIZE) - l = IF_NAMESIZE - 1; - memcpy(tmp, scope, l); - tmp[l] = '\0'; - *scope_id = if_nametoindex(tmp); - if(*scope_id == 0) { - *scope_id = (unsigned int)strtoul(tmp, NULL, 10); - } -#else - /* under windows, scope is numerical */ - char tmp[8]; - int l; - scope++; - /* "%25" is just '%' in URL encoding */ - if(scope[0] == '2' && scope[1] == '5') - scope += 2; /* skip "25" */ - l = p2 - scope; - if(l >= sizeof(tmp)) - l = sizeof(tmp) - 1; - memcpy(tmp, scope, l); - tmp[l] = '\0'; - *scope_id = (unsigned int)strtoul(tmp, NULL, 10); -#endif - } - p3 = strchr(p1, '/'); - if(p2 && p3) - { - p2++; - strncpy(hostname, p1, MIN(MAXHOSTNAMELEN, (int)(p2-p1))); - if(*p2 == ':') - { - *port = 0; - p2++; - while( (*p2 >= '0') && (*p2 <= '9')) - { - *port *= 10; - *port += (unsigned short)(*p2 - '0'); - p2++; - } - } - else - { - *port = 80; - } - *path = p3; - return 1; - } - } - p2 = strchr(p1, ':'); - p3 = strchr(p1, '/'); - if(!p3) - return 0; - if(!p2 || (p2>p3)) - { - strncpy(hostname, p1, MIN(MAXHOSTNAMELEN, (int)(p3-p1))); - *port = 80; - } - else - { - strncpy(hostname, p1, MIN(MAXHOSTNAMELEN, (int)(p2-p1))); - *port = 0; - p2++; - while( (*p2 >= '0') && (*p2 <= '9')) - { - *port *= 10; - *port += (unsigned short)(*p2 - '0'); - p2++; - } - } - *path = p3; - return 1; -} - -void * -miniwget(const char * url, int * size, unsigned int scope_id) -{ - unsigned short port; - char * path; - /* protocol://host:port/chemin */ - char hostname[MAXHOSTNAMELEN+1]; - *size = 0; - if(!parseURL(url, hostname, &port, &path, &scope_id)) - return NULL; -#ifdef DEBUG - printf("parsed url : hostname='%s' port=%hu path='%s' scope_id=%u\n", - hostname, port, path, scope_id); -#endif - return miniwget2(hostname, port, path, size, 0, 0, scope_id); -} - -void * -miniwget_getaddr(const char * url, int * size, - char * addr, int addrlen, unsigned int scope_id) -{ - unsigned short port; - char * path; - /* protocol://host:port/path */ - char hostname[MAXHOSTNAMELEN+1]; - *size = 0; - if(addr) - addr[0] = '\0'; - if(!parseURL(url, hostname, &port, &path, &scope_id)) - return NULL; -#ifdef DEBUG - printf("parsed url : hostname='%s' port=%hu path='%s' scope_id=%u\n", - hostname, port, path, scope_id); -#endif - return miniwget2(hostname, port, path, size, addr, addrlen, scope_id); -} - diff --git a/Externals/miniupnpc/src/miniwget.h b/Externals/miniupnpc/src/miniwget.h deleted file mode 100644 index 31bcea322a36..000000000000 --- a/Externals/miniupnpc/src/miniwget.h +++ /dev/null @@ -1,30 +0,0 @@ -/* $Id: miniwget.h,v 1.8 2012/09/27 15:42:10 nanard Exp $ */ -/* Project : miniupnp - * Author : Thomas Bernard - * Copyright (c) 2005-2012 Thomas Bernard - * This software is subject to the conditions detailed in the - * LICENCE file provided in this distribution. - * */ -#ifndef MINIWGET_H_INCLUDED -#define MINIWGET_H_INCLUDED - -#include "declspec.h" - -#ifdef __cplusplus -extern "C" { -#endif - -LIBSPEC void * getHTTPResponse(int s, int * size); - -LIBSPEC void * miniwget(const char *, int *, unsigned int); - -LIBSPEC void * miniwget_getaddr(const char *, int *, char *, int, unsigned int); - -int parseURL(const char *, char *, unsigned short *, char * *, unsigned int *); - -#ifdef __cplusplus -} -#endif - -#endif - diff --git a/Externals/miniupnpc/src/minixml.c b/Externals/miniupnpc/src/minixml.c deleted file mode 100644 index eb4d7d985753..000000000000 --- a/Externals/miniupnpc/src/minixml.c +++ /dev/null @@ -1,216 +0,0 @@ -/* $Id: minixml.c,v 1.10 2012/03/05 19:42:47 nanard Exp $ */ -/* minixml.c : the minimum size a xml parser can be ! */ -/* Project : miniupnp - * webpage: http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ - * Author : Thomas Bernard - -Copyright (c) 2005-2011, Thomas BERNARD -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. -*/ -#include -#include "minixml.h" - -/* parseatt : used to parse the argument list - * return 0 (false) in case of success and -1 (true) if the end - * of the xmlbuffer is reached. */ -static int parseatt(struct xmlparser * p) -{ - const char * attname; - int attnamelen; - const char * attvalue; - int attvaluelen; - while(p->xml < p->xmlend) - { - if(*p->xml=='/' || *p->xml=='>') - return 0; - if( !IS_WHITE_SPACE(*p->xml) ) - { - char sep; - attname = p->xml; - attnamelen = 0; - while(*p->xml!='=' && !IS_WHITE_SPACE(*p->xml) ) - { - attnamelen++; p->xml++; - if(p->xml >= p->xmlend) - return -1; - } - while(*(p->xml++) != '=') - { - if(p->xml >= p->xmlend) - return -1; - } - while(IS_WHITE_SPACE(*p->xml)) - { - p->xml++; - if(p->xml >= p->xmlend) - return -1; - } - sep = *p->xml; - if(sep=='\'' || sep=='\"') - { - p->xml++; - if(p->xml >= p->xmlend) - return -1; - attvalue = p->xml; - attvaluelen = 0; - while(*p->xml != sep) - { - attvaluelen++; p->xml++; - if(p->xml >= p->xmlend) - return -1; - } - } - else - { - attvalue = p->xml; - attvaluelen = 0; - while( !IS_WHITE_SPACE(*p->xml) - && *p->xml != '>' && *p->xml != '/') - { - attvaluelen++; p->xml++; - if(p->xml >= p->xmlend) - return -1; - } - } - /*printf("%.*s='%.*s'\n", - attnamelen, attname, attvaluelen, attvalue);*/ - if(p->attfunc) - p->attfunc(p->data, attname, attnamelen, attvalue, attvaluelen); - } - p->xml++; - } - return -1; -} - -/* parseelt parse the xml stream and - * call the callback functions when needed... */ -static void parseelt(struct xmlparser * p) -{ - int i; - const char * elementname; - while(p->xml < (p->xmlend - 1)) - { - if((p->xml)[0]=='<' && (p->xml)[1]!='?') - { - i = 0; elementname = ++p->xml; - while( !IS_WHITE_SPACE(*p->xml) - && (*p->xml!='>') && (*p->xml!='/') - ) - { - i++; p->xml++; - if (p->xml >= p->xmlend) - return; - /* to ignore namespace : */ - if(*p->xml==':') - { - i = 0; - elementname = ++p->xml; - } - } - if(i>0) - { - if(p->starteltfunc) - p->starteltfunc(p->data, elementname, i); - if(parseatt(p)) - return; - if(*p->xml!='/') - { - const char * data; - i = 0; data = ++p->xml; - if (p->xml >= p->xmlend) - return; - while( IS_WHITE_SPACE(*p->xml) ) - { - i++; p->xml++; - if (p->xml >= p->xmlend) - return; - } - if(memcmp(p->xml, "xml += 9; - data = p->xml; - i = 0; - while(memcmp(p->xml, "]]>", 3) != 0) - { - i++; p->xml++; - if ((p->xml + 3) >= p->xmlend) - return; - } - if(i>0 && p->datafunc) - p->datafunc(p->data, data, i); - while(*p->xml!='<') - { - p->xml++; - if (p->xml >= p->xmlend) - return; - } - } - else - { - while(*p->xml!='<') - { - i++; p->xml++; - if ((p->xml + 1) >= p->xmlend) - return; - } - if(i>0 && p->datafunc && *(p->xml + 1) == '/') - p->datafunc(p->data, data, i); - } - } - } - else if(*p->xml == '/') - { - i = 0; elementname = ++p->xml; - if (p->xml >= p->xmlend) - return; - while((*p->xml != '>')) - { - i++; p->xml++; - if (p->xml >= p->xmlend) - return; - } - if(p->endeltfunc) - p->endeltfunc(p->data, elementname, i); - p->xml++; - } - } - else - { - p->xml++; - } - } -} - -/* the parser must be initialized before calling this function */ -void parsexml(struct xmlparser * parser) -{ - parser->xml = parser->xmlstart; - parser->xmlend = parser->xmlstart + parser->xmlsize; - parseelt(parser); -} - - diff --git a/Externals/miniupnpc/src/minixml.h b/Externals/miniupnpc/src/minixml.h deleted file mode 100644 index 9f43aa48cbb4..000000000000 --- a/Externals/miniupnpc/src/minixml.h +++ /dev/null @@ -1,37 +0,0 @@ -/* $Id: minixml.h,v 1.7 2012/09/27 15:42:10 nanard Exp $ */ -/* minimal xml parser - * - * Project : miniupnp - * Website : http://miniupnp.free.fr/ - * Author : Thomas Bernard - * Copyright (c) 2005 Thomas Bernard - * This software is subject to the conditions detailed in the - * LICENCE file provided in this distribution. - * */ -#ifndef MINIXML_H_INCLUDED -#define MINIXML_H_INCLUDED -#define IS_WHITE_SPACE(c) ((c==' ') || (c=='\t') || (c=='\r') || (c=='\n')) - -/* if a callback function pointer is set to NULL, - * the function is not called */ -struct xmlparser { - const char *xmlstart; - const char *xmlend; - const char *xml; /* pointer to current character */ - int xmlsize; - void * data; - void (*starteltfunc) (void *, const char *, int); - void (*endeltfunc) (void *, const char *, int); - void (*datafunc) (void *, const char *, int); - void (*attfunc) (void *, const char *, int, const char *, int); -}; - -/* parsexml() - * the xmlparser structure must be initialized before the call - * the following structure members have to be initialized : - * xmlstart, xmlsize, data, *func - * xml is for internal usage, xmlend is computed automatically */ -void parsexml(struct xmlparser *); - -#endif - diff --git a/Externals/miniupnpc/src/portlistingparse.c b/Externals/miniupnpc/src/portlistingparse.c deleted file mode 100644 index 19e3054ebb5c..000000000000 --- a/Externals/miniupnpc/src/portlistingparse.c +++ /dev/null @@ -1,159 +0,0 @@ -/* $Id: portlistingparse.c,v 1.6 2012/05/29 10:26:51 nanard Exp $ */ -/* MiniUPnP project - * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ - * (c) 2011 Thomas Bernard - * This software is subject to the conditions detailed - * in the LICENCE file provided within the distribution */ -#include -#include -#include "portlistingparse.h" -#include "minixml.h" - -/* list of the elements */ -static const struct { - const portMappingElt code; - const char * const str; -} elements[] = { - { PortMappingEntry, "PortMappingEntry"}, - { NewRemoteHost, "NewRemoteHost"}, - { NewExternalPort, "NewExternalPort"}, - { NewProtocol, "NewProtocol"}, - { NewInternalPort, "NewInternalPort"}, - { NewInternalClient, "NewInternalClient"}, - { NewEnabled, "NewEnabled"}, - { NewDescription, "NewDescription"}, - { NewLeaseTime, "NewLeaseTime"}, - { PortMappingEltNone, NULL} -}; - -/* Helper function */ -static UNSIGNED_INTEGER -atoui(const char * p, int l) -{ - UNSIGNED_INTEGER r = 0; - while(l > 0 && *p) - { - if(*p >= '0' && *p <= '9') - r = r*10 + (*p - '0'); - else - break; - p++; - l--; - } - return r; -} - -/* Start element handler */ -static void -startelt(void * d, const char * name, int l) -{ - int i; - struct PortMappingParserData * pdata = (struct PortMappingParserData *)d; - pdata->curelt = PortMappingEltNone; - for(i = 0; elements[i].str; i++) - { - if(memcmp(name, elements[i].str, l) == 0) - { - pdata->curelt = elements[i].code; - break; - } - } - if(pdata->curelt == PortMappingEntry) - { - struct PortMapping * pm; - pm = calloc(1, sizeof(struct PortMapping)); - LIST_INSERT_HEAD( &(pdata->head), pm, entries); - } -} - -/* End element handler */ -static void -endelt(void * d, const char * name, int l) -{ - struct PortMappingParserData * pdata = (struct PortMappingParserData *)d; - (void)name; - (void)l; - pdata->curelt = PortMappingEltNone; -} - -/* Data handler */ -static void -data(void * d, const char * data, int l) -{ - struct PortMapping * pm; - struct PortMappingParserData * pdata = (struct PortMappingParserData *)d; - pm = pdata->head.lh_first; - if(!pm) - return; - if(l > 63) - l = 63; - switch(pdata->curelt) - { - case NewRemoteHost: - memcpy(pm->remoteHost, data, l); - pm->remoteHost[l] = '\0'; - break; - case NewExternalPort: - pm->externalPort = (unsigned short)atoui(data, l); - break; - case NewProtocol: - if(l > 3) - l = 3; - memcpy(pm->protocol, data, l); - pm->protocol[l] = '\0'; - break; - case NewInternalPort: - pm->internalPort = (unsigned short)atoui(data, l); - break; - case NewInternalClient: - memcpy(pm->internalClient, data, l); - pm->internalClient[l] = '\0'; - break; - case NewEnabled: - pm->enabled = (unsigned char)atoui(data, l); - break; - case NewDescription: - memcpy(pm->description, data, l); - pm->description[l] = '\0'; - break; - case NewLeaseTime: - pm->leaseTime = atoui(data, l); - break; - default: - break; - } -} - - -/* Parse the PortMappingList XML document for IGD version 2 - */ -void -ParsePortListing(const char * buffer, int bufsize, - struct PortMappingParserData * pdata) -{ - struct xmlparser parser; - - memset(pdata, 0, sizeof(struct PortMappingParserData)); - LIST_INIT(&(pdata->head)); - /* init xmlparser */ - parser.xmlstart = buffer; - parser.xmlsize = bufsize; - parser.data = pdata; - parser.starteltfunc = startelt; - parser.endeltfunc = endelt; - parser.datafunc = data; - parser.attfunc = 0; - parsexml(&parser); -} - -void -FreePortListing(struct PortMappingParserData * pdata) -{ - struct PortMapping * pm; - while((pm = pdata->head.lh_first) != NULL) - { - LIST_REMOVE(pm, entries); - free(pm); - } -} - diff --git a/Externals/miniupnpc/src/portlistingparse.h b/Externals/miniupnpc/src/portlistingparse.h deleted file mode 100644 index bafa2a47c36c..000000000000 --- a/Externals/miniupnpc/src/portlistingparse.h +++ /dev/null @@ -1,71 +0,0 @@ -/* $Id: portlistingparse.h,v 1.7 2012/09/27 15:42:10 nanard Exp $ */ -/* MiniUPnP project - * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ - * (c) 2011-2012 Thomas Bernard - * This software is subject to the conditions detailed - * in the LICENCE file provided within the distribution */ -#ifndef PORTLISTINGPARSE_H_INCLUDED -#define PORTLISTINGPARSE_H_INCLUDED - -#include "declspec.h" -/* for the definition of UNSIGNED_INTEGER */ -#include "miniupnpctypes.h" - -#if defined(NO_SYS_QUEUE_H) || defined(_WIN32) || defined(__HAIKU__) -#include "bsdqueue.h" -#else -#include -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -/* sample of PortMappingEntry : - - 202.233.2.1 - 2345 - TCP - 2345 - 192.168.1.137 - 1 - dooom - 345 - - */ -typedef enum { PortMappingEltNone, - PortMappingEntry, NewRemoteHost, - NewExternalPort, NewProtocol, - NewInternalPort, NewInternalClient, - NewEnabled, NewDescription, - NewLeaseTime } portMappingElt; - -struct PortMapping { - LIST_ENTRY(PortMapping) entries; - UNSIGNED_INTEGER leaseTime; - unsigned short externalPort; - unsigned short internalPort; - char remoteHost[64]; - char internalClient[64]; - char description[64]; - char protocol[4]; - unsigned char enabled; -}; - -struct PortMappingParserData { - LIST_HEAD(portmappinglisthead, PortMapping) head; - portMappingElt curelt; -}; - -LIBSPEC void -ParsePortListing(const char * buffer, int bufsize, - struct PortMappingParserData * pdata); - -LIBSPEC void -FreePortListing(struct PortMappingParserData * pdata); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/Externals/miniupnpc/src/receivedata.c b/Externals/miniupnpc/src/receivedata.c deleted file mode 100644 index f9b9901fc039..000000000000 --- a/Externals/miniupnpc/src/receivedata.c +++ /dev/null @@ -1,104 +0,0 @@ -/* $Id: receivedata.c,v 1.4 2012/06/23 22:34:47 nanard Exp $ */ -/* Project : miniupnp - * Website : http://miniupnp.free.fr/ - * Author : Thomas Bernard - * Copyright (c) 2011-2012 Thomas Bernard - * This software is subject to the conditions detailed in the - * LICENCE file provided in this distribution. */ - -#include -#ifdef _WIN32 -#include -#include -#else -#include -#if defined(__amigaos__) && !defined(__amigaos4__) -#define socklen_t int -#else /* #if defined(__amigaos__) && !defined(__amigaos4__) */ -#include -#endif /* #else defined(__amigaos__) && !defined(__amigaos4__) */ -#include -#include -#if !defined(__amigaos__) && !defined(__amigaos4__) -#include -#endif -#include -#define MINIUPNPC_IGNORE_EINTR -#endif - -#ifdef _WIN32 -#define PRINT_SOCKET_ERROR(x) printf("Socket error: %s, %d\n", x, WSAGetLastError()); -#else -#define PRINT_SOCKET_ERROR(x) perror(x) -#endif - -#include "receivedata.h" - -int -receivedata(int socket, - char * data, int length, - int timeout, unsigned int * scope_id) -{ -#if MINIUPNPC_GET_SRC_ADDR - struct sockaddr_storage src_addr; - socklen_t src_addr_len = sizeof(src_addr); -#endif - int n; -#if !defined(_WIN32) && !defined(__amigaos__) && !defined(__amigaos4__) - /* using poll */ - struct pollfd fds[1]; /* for the poll */ -#ifdef MINIUPNPC_IGNORE_EINTR - do { -#endif - fds[0].fd = socket; - fds[0].events = POLLIN; - n = poll(fds, 1, timeout); -#ifdef MINIUPNPC_IGNORE_EINTR - } while(n < 0 && errno == EINTR); -#endif - if(n < 0) { - PRINT_SOCKET_ERROR("poll"); - return -1; - } else if(n == 0) { - /* timeout */ - return 0; - } -#else /* !defined(_WIN32) && !defined(__amigaos__) && !defined(__amigaos4__) */ - /* using select under _WIN32 and amigaos */ - fd_set socketSet; - TIMEVAL timeval; - FD_ZERO(&socketSet); - FD_SET(socket, &socketSet); - timeval.tv_sec = timeout / 1000; - timeval.tv_usec = (timeout % 1000) * 1000; - n = select(FD_SETSIZE, &socketSet, NULL, NULL, &timeval); - if(n < 0) { - PRINT_SOCKET_ERROR("select"); - return -1; - } else if(n == 0) { - return 0; - } -#endif -#if MINIUPNPC_GET_SRC_ADDR - n = recvfrom(socket, data, length, 0, - (struct sockaddr *)&src_addr, &src_addr_len); -#else - n = recv(socket, data, length, 0); -#endif - if(n<0) { - PRINT_SOCKET_ERROR("recv"); - } -#if MINIUPNPC_GET_SRC_ADDR - if (src_addr.ss_family == AF_INET6) { - const struct sockaddr_in6 * src_addr6 = (struct sockaddr_in6 *)&src_addr; -#ifdef DEBUG - printf("scope_id=%u\n", src_addr6->sin6_scope_id); -#endif - if(scope_id) - *scope_id = src_addr6->sin6_scope_id; - } -#endif - return n; -} - - diff --git a/Externals/miniupnpc/src/receivedata.h b/Externals/miniupnpc/src/receivedata.h deleted file mode 100644 index 0520a11d388b..000000000000 --- a/Externals/miniupnpc/src/receivedata.h +++ /dev/null @@ -1,19 +0,0 @@ -/* $Id: receivedata.h,v 1.4 2012/09/27 15:42:10 nanard Exp $ */ -/* Project: miniupnp - * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ - * Author: Thomas Bernard - * Copyright (c) 2011-2012 Thomas Bernard - * This software is subjects to the conditions detailed - * in the LICENCE file provided within this distribution */ -#ifndef RECEIVEDATA_H_INCLUDED -#define RECEIVEDATA_H_INCLUDED - -/* Reads data from the specified socket. - * Returns the number of bytes read if successful, zero if no bytes were - * read or if we timed out. Returns negative if there was an error. */ -int receivedata(int socket, - char * data, int length, - int timeout, unsigned int * scope_id); - -#endif - diff --git a/Externals/miniupnpc/src/upnpc.c b/Externals/miniupnpc/src/upnpc.c deleted file mode 100644 index 54cddf9a07c9..000000000000 --- a/Externals/miniupnpc/src/upnpc.c +++ /dev/null @@ -1,715 +0,0 @@ -/* $Id: upnpc.c,v 1.99 2013/02/06 12:56:41 nanard Exp $ */ -/* Project : miniupnp - * Author : Thomas Bernard - * Copyright (c) 2005-2013 Thomas Bernard - * This software is subject to the conditions detailed in the - * LICENCE file provided in this distribution. */ - -#include -#include -#include -#include -#ifdef _WIN32 -#include -#define snprintf _snprintf -#else -/* for IPPROTO_TCP / IPPROTO_UDP */ -#include -#endif -#include "miniwget.h" -#include "miniupnpc.h" -#include "upnpcommands.h" -#include "upnperrors.h" - -/* protofix() checks if protocol is "UDP" or "TCP" - * returns NULL if not */ -const char * protofix(const char * proto) -{ - static const char proto_tcp[4] = { 'T', 'C', 'P', 0}; - static const char proto_udp[4] = { 'U', 'D', 'P', 0}; - int i, b; - for(i=0, b=1; i<4; i++) - b = b && ( (proto[i] == proto_tcp[i]) - || (proto[i] == (proto_tcp[i] | 32)) ); - if(b) - return proto_tcp; - for(i=0, b=1; i<4; i++) - b = b && ( (proto[i] == proto_udp[i]) - || (proto[i] == (proto_udp[i] | 32)) ); - if(b) - return proto_udp; - return 0; -} - -static void DisplayInfos(struct UPNPUrls * urls, - struct IGDdatas * data) -{ - char externalIPAddress[40]; - char connectionType[64]; - char status[64]; - char lastconnerr[64]; - unsigned int uptime; - unsigned int brUp, brDown; - time_t timenow, timestarted; - int r; - if(UPNP_GetConnectionTypeInfo(urls->controlURL, - data->first.servicetype, - connectionType) != UPNPCOMMAND_SUCCESS) - printf("GetConnectionTypeInfo failed.\n"); - else - printf("Connection Type : %s\n", connectionType); - if(UPNP_GetStatusInfo(urls->controlURL, data->first.servicetype, - status, &uptime, lastconnerr) != UPNPCOMMAND_SUCCESS) - printf("GetStatusInfo failed.\n"); - else - printf("Status : %s, uptime=%us, LastConnectionError : %s\n", - status, uptime, lastconnerr); - timenow = time(NULL); - timestarted = timenow - uptime; - printf(" Time started : %s", ctime(×tarted)); - if(UPNP_GetLinkLayerMaxBitRates(urls->controlURL_CIF, data->CIF.servicetype, - &brDown, &brUp) != UPNPCOMMAND_SUCCESS) { - printf("GetLinkLayerMaxBitRates failed.\n"); - } else { - printf("MaxBitRateDown : %u bps", brDown); - if(brDown >= 1000000) { - printf(" (%u.%u Mbps)", brDown / 1000000, (brDown / 100000) % 10); - } else if(brDown >= 1000) { - printf(" (%u Kbps)", brDown / 1000); - } - printf(" MaxBitRateUp %u bps", brUp); - if(brUp >= 1000000) { - printf(" (%u.%u Mbps)", brUp / 1000000, (brUp / 100000) % 10); - } else if(brUp >= 1000) { - printf(" (%u Kbps)", brUp / 1000); - } - printf("\n"); - } - r = UPNP_GetExternalIPAddress(urls->controlURL, - data->first.servicetype, - externalIPAddress); - if(r != UPNPCOMMAND_SUCCESS) { - printf("GetExternalIPAddress failed. (errorcode=%d)\n", r); - } else { - printf("ExternalIPAddress = %s\n", externalIPAddress); - } -} - -static void GetConnectionStatus(struct UPNPUrls * urls, - struct IGDdatas * data) -{ - unsigned int bytessent, bytesreceived, packetsreceived, packetssent; - DisplayInfos(urls, data); - bytessent = UPNP_GetTotalBytesSent(urls->controlURL_CIF, data->CIF.servicetype); - bytesreceived = UPNP_GetTotalBytesReceived(urls->controlURL_CIF, data->CIF.servicetype); - packetssent = UPNP_GetTotalPacketsSent(urls->controlURL_CIF, data->CIF.servicetype); - packetsreceived = UPNP_GetTotalPacketsReceived(urls->controlURL_CIF, data->CIF.servicetype); - printf("Bytes: Sent: %8u\tRecv: %8u\n", bytessent, bytesreceived); - printf("Packets: Sent: %8u\tRecv: %8u\n", packetssent, packetsreceived); -} - -static void ListRedirections(struct UPNPUrls * urls, - struct IGDdatas * data) -{ - int r; - int i = 0; - char index[6]; - char intClient[40]; - char intPort[6]; - char extPort[6]; - char protocol[4]; - char desc[80]; - char enabled[6]; - char rHost[64]; - char duration[16]; - /*unsigned int num=0; - UPNP_GetPortMappingNumberOfEntries(urls->controlURL, data->servicetype, &num); - printf("PortMappingNumberOfEntries : %u\n", num);*/ - printf(" i protocol exPort->inAddr:inPort description remoteHost leaseTime\n"); - do { - snprintf(index, 6, "%d", i); - rHost[0] = '\0'; enabled[0] = '\0'; - duration[0] = '\0'; desc[0] = '\0'; - extPort[0] = '\0'; intPort[0] = '\0'; intClient[0] = '\0'; - r = UPNP_GetGenericPortMappingEntry(urls->controlURL, - data->first.servicetype, - index, - extPort, intClient, intPort, - protocol, desc, enabled, - rHost, duration); - if(r==0) - /* - printf("%02d - %s %s->%s:%s\tenabled=%s leaseDuration=%s\n" - " desc='%s' rHost='%s'\n", - i, protocol, extPort, intClient, intPort, - enabled, duration, - desc, rHost); - */ - printf("%2d %s %5s->%s:%-5s '%s' '%s' %s\n", - i, protocol, extPort, intClient, intPort, - desc, rHost, duration); - else - printf("GetGenericPortMappingEntry() returned %d (%s)\n", - r, strupnperror(r)); - i++; - } while(r==0); -} - -static void NewListRedirections(struct UPNPUrls * urls, - struct IGDdatas * data) -{ - int r; - int i = 0; - struct PortMappingParserData pdata; - struct PortMapping * pm; - - memset(&pdata, 0, sizeof(struct PortMappingParserData)); - r = UPNP_GetListOfPortMappings(urls->controlURL, - data->first.servicetype, - "0", - "65535", - "TCP", - "1000", - &pdata); - if(r == UPNPCOMMAND_SUCCESS) - { - printf(" i protocol exPort->inAddr:inPort description remoteHost leaseTime\n"); - for(pm = pdata.head.lh_first; pm != NULL; pm = pm->entries.le_next) - { - printf("%2d %s %5hu->%s:%-5hu '%s' '%s' %u\n", - i, pm->protocol, pm->externalPort, pm->internalClient, - pm->internalPort, - pm->description, pm->remoteHost, - (unsigned)pm->leaseTime); - i++; - } - FreePortListing(&pdata); - } - else - { - printf("GetListOfPortMappings() returned %d (%s)\n", - r, strupnperror(r)); - } - r = UPNP_GetListOfPortMappings(urls->controlURL, - data->first.servicetype, - "0", - "65535", - "UDP", - "1000", - &pdata); - if(r == UPNPCOMMAND_SUCCESS) - { - for(pm = pdata.head.lh_first; pm != NULL; pm = pm->entries.le_next) - { - printf("%2d %s %5hu->%s:%-5hu '%s' '%s' %u\n", - i, pm->protocol, pm->externalPort, pm->internalClient, - pm->internalPort, - pm->description, pm->remoteHost, - (unsigned)pm->leaseTime); - i++; - } - FreePortListing(&pdata); - } - else - { - printf("GetListOfPortMappings() returned %d (%s)\n", - r, strupnperror(r)); - } -} - -/* Test function - * 1 - get connection type - * 2 - get extenal ip address - * 3 - Add port mapping - * 4 - get this port mapping from the IGD */ -static void SetRedirectAndTest(struct UPNPUrls * urls, - struct IGDdatas * data, - const char * iaddr, - const char * iport, - const char * eport, - const char * proto, - const char * leaseDuration, - const char * description) -{ - char externalIPAddress[40]; - char intClient[40]; - char intPort[6]; - char duration[16]; - int r; - - if(!iaddr || !iport || !eport || !proto) - { - fprintf(stderr, "Wrong arguments\n"); - return; - } - proto = protofix(proto); - if(!proto) - { - fprintf(stderr, "invalid protocol\n"); - return; - } - - UPNP_GetExternalIPAddress(urls->controlURL, - data->first.servicetype, - externalIPAddress); - if(externalIPAddress[0]) - printf("ExternalIPAddress = %s\n", externalIPAddress); - else - printf("GetExternalIPAddress failed.\n"); - - r = UPNP_AddPortMapping(urls->controlURL, data->first.servicetype, - eport, iport, iaddr, description, - proto, 0, leaseDuration); - if(r!=UPNPCOMMAND_SUCCESS) - printf("AddPortMapping(%s, %s, %s) failed with code %d (%s)\n", - eport, iport, iaddr, r, strupnperror(r)); - - r = UPNP_GetSpecificPortMappingEntry(urls->controlURL, - data->first.servicetype, - eport, proto, - intClient, intPort, NULL/*desc*/, - NULL/*enabled*/, duration); - if(r!=UPNPCOMMAND_SUCCESS) - printf("GetSpecificPortMappingEntry() failed with code %d (%s)\n", - r, strupnperror(r)); - - if(intClient[0]) { - printf("InternalIP:Port = %s:%s\n", intClient, intPort); - printf("external %s:%s %s is redirected to internal %s:%s (duration=%s)\n", - externalIPAddress, eport, proto, intClient, intPort, duration); - } -} - -static void -RemoveRedirect(struct UPNPUrls * urls, - struct IGDdatas * data, - const char * eport, - const char * proto) -{ - int r; - if(!proto || !eport) - { - fprintf(stderr, "invalid arguments\n"); - return; - } - proto = protofix(proto); - if(!proto) - { - fprintf(stderr, "protocol invalid\n"); - return; - } - r = UPNP_DeletePortMapping(urls->controlURL, data->first.servicetype, eport, proto, 0); - printf("UPNP_DeletePortMapping() returned : %d\n", r); -} - -/* IGD:2, functions for service WANIPv6FirewallControl:1 */ -static void GetFirewallStatus(struct UPNPUrls * urls, struct IGDdatas * data) -{ - unsigned int bytessent, bytesreceived, packetsreceived, packetssent; - int firewallEnabled = 0, inboundPinholeAllowed = 0; - - UPNP_GetFirewallStatus(urls->controlURL_6FC, data->IPv6FC.servicetype, &firewallEnabled, &inboundPinholeAllowed); - printf("FirewallEnabled: %d & Inbound Pinhole Allowed: %d\n", firewallEnabled, inboundPinholeAllowed); - printf("GetFirewallStatus:\n Firewall Enabled: %s\n Inbound Pinhole Allowed: %s\n", (firewallEnabled)? "Yes":"No", (inboundPinholeAllowed)? "Yes":"No"); - - bytessent = UPNP_GetTotalBytesSent(urls->controlURL_CIF, data->CIF.servicetype); - bytesreceived = UPNP_GetTotalBytesReceived(urls->controlURL_CIF, data->CIF.servicetype); - packetssent = UPNP_GetTotalPacketsSent(urls->controlURL_CIF, data->CIF.servicetype); - packetsreceived = UPNP_GetTotalPacketsReceived(urls->controlURL_CIF, data->CIF.servicetype); - printf("Bytes: Sent: %8u\tRecv: %8u\n", bytessent, bytesreceived); - printf("Packets: Sent: %8u\tRecv: %8u\n", packetssent, packetsreceived); -} - -/* Test function - * 1 - Add pinhole - * 2 - Check if pinhole is working from the IGD side */ -static void SetPinholeAndTest(struct UPNPUrls * urls, struct IGDdatas * data, - const char * remoteaddr, const char * eport, - const char * intaddr, const char * iport, - const char * proto, const char * lease_time) -{ - char uniqueID[8]; - /*int isWorking = 0;*/ - int r; - char proto_tmp[8]; - - if(!intaddr || !remoteaddr || !iport || !eport || !proto || !lease_time) - { - fprintf(stderr, "Wrong arguments\n"); - return; - } - if(atoi(proto) == 0) - { - const char * protocol; - protocol = protofix(proto); - if(protocol && (strcmp("TCP", protocol) == 0)) - { - snprintf(proto_tmp, sizeof(proto_tmp), "%d", IPPROTO_TCP); - proto = proto_tmp; - } - else if(protocol && (strcmp("UDP", protocol) == 0)) - { - snprintf(proto_tmp, sizeof(proto_tmp), "%d", IPPROTO_UDP); - proto = proto_tmp; - } - else - { - fprintf(stderr, "invalid protocol\n"); - return; - } - } - r = UPNP_AddPinhole(urls->controlURL_6FC, data->IPv6FC.servicetype, remoteaddr, eport, intaddr, iport, proto, lease_time, uniqueID); - if(r!=UPNPCOMMAND_SUCCESS) - printf("AddPinhole([%s]:%s -> [%s]:%s) failed with code %d (%s)\n", - remoteaddr, eport, intaddr, iport, r, strupnperror(r)); - else - { - printf("AddPinhole: ([%s]:%s -> [%s]:%s) / Pinhole ID = %s\n", - remoteaddr, eport, intaddr, iport, uniqueID); - /*r = UPNP_CheckPinholeWorking(urls->controlURL_6FC, data->servicetype_6FC, uniqueID, &isWorking); - if(r!=UPNPCOMMAND_SUCCESS) - printf("CheckPinholeWorking() failed with code %d (%s)\n", r, strupnperror(r)); - printf("CheckPinholeWorking: Pinhole ID = %s / IsWorking = %s\n", uniqueID, (isWorking)? "Yes":"No");*/ - } -} - -/* Test function - * 1 - Check if pinhole is working from the IGD side - * 2 - Update pinhole */ -static void GetPinholeAndUpdate(struct UPNPUrls * urls, struct IGDdatas * data, - const char * uniqueID, const char * lease_time) -{ - int isWorking = 0; - int r; - - if(!uniqueID || !lease_time) - { - fprintf(stderr, "Wrong arguments\n"); - return; - } - r = UPNP_CheckPinholeWorking(urls->controlURL_6FC, data->IPv6FC.servicetype, uniqueID, &isWorking); - printf("CheckPinholeWorking: Pinhole ID = %s / IsWorking = %s\n", uniqueID, (isWorking)? "Yes":"No"); - if(r!=UPNPCOMMAND_SUCCESS) - printf("CheckPinholeWorking() failed with code %d (%s)\n", r, strupnperror(r)); - if(isWorking || r==709) - { - r = UPNP_UpdatePinhole(urls->controlURL_6FC, data->IPv6FC.servicetype, uniqueID, lease_time); - printf("UpdatePinhole: Pinhole ID = %s with Lease Time: %s\n", uniqueID, lease_time); - if(r!=UPNPCOMMAND_SUCCESS) - printf("UpdatePinhole: ID (%s) failed with code %d (%s)\n", uniqueID, r, strupnperror(r)); - } -} - -/* Test function - * Get pinhole timeout - */ -static void GetPinholeOutboundTimeout(struct UPNPUrls * urls, struct IGDdatas * data, - const char * remoteaddr, const char * eport, - const char * intaddr, const char * iport, - const char * proto) -{ - int timeout = 0; - int r; - - if(!intaddr || !remoteaddr || !iport || !eport || !proto) - { - fprintf(stderr, "Wrong arguments\n"); - return; - } - - r = UPNP_GetOutboundPinholeTimeout(urls->controlURL_6FC, data->IPv6FC.servicetype, remoteaddr, eport, intaddr, iport, proto, &timeout); - if(r!=UPNPCOMMAND_SUCCESS) - printf("GetOutboundPinholeTimeout([%s]:%s -> [%s]:%s) failed with code %d (%s)\n", - intaddr, iport, remoteaddr, eport, r, strupnperror(r)); - else - printf("GetOutboundPinholeTimeout: ([%s]:%s -> [%s]:%s) / Timeout = %d\n", intaddr, iport, remoteaddr, eport, timeout); -} - -static void -GetPinholePackets(struct UPNPUrls * urls, - struct IGDdatas * data, const char * uniqueID) -{ - int r, pinholePackets = 0; - if(!uniqueID) - { - fprintf(stderr, "invalid arguments\n"); - return; - } - r = UPNP_GetPinholePackets(urls->controlURL_6FC, data->IPv6FC.servicetype, uniqueID, &pinholePackets); - if(r!=UPNPCOMMAND_SUCCESS) - printf("GetPinholePackets() failed with code %d (%s)\n", r, strupnperror(r)); - else - printf("GetPinholePackets: Pinhole ID = %s / PinholePackets = %d\n", uniqueID, pinholePackets); -} - -static void -CheckPinhole(struct UPNPUrls * urls, - struct IGDdatas * data, const char * uniqueID) -{ - int r, isWorking = 0; - if(!uniqueID) - { - fprintf(stderr, "invalid arguments\n"); - return; - } - r = UPNP_CheckPinholeWorking(urls->controlURL_6FC, data->IPv6FC.servicetype, uniqueID, &isWorking); - if(r!=UPNPCOMMAND_SUCCESS) - printf("CheckPinholeWorking() failed with code %d (%s)\n", r, strupnperror(r)); - else - printf("CheckPinholeWorking: Pinhole ID = %s / IsWorking = %s\n", uniqueID, (isWorking)? "Yes":"No"); -} - -static void -RemovePinhole(struct UPNPUrls * urls, - struct IGDdatas * data, const char * uniqueID) -{ - int r; - if(!uniqueID) - { - fprintf(stderr, "invalid arguments\n"); - return; - } - r = UPNP_DeletePinhole(urls->controlURL_6FC, data->IPv6FC.servicetype, uniqueID); - printf("UPNP_DeletePinhole() returned : %d\n", r); -} - - -/* sample upnp client program */ -int main(int argc, char ** argv) -{ - char command = 0; - char ** commandargv = 0; - int commandargc = 0; - struct UPNPDev * devlist = 0; - char lanaddr[64]; /* my ip address on the LAN */ - int i; - const char * rootdescurl = 0; - const char * multicastif = 0; - const char * minissdpdpath = 0; - int retcode = 0; - int error = 0; - int ipv6 = 0; - const char * description = 0; - -#ifdef _WIN32 - WSADATA wsaData; - int nResult = WSAStartup(MAKEWORD(2,2), &wsaData); - if(nResult != NO_ERROR) - { - fprintf(stderr, "WSAStartup() failed.\n"); - return -1; - } -#endif - printf("upnpc : miniupnpc library test client. (c) 2005-2013 Thomas Bernard\n"); - printf("Go to http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/\n" - "for more information.\n"); - /* command line processing */ - for(i=1; ipNext) - { - printf(" desc: %s\n st: %s\n\n", - device->descURL, device->st); - } - } - else - { - printf("upnpDiscover() error code=%d\n", error); - } - i = 1; - if( (rootdescurl && UPNP_GetIGDFromUrl(rootdescurl, &urls, &data, lanaddr, sizeof(lanaddr))) - || (i = UPNP_GetValidIGD(devlist, &urls, &data, lanaddr, sizeof(lanaddr)))) - { - switch(i) { - case 1: - printf("Found valid IGD : %s\n", urls.controlURL); - break; - case 2: - printf("Found a (not connected?) IGD : %s\n", urls.controlURL); - printf("Trying to continue anyway\n"); - break; - case 3: - printf("UPnP device found. Is it an IGD ? : %s\n", urls.controlURL); - printf("Trying to continue anyway\n"); - break; - default: - printf("Found device (igd ?) : %s\n", urls.controlURL); - printf("Trying to continue anyway\n"); - } - printf("Local LAN ip address : %s\n", lanaddr); - #if 0 - printf("getting \"%s\"\n", urls.ipcondescURL); - descXML = miniwget(urls.ipcondescURL, &descXMLsize); - if(descXML) - { - /*fwrite(descXML, 1, descXMLsize, stdout);*/ - free(descXML); descXML = NULL; - } - #endif - - switch(command) - { - case 'l': - DisplayInfos(&urls, &data); - ListRedirections(&urls, &data); - break; - case 'L': - NewListRedirections(&urls, &data); - break; - case 'a': - SetRedirectAndTest(&urls, &data, - commandargv[0], commandargv[1], - commandargv[2], commandargv[3], - (commandargc > 4)?commandargv[4]:"0", - description); - break; - case 'd': - for(i=0; i -#include -#include -#include "upnpcommands.h" -#include "miniupnpc.h" -#include "portlistingparse.h" - -static UNSIGNED_INTEGER -my_atoui(const char * s) -{ - return s ? ((UNSIGNED_INTEGER)STRTOUI(s, NULL, 0)) : 0; -} - -/* - * */ -LIBSPEC UNSIGNED_INTEGER -UPNP_GetTotalBytesSent(const char * controlURL, - const char * servicetype) -{ - struct NameValueParserData pdata; - char * buffer; - int bufsize; - unsigned int r = 0; - char * p; - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetTotalBytesSent", 0, &bufsize))) { - return UPNPCOMMAND_HTTP_ERROR; - } - ParseNameValue(buffer, bufsize, &pdata); - /*DisplayNameValueList(buffer, bufsize);*/ - free(buffer); buffer = NULL; - p = GetValueFromNameValueList(&pdata, "NewTotalBytesSent"); - r = my_atoui(p); - ClearNameValueList(&pdata); - return r; -} - -/* - * */ -LIBSPEC UNSIGNED_INTEGER -UPNP_GetTotalBytesReceived(const char * controlURL, - const char * servicetype) -{ - struct NameValueParserData pdata; - char * buffer; - int bufsize; - unsigned int r = 0; - char * p; - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetTotalBytesReceived", 0, &bufsize))) { - return UPNPCOMMAND_HTTP_ERROR; - } - ParseNameValue(buffer, bufsize, &pdata); - /*DisplayNameValueList(buffer, bufsize);*/ - free(buffer); buffer = NULL; - p = GetValueFromNameValueList(&pdata, "NewTotalBytesReceived"); - r = my_atoui(p); - ClearNameValueList(&pdata); - return r; -} - -/* - * */ -LIBSPEC UNSIGNED_INTEGER -UPNP_GetTotalPacketsSent(const char * controlURL, - const char * servicetype) -{ - struct NameValueParserData pdata; - char * buffer; - int bufsize; - unsigned int r = 0; - char * p; - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetTotalPacketsSent", 0, &bufsize))) { - return UPNPCOMMAND_HTTP_ERROR; - } - ParseNameValue(buffer, bufsize, &pdata); - /*DisplayNameValueList(buffer, bufsize);*/ - free(buffer); buffer = NULL; - p = GetValueFromNameValueList(&pdata, "NewTotalPacketsSent"); - r = my_atoui(p); - ClearNameValueList(&pdata); - return r; -} - -/* - * */ -LIBSPEC UNSIGNED_INTEGER -UPNP_GetTotalPacketsReceived(const char * controlURL, - const char * servicetype) -{ - struct NameValueParserData pdata; - char * buffer; - int bufsize; - unsigned int r = 0; - char * p; - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetTotalPacketsReceived", 0, &bufsize))) { - return UPNPCOMMAND_HTTP_ERROR; - } - ParseNameValue(buffer, bufsize, &pdata); - /*DisplayNameValueList(buffer, bufsize);*/ - free(buffer); buffer = NULL; - p = GetValueFromNameValueList(&pdata, "NewTotalPacketsReceived"); - r = my_atoui(p); - ClearNameValueList(&pdata); - return r; -} - -/* UPNP_GetStatusInfo() call the corresponding UPNP method - * returns the current status and uptime */ -LIBSPEC int -UPNP_GetStatusInfo(const char * controlURL, - const char * servicetype, - char * status, - unsigned int * uptime, - char * lastconnerror) -{ - struct NameValueParserData pdata; - char * buffer; - int bufsize; - char * p; - char * up; - char * err; - int ret = UPNPCOMMAND_UNKNOWN_ERROR; - - if(!status && !uptime) - return UPNPCOMMAND_INVALID_ARGS; - - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetStatusInfo", 0, &bufsize))) { - return UPNPCOMMAND_HTTP_ERROR; - } - ParseNameValue(buffer, bufsize, &pdata); - /*DisplayNameValueList(buffer, bufsize);*/ - free(buffer); buffer = NULL; - up = GetValueFromNameValueList(&pdata, "NewUptime"); - p = GetValueFromNameValueList(&pdata, "NewConnectionStatus"); - err = GetValueFromNameValueList(&pdata, "NewLastConnectionError"); - if(p && up) - ret = UPNPCOMMAND_SUCCESS; - - if(status) { - if(p){ - strncpy(status, p, 64 ); - status[63] = '\0'; - }else - status[0]= '\0'; - } - - if(uptime) { - if(up) - sscanf(up,"%u",uptime); - else - uptime = 0; - } - - if(lastconnerror) { - if(err) { - strncpy(lastconnerror, err, 64 ); - lastconnerror[63] = '\0'; - } else - lastconnerror[0] = '\0'; - } - - p = GetValueFromNameValueList(&pdata, "errorCode"); - if(p) { - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(p, "%d", &ret); - } - ClearNameValueList(&pdata); - return ret; -} - -/* UPNP_GetConnectionTypeInfo() call the corresponding UPNP method - * returns the connection type */ -LIBSPEC int -UPNP_GetConnectionTypeInfo(const char * controlURL, - const char * servicetype, - char * connectionType) -{ - struct NameValueParserData pdata; - char * buffer; - int bufsize; - char * p; - int ret = UPNPCOMMAND_UNKNOWN_ERROR; - - if(!connectionType) - return UPNPCOMMAND_INVALID_ARGS; - - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetConnectionTypeInfo", 0, &bufsize))) { - return UPNPCOMMAND_HTTP_ERROR; - } - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - p = GetValueFromNameValueList(&pdata, "NewConnectionType"); - /*p = GetValueFromNameValueList(&pdata, "NewPossibleConnectionTypes");*/ - /* PossibleConnectionTypes will have several values.... */ - if(p) { - strncpy(connectionType, p, 64 ); - connectionType[63] = '\0'; - ret = UPNPCOMMAND_SUCCESS; - } else - connectionType[0] = '\0'; - p = GetValueFromNameValueList(&pdata, "errorCode"); - if(p) { - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(p, "%d", &ret); - } - ClearNameValueList(&pdata); - return ret; -} - -/* UPNP_GetLinkLayerMaxBitRate() call the corresponding UPNP method. - * Returns 2 values: Downloadlink bandwidth and Uplink bandwidth. - * One of the values can be null - * Note : GetLinkLayerMaxBitRates belongs to WANPPPConnection:1 only - * We can use the GetCommonLinkProperties from WANCommonInterfaceConfig:1 */ -LIBSPEC int -UPNP_GetLinkLayerMaxBitRates(const char * controlURL, - const char * servicetype, - unsigned int * bitrateDown, - unsigned int * bitrateUp) -{ - struct NameValueParserData pdata; - char * buffer; - int bufsize; - int ret = UPNPCOMMAND_UNKNOWN_ERROR; - char * down; - char * up; - char * p; - - if(!bitrateDown && !bitrateUp) - return UPNPCOMMAND_INVALID_ARGS; - - /* shouldn't we use GetCommonLinkProperties ? */ - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetCommonLinkProperties", 0, &bufsize))) { - /*"GetLinkLayerMaxBitRates", 0, &bufsize);*/ - return UPNPCOMMAND_HTTP_ERROR; - } - /*DisplayNameValueList(buffer, bufsize);*/ - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - /*down = GetValueFromNameValueList(&pdata, "NewDownstreamMaxBitRate");*/ - /*up = GetValueFromNameValueList(&pdata, "NewUpstreamMaxBitRate");*/ - down = GetValueFromNameValueList(&pdata, "NewLayer1DownstreamMaxBitRate"); - up = GetValueFromNameValueList(&pdata, "NewLayer1UpstreamMaxBitRate"); - /*GetValueFromNameValueList(&pdata, "NewWANAccessType");*/ - /*GetValueFromNameValueList(&pdata, "NewPhysicalLinkStatus");*/ - if(down && up) - ret = UPNPCOMMAND_SUCCESS; - - if(bitrateDown) { - if(down) - sscanf(down,"%u",bitrateDown); - else - *bitrateDown = 0; - } - - if(bitrateUp) { - if(up) - sscanf(up,"%u",bitrateUp); - else - *bitrateUp = 0; - } - p = GetValueFromNameValueList(&pdata, "errorCode"); - if(p) { - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(p, "%d", &ret); - } - ClearNameValueList(&pdata); - return ret; -} - - -/* UPNP_GetExternalIPAddress() call the corresponding UPNP method. - * if the third arg is not null the value is copied to it. - * at least 16 bytes must be available - * - * Return values : - * 0 : SUCCESS - * NON ZERO : ERROR Either an UPnP error code or an unknown error. - * - * 402 Invalid Args - See UPnP Device Architecture section on Control. - * 501 Action Failed - See UPnP Device Architecture section on Control. - */ -LIBSPEC int -UPNP_GetExternalIPAddress(const char * controlURL, - const char * servicetype, - char * extIpAdd) -{ - struct NameValueParserData pdata; - char * buffer; - int bufsize; - char * p; - int ret = UPNPCOMMAND_UNKNOWN_ERROR; - - if(!extIpAdd || !controlURL || !servicetype) - return UPNPCOMMAND_INVALID_ARGS; - - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetExternalIPAddress", 0, &bufsize))) { - return UPNPCOMMAND_HTTP_ERROR; - } - /*DisplayNameValueList(buffer, bufsize);*/ - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - /*printf("external ip = %s\n", GetValueFromNameValueList(&pdata, "NewExternalIPAddress") );*/ - p = GetValueFromNameValueList(&pdata, "NewExternalIPAddress"); - if(p) { - strncpy(extIpAdd, p, 16 ); - extIpAdd[15] = '\0'; - ret = UPNPCOMMAND_SUCCESS; - } else - extIpAdd[0] = '\0'; - - p = GetValueFromNameValueList(&pdata, "errorCode"); - if(p) { - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(p, "%d", &ret); - } - - ClearNameValueList(&pdata); - return ret; -} - -LIBSPEC int -UPNP_AddPortMapping(const char * controlURL, const char * servicetype, - const char * extPort, - const char * inPort, - const char * inClient, - const char * desc, - const char * proto, - const char * remoteHost, - const char * leaseDuration) -{ - struct UPNParg * AddPortMappingArgs; - char * buffer; - int bufsize; - struct NameValueParserData pdata; - const char * resVal; - int ret; - - if(!inPort || !inClient || !proto || !extPort) - return UPNPCOMMAND_INVALID_ARGS; - - AddPortMappingArgs = calloc(9, sizeof(struct UPNParg)); - AddPortMappingArgs[0].elt = "NewRemoteHost"; - AddPortMappingArgs[0].val = remoteHost; - AddPortMappingArgs[1].elt = "NewExternalPort"; - AddPortMappingArgs[1].val = extPort; - AddPortMappingArgs[2].elt = "NewProtocol"; - AddPortMappingArgs[2].val = proto; - AddPortMappingArgs[3].elt = "NewInternalPort"; - AddPortMappingArgs[3].val = inPort; - AddPortMappingArgs[4].elt = "NewInternalClient"; - AddPortMappingArgs[4].val = inClient; - AddPortMappingArgs[5].elt = "NewEnabled"; - AddPortMappingArgs[5].val = "1"; - AddPortMappingArgs[6].elt = "NewPortMappingDescription"; - AddPortMappingArgs[6].val = desc?desc:"libminiupnpc"; - AddPortMappingArgs[7].elt = "NewLeaseDuration"; - AddPortMappingArgs[7].val = leaseDuration?leaseDuration:"0"; - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "AddPortMapping", AddPortMappingArgs, - &bufsize))) { - free(AddPortMappingArgs); - return UPNPCOMMAND_HTTP_ERROR; - } - /*DisplayNameValueList(buffer, bufsize);*/ - /*buffer[bufsize] = '\0';*/ - /*puts(buffer);*/ - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - resVal = GetValueFromNameValueList(&pdata, "errorCode"); - if(resVal) { - /*printf("AddPortMapping errorCode = '%s'\n", resVal); */ - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(resVal, "%d", &ret); - } else { - ret = UPNPCOMMAND_SUCCESS; - } - ClearNameValueList(&pdata); - free(AddPortMappingArgs); - return ret; -} - -LIBSPEC int -UPNP_DeletePortMapping(const char * controlURL, const char * servicetype, - const char * extPort, const char * proto, - const char * remoteHost) -{ - /*struct NameValueParserData pdata;*/ - struct UPNParg * DeletePortMappingArgs; - char * buffer; - int bufsize; - struct NameValueParserData pdata; - const char * resVal; - int ret; - - if(!extPort || !proto) - return UPNPCOMMAND_INVALID_ARGS; - - DeletePortMappingArgs = calloc(4, sizeof(struct UPNParg)); - DeletePortMappingArgs[0].elt = "NewRemoteHost"; - DeletePortMappingArgs[0].val = remoteHost; - DeletePortMappingArgs[1].elt = "NewExternalPort"; - DeletePortMappingArgs[1].val = extPort; - DeletePortMappingArgs[2].elt = "NewProtocol"; - DeletePortMappingArgs[2].val = proto; - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "DeletePortMapping", - DeletePortMappingArgs, &bufsize))) { - free(DeletePortMappingArgs); - return UPNPCOMMAND_HTTP_ERROR; - } - /*DisplayNameValueList(buffer, bufsize);*/ - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - resVal = GetValueFromNameValueList(&pdata, "errorCode"); - if(resVal) { - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(resVal, "%d", &ret); - } else { - ret = UPNPCOMMAND_SUCCESS; - } - ClearNameValueList(&pdata); - free(DeletePortMappingArgs); - return ret; -} - -LIBSPEC int -UPNP_GetGenericPortMappingEntry(const char * controlURL, - const char * servicetype, - const char * index, - char * extPort, - char * intClient, - char * intPort, - char * protocol, - char * desc, - char * enabled, - char * rHost, - char * duration) -{ - struct NameValueParserData pdata; - struct UPNParg * GetPortMappingArgs; - char * buffer; - int bufsize; - char * p; - int r = UPNPCOMMAND_UNKNOWN_ERROR; - if(!index) - return UPNPCOMMAND_INVALID_ARGS; - intClient[0] = '\0'; - intPort[0] = '\0'; - GetPortMappingArgs = calloc(2, sizeof(struct UPNParg)); - GetPortMappingArgs[0].elt = "NewPortMappingIndex"; - GetPortMappingArgs[0].val = index; - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetGenericPortMappingEntry", - GetPortMappingArgs, &bufsize))) { - free(GetPortMappingArgs); - return UPNPCOMMAND_HTTP_ERROR; - } - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - - p = GetValueFromNameValueList(&pdata, "NewRemoteHost"); - if(p && rHost) - { - strncpy(rHost, p, 64); - rHost[63] = '\0'; - } - p = GetValueFromNameValueList(&pdata, "NewExternalPort"); - if(p && extPort) - { - strncpy(extPort, p, 6); - extPort[5] = '\0'; - r = UPNPCOMMAND_SUCCESS; - } - p = GetValueFromNameValueList(&pdata, "NewProtocol"); - if(p && protocol) - { - strncpy(protocol, p, 4); - protocol[3] = '\0'; - } - p = GetValueFromNameValueList(&pdata, "NewInternalClient"); - if(p && intClient) - { - strncpy(intClient, p, 16); - intClient[15] = '\0'; - r = 0; - } - p = GetValueFromNameValueList(&pdata, "NewInternalPort"); - if(p && intPort) - { - strncpy(intPort, p, 6); - intPort[5] = '\0'; - } - p = GetValueFromNameValueList(&pdata, "NewEnabled"); - if(p && enabled) - { - strncpy(enabled, p, 4); - enabled[3] = '\0'; - } - p = GetValueFromNameValueList(&pdata, "NewPortMappingDescription"); - if(p && desc) - { - strncpy(desc, p, 80); - desc[79] = '\0'; - } - p = GetValueFromNameValueList(&pdata, "NewLeaseDuration"); - if(p && duration) - { - strncpy(duration, p, 16); - duration[15] = '\0'; - } - p = GetValueFromNameValueList(&pdata, "errorCode"); - if(p) { - r = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(p, "%d", &r); - } - ClearNameValueList(&pdata); - free(GetPortMappingArgs); - return r; -} - -LIBSPEC int -UPNP_GetPortMappingNumberOfEntries(const char * controlURL, - const char * servicetype, - unsigned int * numEntries) -{ - struct NameValueParserData pdata; - char * buffer; - int bufsize; - char* p; - int ret = UPNPCOMMAND_UNKNOWN_ERROR; - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetPortMappingNumberOfEntries", 0, - &bufsize))) { - return UPNPCOMMAND_HTTP_ERROR; - } -#ifdef DEBUG - DisplayNameValueList(buffer, bufsize); -#endif - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - - p = GetValueFromNameValueList(&pdata, "NewPortMappingNumberOfEntries"); - if(numEntries && p) { - *numEntries = 0; - sscanf(p, "%u", numEntries); - ret = UPNPCOMMAND_SUCCESS; - } - - p = GetValueFromNameValueList(&pdata, "errorCode"); - if(p) { - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(p, "%d", &ret); - } - - ClearNameValueList(&pdata); - return ret; -} - -/* UPNP_GetSpecificPortMappingEntry retrieves an existing port mapping - * the result is returned in the intClient and intPort strings - * please provide 16 and 6 bytes of data */ -LIBSPEC int -UPNP_GetSpecificPortMappingEntry(const char * controlURL, - const char * servicetype, - const char * extPort, - const char * proto, - char * intClient, - char * intPort, - char * desc, - char * enabled, - char * leaseDuration) -{ - struct NameValueParserData pdata; - struct UPNParg * GetPortMappingArgs; - char * buffer; - int bufsize; - char * p; - int ret = UPNPCOMMAND_UNKNOWN_ERROR; - - if(!intPort || !intClient || !extPort || !proto) - return UPNPCOMMAND_INVALID_ARGS; - - GetPortMappingArgs = calloc(4, sizeof(struct UPNParg)); - GetPortMappingArgs[0].elt = "NewRemoteHost"; - /* TODO : add remote host ? */ - GetPortMappingArgs[1].elt = "NewExternalPort"; - GetPortMappingArgs[1].val = extPort; - GetPortMappingArgs[2].elt = "NewProtocol"; - GetPortMappingArgs[2].val = proto; - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetSpecificPortMappingEntry", - GetPortMappingArgs, &bufsize))) { - free(GetPortMappingArgs); - return UPNPCOMMAND_HTTP_ERROR; - } - /*DisplayNameValueList(buffer, bufsize);*/ - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - - p = GetValueFromNameValueList(&pdata, "NewInternalClient"); - if(p) { - strncpy(intClient, p, 16); - intClient[15] = '\0'; - ret = UPNPCOMMAND_SUCCESS; - } else - intClient[0] = '\0'; - - p = GetValueFromNameValueList(&pdata, "NewInternalPort"); - if(p) { - strncpy(intPort, p, 6); - intPort[5] = '\0'; - } else - intPort[0] = '\0'; - - p = GetValueFromNameValueList(&pdata, "NewEnabled"); - if(p && enabled) { - strncpy(enabled, p, 4); - enabled[3] = '\0'; - } - - p = GetValueFromNameValueList(&pdata, "NewPortMappingDescription"); - if(p && desc) { - strncpy(desc, p, 80); - desc[79] = '\0'; - } - - p = GetValueFromNameValueList(&pdata, "NewLeaseDuration"); - if(p && leaseDuration) - { - strncpy(leaseDuration, p, 16); - leaseDuration[15] = '\0'; - } - - p = GetValueFromNameValueList(&pdata, "errorCode"); - if(p) { - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(p, "%d", &ret); - } - - ClearNameValueList(&pdata); - free(GetPortMappingArgs); - return ret; -} - -/* UPNP_GetListOfPortMappings() - * - * Possible UPNP Error codes : - * 606 Action not Authorized - * 730 PortMappingNotFound - no port mapping is found in the specified range. - * 733 InconsistantParameters - NewStartPort and NewEndPort values are not - * consistent. - */ -LIBSPEC int -UPNP_GetListOfPortMappings(const char * controlURL, - const char * servicetype, - const char * startPort, - const char * endPort, - const char * protocol, - const char * numberOfPorts, - struct PortMappingParserData * data) -{ - struct NameValueParserData pdata; - struct UPNParg * GetListOfPortMappingsArgs; - const char * p; - char * buffer; - int bufsize; - int ret = UPNPCOMMAND_UNKNOWN_ERROR; - - if(!startPort || !endPort || !protocol) - return UPNPCOMMAND_INVALID_ARGS; - - GetListOfPortMappingsArgs = calloc(6, sizeof(struct UPNParg)); - GetListOfPortMappingsArgs[0].elt = "NewStartPort"; - GetListOfPortMappingsArgs[0].val = startPort; - GetListOfPortMappingsArgs[1].elt = "NewEndPort"; - GetListOfPortMappingsArgs[1].val = endPort; - GetListOfPortMappingsArgs[2].elt = "NewProtocol"; - GetListOfPortMappingsArgs[2].val = protocol; - GetListOfPortMappingsArgs[3].elt = "NewManage"; - GetListOfPortMappingsArgs[3].val = "1"; - GetListOfPortMappingsArgs[4].elt = "NewNumberOfPorts"; - GetListOfPortMappingsArgs[4].val = numberOfPorts?numberOfPorts:"1000"; - - if(!(buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetListOfPortMappings", - GetListOfPortMappingsArgs, &bufsize))) { - free(GetListOfPortMappingsArgs); - return UPNPCOMMAND_HTTP_ERROR; - } - free(GetListOfPortMappingsArgs); - - /*DisplayNameValueList(buffer, bufsize);*/ - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - - /*p = GetValueFromNameValueList(&pdata, "NewPortListing");*/ - /*if(p) { - printf("NewPortListing : %s\n", p); - }*/ - /*printf("NewPortListing(%d chars) : %s\n", - pdata.portListingLength, pdata.portListing);*/ - if(pdata.portListing) - { - /*struct PortMapping * pm; - int i = 0;*/ - ParsePortListing(pdata.portListing, pdata.portListingLength, - data); - ret = UPNPCOMMAND_SUCCESS; - /* - for(pm = data->head.lh_first; pm != NULL; pm = pm->entries.le_next) - { - printf("%2d %s %5hu->%s:%-5hu '%s' '%s'\n", - i, pm->protocol, pm->externalPort, pm->internalClient, - pm->internalPort, - pm->description, pm->remoteHost); - i++; - } - */ - /*FreePortListing(&data);*/ - } - - p = GetValueFromNameValueList(&pdata, "errorCode"); - if(p) { - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(p, "%d", &ret); - } - ClearNameValueList(&pdata); - - /*printf("%.*s", bufsize, buffer);*/ - - return ret; -} - -/* IGD:2, functions for service WANIPv6FirewallControl:1 */ -LIBSPEC int -UPNP_GetFirewallStatus(const char * controlURL, - const char * servicetype, - int * firewallEnabled, - int * inboundPinholeAllowed) -{ - struct NameValueParserData pdata; - char * buffer; - int bufsize; - char * fe, *ipa, *p; - int ret = UPNPCOMMAND_UNKNOWN_ERROR; - - if(!firewallEnabled && !inboundPinholeAllowed) - return UPNPCOMMAND_INVALID_ARGS; - - buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetFirewallStatus", 0, &bufsize); - if(!buffer) { - return UPNPCOMMAND_HTTP_ERROR; - } - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - fe = GetValueFromNameValueList(&pdata, "FirewallEnabled"); - ipa = GetValueFromNameValueList(&pdata, "InboundPinholeAllowed"); - if(ipa && fe) - ret = UPNPCOMMAND_SUCCESS; - if(fe) - *firewallEnabled = my_atoui(fe); - /*else - *firewallEnabled = 0;*/ - if(ipa) - *inboundPinholeAllowed = my_atoui(ipa); - /*else - *inboundPinholeAllowed = 0;*/ - p = GetValueFromNameValueList(&pdata, "errorCode"); - if(p) - { - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(p, "%d", &ret); - } - ClearNameValueList(&pdata); - return ret; -} - -LIBSPEC int -UPNP_GetOutboundPinholeTimeout(const char * controlURL, const char * servicetype, - const char * remoteHost, - const char * remotePort, - const char * intClient, - const char * intPort, - const char * proto, - int * opTimeout) -{ - struct UPNParg * GetOutboundPinholeTimeoutArgs; - char * buffer; - int bufsize; - struct NameValueParserData pdata; - const char * resVal; - char * p; - int ret; - - if(!intPort || !intClient || !proto || !remotePort || !remoteHost) - return UPNPCOMMAND_INVALID_ARGS; - - GetOutboundPinholeTimeoutArgs = calloc(6, sizeof(struct UPNParg)); - GetOutboundPinholeTimeoutArgs[0].elt = "RemoteHost"; - GetOutboundPinholeTimeoutArgs[0].val = remoteHost; - GetOutboundPinholeTimeoutArgs[1].elt = "RemotePort"; - GetOutboundPinholeTimeoutArgs[1].val = remotePort; - GetOutboundPinholeTimeoutArgs[2].elt = "Protocol"; - GetOutboundPinholeTimeoutArgs[2].val = proto; - GetOutboundPinholeTimeoutArgs[3].elt = "InternalPort"; - GetOutboundPinholeTimeoutArgs[3].val = intPort; - GetOutboundPinholeTimeoutArgs[4].elt = "InternalClient"; - GetOutboundPinholeTimeoutArgs[4].val = intClient; - buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetOutboundPinholeTimeout", GetOutboundPinholeTimeoutArgs, &bufsize); - if(!buffer) - return UPNPCOMMAND_HTTP_ERROR; - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - resVal = GetValueFromNameValueList(&pdata, "errorCode"); - if(resVal) - { - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(resVal, "%d", &ret); - } - else - { - ret = UPNPCOMMAND_SUCCESS; - p = GetValueFromNameValueList(&pdata, "OutboundPinholeTimeout"); - if(p) - *opTimeout = my_atoui(p); - } - ClearNameValueList(&pdata); - free(GetOutboundPinholeTimeoutArgs); - return ret; -} - -LIBSPEC int -UPNP_AddPinhole(const char * controlURL, const char * servicetype, - const char * remoteHost, - const char * remotePort, - const char * intClient, - const char * intPort, - const char * proto, - const char * leaseTime, - char * uniqueID) -{ - struct UPNParg * AddPinholeArgs; - char * buffer; - int bufsize; - struct NameValueParserData pdata; - const char * resVal; - char * p; - int ret; - - if(!intPort || !intClient || !proto || !remoteHost || !remotePort || !leaseTime) - return UPNPCOMMAND_INVALID_ARGS; - - AddPinholeArgs = calloc(7, sizeof(struct UPNParg)); - /* RemoteHost can be wilcarded */ - if(strncmp(remoteHost, "empty", 5)==0) - { - AddPinholeArgs[0].elt = "RemoteHost"; - AddPinholeArgs[0].val = ""; - } - else - { - AddPinholeArgs[0].elt = "RemoteHost"; - AddPinholeArgs[0].val = remoteHost; - } - AddPinholeArgs[1].elt = "RemotePort"; - AddPinholeArgs[1].val = remotePort; - AddPinholeArgs[2].elt = "Protocol"; - AddPinholeArgs[2].val = proto; - AddPinholeArgs[3].elt = "InternalPort"; - AddPinholeArgs[3].val = intPort; - if(strncmp(intClient, "empty", 5)==0) - { - AddPinholeArgs[4].elt = "InternalClient"; - AddPinholeArgs[4].val = ""; - } - else - { - AddPinholeArgs[4].elt = "InternalClient"; - AddPinholeArgs[4].val = intClient; - } - AddPinholeArgs[5].elt = "LeaseTime"; - AddPinholeArgs[5].val = leaseTime; - buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "AddPinhole", AddPinholeArgs, &bufsize); - if(!buffer) - return UPNPCOMMAND_HTTP_ERROR; - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - p = GetValueFromNameValueList(&pdata, "UniqueID"); - if(p) - { - strncpy(uniqueID, p, 8); - uniqueID[7] = '\0'; - } - resVal = GetValueFromNameValueList(&pdata, "errorCode"); - if(resVal) - { - /*printf("AddPortMapping errorCode = '%s'\n", resVal);*/ - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(resVal, "%d", &ret); - } - else - { - ret = UPNPCOMMAND_SUCCESS; - } - ClearNameValueList(&pdata); - free(AddPinholeArgs); - return ret; -} - -LIBSPEC int -UPNP_UpdatePinhole(const char * controlURL, const char * servicetype, - const char * uniqueID, - const char * leaseTime) -{ - struct UPNParg * UpdatePinholeArgs; - char * buffer; - int bufsize; - struct NameValueParserData pdata; - const char * resVal; - int ret; - - if(!uniqueID || !leaseTime) - return UPNPCOMMAND_INVALID_ARGS; - - UpdatePinholeArgs = calloc(3, sizeof(struct UPNParg)); - UpdatePinholeArgs[0].elt = "UniqueID"; - UpdatePinholeArgs[0].val = uniqueID; - UpdatePinholeArgs[1].elt = "NewLeaseTime"; - UpdatePinholeArgs[1].val = leaseTime; - buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "UpdatePinhole", UpdatePinholeArgs, &bufsize); - if(!buffer) - return UPNPCOMMAND_HTTP_ERROR; - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - resVal = GetValueFromNameValueList(&pdata, "errorCode"); - if(resVal) - { - /*printf("AddPortMapping errorCode = '%s'\n", resVal); */ - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(resVal, "%d", &ret); - } - else - { - ret = UPNPCOMMAND_SUCCESS; - } - ClearNameValueList(&pdata); - free(UpdatePinholeArgs); - return ret; -} - -LIBSPEC int -UPNP_DeletePinhole(const char * controlURL, const char * servicetype, const char * uniqueID) -{ - /*struct NameValueParserData pdata;*/ - struct UPNParg * DeletePinholeArgs; - char * buffer; - int bufsize; - struct NameValueParserData pdata; - const char * resVal; - int ret; - - if(!uniqueID) - return UPNPCOMMAND_INVALID_ARGS; - - DeletePinholeArgs = calloc(2, sizeof(struct UPNParg)); - DeletePinholeArgs[0].elt = "UniqueID"; - DeletePinholeArgs[0].val = uniqueID; - buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "DeletePinhole", DeletePinholeArgs, &bufsize); - if(!buffer) - return UPNPCOMMAND_HTTP_ERROR; - /*DisplayNameValueList(buffer, bufsize);*/ - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - resVal = GetValueFromNameValueList(&pdata, "errorCode"); - if(resVal) - { - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(resVal, "%d", &ret); - } - else - { - ret = UPNPCOMMAND_SUCCESS; - } - ClearNameValueList(&pdata); - free(DeletePinholeArgs); - return ret; -} - -LIBSPEC int -UPNP_CheckPinholeWorking(const char * controlURL, const char * servicetype, - const char * uniqueID, int * isWorking) -{ - struct NameValueParserData pdata; - struct UPNParg * CheckPinholeWorkingArgs; - char * buffer; - int bufsize; - char * p; - int ret = UPNPCOMMAND_UNKNOWN_ERROR; - - if(!uniqueID) - return UPNPCOMMAND_INVALID_ARGS; - - CheckPinholeWorkingArgs = calloc(4, sizeof(struct UPNParg)); - CheckPinholeWorkingArgs[0].elt = "UniqueID"; - CheckPinholeWorkingArgs[0].val = uniqueID; - buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "CheckPinholeWorking", CheckPinholeWorkingArgs, &bufsize); - if(!buffer) - return UPNPCOMMAND_HTTP_ERROR; - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - - p = GetValueFromNameValueList(&pdata, "IsWorking"); - if(p) - { - *isWorking=my_atoui(p); - ret = UPNPCOMMAND_SUCCESS; - } - else - *isWorking = 0; - - p = GetValueFromNameValueList(&pdata, "errorCode"); - if(p) - { - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(p, "%d", &ret); - } - - ClearNameValueList(&pdata); - free(CheckPinholeWorkingArgs); - return ret; -} - -LIBSPEC int -UPNP_GetPinholePackets(const char * controlURL, const char * servicetype, - const char * uniqueID, int * packets) -{ - struct NameValueParserData pdata; - struct UPNParg * GetPinholePacketsArgs; - char * buffer; - int bufsize; - char * p; - int ret = UPNPCOMMAND_UNKNOWN_ERROR; - - if(!uniqueID) - return UPNPCOMMAND_INVALID_ARGS; - - GetPinholePacketsArgs = calloc(4, sizeof(struct UPNParg)); - GetPinholePacketsArgs[0].elt = "UniqueID"; - GetPinholePacketsArgs[0].val = uniqueID; - buffer = simpleUPnPcommand(-1, controlURL, servicetype, - "GetPinholePackets", GetPinholePacketsArgs, &bufsize); - if(!buffer) - return UPNPCOMMAND_HTTP_ERROR; - ParseNameValue(buffer, bufsize, &pdata); - free(buffer); buffer = NULL; - - p = GetValueFromNameValueList(&pdata, "PinholePackets"); - if(p) - { - *packets=my_atoui(p); - ret = UPNPCOMMAND_SUCCESS; - } - - p = GetValueFromNameValueList(&pdata, "errorCode"); - if(p) - { - ret = UPNPCOMMAND_UNKNOWN_ERROR; - sscanf(p, "%d", &ret); - } - - ClearNameValueList(&pdata); - free(GetPinholePacketsArgs); - return ret; -} - - diff --git a/Externals/miniupnpc/src/upnpcommands.h b/Externals/miniupnpc/src/upnpcommands.h deleted file mode 100644 index ee3ce89df245..000000000000 --- a/Externals/miniupnpc/src/upnpcommands.h +++ /dev/null @@ -1,271 +0,0 @@ -/* $Id: upnpcommands.h,v 1.25 2012/09/27 15:42:10 nanard Exp $ */ -/* Miniupnp project : http://miniupnp.free.fr/ - * Author : Thomas Bernard - * Copyright (c) 2005-2011 Thomas Bernard - * This software is subject to the conditions detailed in the - * LICENCE file provided within this distribution */ -#ifndef UPNPCOMMANDS_H_INCLUDED -#define UPNPCOMMANDS_H_INCLUDED - -#include "upnpreplyparse.h" -#include "portlistingparse.h" -#include "declspec.h" -#include "miniupnpctypes.h" - -/* MiniUPnPc return codes : */ -#define UPNPCOMMAND_SUCCESS (0) -#define UPNPCOMMAND_UNKNOWN_ERROR (-1) -#define UPNPCOMMAND_INVALID_ARGS (-2) -#define UPNPCOMMAND_HTTP_ERROR (-3) - -#ifdef __cplusplus -extern "C" { -#endif - -LIBSPEC UNSIGNED_INTEGER -UPNP_GetTotalBytesSent(const char * controlURL, - const char * servicetype); - -LIBSPEC UNSIGNED_INTEGER -UPNP_GetTotalBytesReceived(const char * controlURL, - const char * servicetype); - -LIBSPEC UNSIGNED_INTEGER -UPNP_GetTotalPacketsSent(const char * controlURL, - const char * servicetype); - -LIBSPEC UNSIGNED_INTEGER -UPNP_GetTotalPacketsReceived(const char * controlURL, - const char * servicetype); - -/* UPNP_GetStatusInfo() - * status and lastconnerror are 64 byte buffers - * Return values : - * UPNPCOMMAND_SUCCESS, UPNPCOMMAND_INVALID_ARGS, UPNPCOMMAND_UNKNOWN_ERROR - * or a UPnP Error code */ -LIBSPEC int -UPNP_GetStatusInfo(const char * controlURL, - const char * servicetype, - char * status, - unsigned int * uptime, - char * lastconnerror); - -/* UPNP_GetConnectionTypeInfo() - * argument connectionType is a 64 character buffer - * Return Values : - * UPNPCOMMAND_SUCCESS, UPNPCOMMAND_INVALID_ARGS, UPNPCOMMAND_UNKNOWN_ERROR - * or a UPnP Error code */ -LIBSPEC int -UPNP_GetConnectionTypeInfo(const char * controlURL, - const char * servicetype, - char * connectionType); - -/* UPNP_GetExternalIPAddress() call the corresponding UPNP method. - * if the third arg is not null the value is copied to it. - * at least 16 bytes must be available - * - * Return values : - * 0 : SUCCESS - * NON ZERO : ERROR Either an UPnP error code or an unknown error. - * - * possible UPnP Errors : - * 402 Invalid Args - See UPnP Device Architecture section on Control. - * 501 Action Failed - See UPnP Device Architecture section on Control. */ -LIBSPEC int -UPNP_GetExternalIPAddress(const char * controlURL, - const char * servicetype, - char * extIpAdd); - -/* UPNP_GetLinkLayerMaxBitRates() - * call WANCommonInterfaceConfig:1#GetCommonLinkProperties - * - * return values : - * UPNPCOMMAND_SUCCESS, UPNPCOMMAND_INVALID_ARGS, UPNPCOMMAND_UNKNOWN_ERROR - * or a UPnP Error Code. */ -LIBSPEC int -UPNP_GetLinkLayerMaxBitRates(const char* controlURL, - const char* servicetype, - unsigned int * bitrateDown, - unsigned int * bitrateUp); - -/* UPNP_AddPortMapping() - * if desc is NULL, it will be defaulted to "libminiupnpc" - * remoteHost is usually NULL because IGD don't support it. - * - * Return values : - * 0 : SUCCESS - * NON ZERO : ERROR. Either an UPnP error code or an unknown error. - * - * List of possible UPnP errors for AddPortMapping : - * errorCode errorDescription (short) - Description (long) - * 402 Invalid Args - See UPnP Device Architecture section on Control. - * 501 Action Failed - See UPnP Device Architecture section on Control. - * 715 WildCardNotPermittedInSrcIP - The source IP address cannot be - * wild-carded - * 716 WildCardNotPermittedInExtPort - The external port cannot be wild-carded - * 718 ConflictInMappingEntry - The port mapping entry specified conflicts - * with a mapping assigned previously to another client - * 724 SamePortValuesRequired - Internal and External port values - * must be the same - * 725 OnlyPermanentLeasesSupported - The NAT implementation only supports - * permanent lease times on port mappings - * 726 RemoteHostOnlySupportsWildcard - RemoteHost must be a wildcard - * and cannot be a specific IP address or DNS name - * 727 ExternalPortOnlySupportsWildcard - ExternalPort must be a wildcard and - * cannot be a specific port value */ -LIBSPEC int -UPNP_AddPortMapping(const char * controlURL, const char * servicetype, - const char * extPort, - const char * inPort, - const char * inClient, - const char * desc, - const char * proto, - const char * remoteHost, - const char * leaseDuration); - -/* UPNP_DeletePortMapping() - * Use same argument values as what was used for AddPortMapping(). - * remoteHost is usually NULL because IGD don't support it. - * Return Values : - * 0 : SUCCESS - * NON ZERO : error. Either an UPnP error code or an undefined error. - * - * List of possible UPnP errors for DeletePortMapping : - * 402 Invalid Args - See UPnP Device Architecture section on Control. - * 714 NoSuchEntryInArray - The specified value does not exist in the array */ -LIBSPEC int -UPNP_DeletePortMapping(const char * controlURL, const char * servicetype, - const char * extPort, const char * proto, - const char * remoteHost); - -/* UPNP_GetPortMappingNumberOfEntries() - * not supported by all routers */ -LIBSPEC int -UPNP_GetPortMappingNumberOfEntries(const char* controlURL, - const char* servicetype, - unsigned int * num); - -/* UPNP_GetSpecificPortMappingEntry() - * retrieves an existing port mapping - * params : - * in extPort - * in proto - * out intClient (16 bytes) - * out intPort (6 bytes) - * out desc (80 bytes) - * out enabled (4 bytes) - * out leaseDuration (16 bytes) - * - * return value : - * UPNPCOMMAND_SUCCESS, UPNPCOMMAND_INVALID_ARGS, UPNPCOMMAND_UNKNOWN_ERROR - * or a UPnP Error Code. */ -LIBSPEC int -UPNP_GetSpecificPortMappingEntry(const char * controlURL, - const char * servicetype, - const char * extPort, - const char * proto, - char * intClient, - char * intPort, - char * desc, - char * enabled, - char * leaseDuration); - -/* UPNP_GetGenericPortMappingEntry() - * params : - * in index - * out extPort (6 bytes) - * out intClient (16 bytes) - * out intPort (6 bytes) - * out protocol (4 bytes) - * out desc (80 bytes) - * out enabled (4 bytes) - * out rHost (64 bytes) - * out duration (16 bytes) - * - * return value : - * UPNPCOMMAND_SUCCESS, UPNPCOMMAND_INVALID_ARGS, UPNPCOMMAND_UNKNOWN_ERROR - * or a UPnP Error Code. - * - * Possible UPNP Error codes : - * 402 Invalid Args - See UPnP Device Architecture section on Control. - * 713 SpecifiedArrayIndexInvalid - The specified array index is out of bounds - */ -LIBSPEC int -UPNP_GetGenericPortMappingEntry(const char * controlURL, - const char * servicetype, - const char * index, - char * extPort, - char * intClient, - char * intPort, - char * protocol, - char * desc, - char * enabled, - char * rHost, - char * duration); - -/* UPNP_GetListOfPortMappings() Available in IGD v2 - * - * - * Possible UPNP Error codes : - * 606 Action not Authorized - * 730 PortMappingNotFound - no port mapping is found in the specified range. - * 733 InconsistantParameters - NewStartPort and NewEndPort values are not - * consistent. - */ -LIBSPEC int -UPNP_GetListOfPortMappings(const char * controlURL, - const char * servicetype, - const char * startPort, - const char * endPort, - const char * protocol, - const char * numberOfPorts, - struct PortMappingParserData * data); - -/* IGD:2, functions for service WANIPv6FirewallControl:1 */ -LIBSPEC int -UPNP_GetFirewallStatus(const char * controlURL, - const char * servicetype, - int * firewallEnabled, - int * inboundPinholeAllowed); - -LIBSPEC int -UPNP_GetOutboundPinholeTimeout(const char * controlURL, const char * servicetype, - const char * remoteHost, - const char * remotePort, - const char * intClient, - const char * intPort, - const char * proto, - int * opTimeout); - -LIBSPEC int -UPNP_AddPinhole(const char * controlURL, const char * servicetype, - const char * remoteHost, - const char * remotePort, - const char * intClient, - const char * intPort, - const char * proto, - const char * leaseTime, - char * uniqueID); - -LIBSPEC int -UPNP_UpdatePinhole(const char * controlURL, const char * servicetype, - const char * uniqueID, - const char * leaseTime); - -LIBSPEC int -UPNP_DeletePinhole(const char * controlURL, const char * servicetype, const char * uniqueID); - -LIBSPEC int -UPNP_CheckPinholeWorking(const char * controlURL, const char * servicetype, - const char * uniqueID, int * isWorking); - -LIBSPEC int -UPNP_GetPinholePackets(const char * controlURL, const char * servicetype, - const char * uniqueID, int * packets); - -#ifdef __cplusplus -} -#endif - -#endif - diff --git a/Externals/miniupnpc/src/upnperrors.c b/Externals/miniupnpc/src/upnperrors.c deleted file mode 100644 index 644098f0d796..000000000000 --- a/Externals/miniupnpc/src/upnperrors.c +++ /dev/null @@ -1,104 +0,0 @@ -/* $Id: upnperrors.c,v 1.6 2012/03/15 01:02:03 nanard Exp $ */ -/* Project : miniupnp - * Author : Thomas BERNARD - * copyright (c) 2007 Thomas Bernard - * All Right reserved. - * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ - * This software is subjet to the conditions detailed in the - * provided LICENCE file. */ -#include -#include "upnperrors.h" -#include "upnpcommands.h" -#include "miniupnpc.h" - -const char * strupnperror(int err) -{ - const char * s = NULL; - switch(err) { - case UPNPCOMMAND_SUCCESS: - s = "Success"; - break; - case UPNPCOMMAND_UNKNOWN_ERROR: - s = "Miniupnpc Unknown Error"; - break; - case UPNPCOMMAND_INVALID_ARGS: - s = "Miniupnpc Invalid Arguments"; - break; - case UPNPDISCOVER_SOCKET_ERROR: - s = "Miniupnpc Socket error"; - break; - case UPNPDISCOVER_MEMORY_ERROR: - s = "Miniupnpc Memory allocation error"; - break; - case 401: - s = "Invalid Action"; - break; - case 402: - s = "Invalid Args"; - break; - case 501: - s = "Action Failed"; - break; - case 606: - s = "Action not authorized"; - break; - case 701: - s = "PinholeSpaceExhausted"; - break; - case 702: - s = "FirewallDisabled"; - break; - case 703: - s = "InboundPinholeNotAllowed"; - break; - case 704: - s = "NoSuchEntry"; - break; - case 705: - s = "ProtocolNotSupported"; - break; - case 706: - s = "InternalPortWildcardingNotAllowed"; - break; - case 707: - s = "ProtocolWildcardingNotAllowed"; - break; - case 708: - s = "WildcardNotPermittedInSrcIP"; - break; - case 709: - s = "NoPacketSent"; - break; - case 713: - s = "SpecifiedArrayIndexInvalid"; - break; - case 714: - s = "NoSuchEntryInArray"; - break; - case 715: - s = "WildCardNotPermittedInSrcIP"; - break; - case 716: - s = "WildCardNotPermittedInExtPort"; - break; - case 718: - s = "ConflictInMappingEntry"; - break; - case 724: - s = "SamePortValuesRequired"; - break; - case 725: - s = "OnlyPermanentLeasesSupported"; - break; - case 726: - s = "RemoteHostOnlySupportsWildcard"; - break; - case 727: - s = "ExternalPortOnlySupportsWildcard"; - break; - default: - s = "UnknownError"; - break; - } - return s; -} diff --git a/Externals/miniupnpc/src/upnperrors.h b/Externals/miniupnpc/src/upnperrors.h deleted file mode 100644 index 077d6938b2f7..000000000000 --- a/Externals/miniupnpc/src/upnperrors.h +++ /dev/null @@ -1,26 +0,0 @@ -/* $Id: upnperrors.h,v 1.4 2012/09/27 15:42:11 nanard Exp $ */ -/* (c) 2007 Thomas Bernard - * All rights reserved. - * MiniUPnP Project. - * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ - * This software is subjet to the conditions detailed in the - * provided LICENCE file. */ -#ifndef UPNPERRORS_H_INCLUDED -#define UPNPERRORS_H_INCLUDED - -#include "declspec.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/* strupnperror() - * Return a string description of the UPnP error code - * or NULL for undefinded errors */ -LIBSPEC const char * strupnperror(int err); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/Externals/miniupnpc/src/upnpreplyparse.c b/Externals/miniupnpc/src/upnpreplyparse.c deleted file mode 100644 index e3a0e992fc93..000000000000 --- a/Externals/miniupnpc/src/upnpreplyparse.c +++ /dev/null @@ -1,152 +0,0 @@ -/* $Id: upnpreplyparse.c,v 1.12 2012/03/05 19:42:48 nanard Exp $ */ -/* MiniUPnP project - * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ - * (c) 2006-2011 Thomas Bernard - * This software is subject to the conditions detailed - * in the LICENCE file provided within the distribution */ - -#include -#include -#include - -#include "upnpreplyparse.h" -#include "minixml.h" - -static void -NameValueParserStartElt(void * d, const char * name, int l) -{ - struct NameValueParserData * data = (struct NameValueParserData *)d; - if(l>63) - l = 63; - memcpy(data->curelt, name, l); - data->curelt[l] = '\0'; -} - -static void -NameValueParserGetData(void * d, const char * datas, int l) -{ - struct NameValueParserData * data = (struct NameValueParserData *)d; - struct NameValue * nv; - if(strcmp(data->curelt, "NewPortListing") == 0) - { - /* specific case for NewPortListing which is a XML Document */ - data->portListing = malloc(l + 1); - if(!data->portListing) - { - /* malloc error */ - return; - } - memcpy(data->portListing, datas, l); - data->portListing[l] = '\0'; - data->portListingLength = l; - } - else - { - /* standard case. Limited to 63 chars strings */ - nv = malloc(sizeof(struct NameValue)); - if(l>63) - l = 63; - strncpy(nv->name, data->curelt, 64); - nv->name[63] = '\0'; - memcpy(nv->value, datas, l); - nv->value[l] = '\0'; - LIST_INSERT_HEAD( &(data->head), nv, entries); - } -} - -void -ParseNameValue(const char * buffer, int bufsize, - struct NameValueParserData * data) -{ - struct xmlparser parser; - LIST_INIT(&(data->head)); - data->portListing = NULL; - data->portListingLength = 0; - /* init xmlparser object */ - parser.xmlstart = buffer; - parser.xmlsize = bufsize; - parser.data = data; - parser.starteltfunc = NameValueParserStartElt; - parser.endeltfunc = 0; - parser.datafunc = NameValueParserGetData; - parser.attfunc = 0; - parsexml(&parser); -} - -void -ClearNameValueList(struct NameValueParserData * pdata) -{ - struct NameValue * nv; - if(pdata->portListing) - { - free(pdata->portListing); - pdata->portListing = NULL; - pdata->portListingLength = 0; - } - while((nv = pdata->head.lh_first) != NULL) - { - LIST_REMOVE(nv, entries); - free(nv); - } -} - -char * -GetValueFromNameValueList(struct NameValueParserData * pdata, - const char * Name) -{ - struct NameValue * nv; - char * p = NULL; - for(nv = pdata->head.lh_first; - (nv != NULL) && (p == NULL); - nv = nv->entries.le_next) - { - if(strcmp(nv->name, Name) == 0) - p = nv->value; - } - return p; -} - -#if 0 -/* useless now that minixml ignores namespaces by itself */ -char * -GetValueFromNameValueListIgnoreNS(struct NameValueParserData * pdata, - const char * Name) -{ - struct NameValue * nv; - char * p = NULL; - char * pname; - for(nv = pdata->head.lh_first; - (nv != NULL) && (p == NULL); - nv = nv->entries.le_next) - { - pname = strrchr(nv->name, ':'); - if(pname) - pname++; - else - pname = nv->name; - if(strcmp(pname, Name)==0) - p = nv->value; - } - return p; -} -#endif - -/* debug all-in-one function - * do parsing then display to stdout */ -#ifdef DEBUG -void -DisplayNameValueList(char * buffer, int bufsize) -{ - struct NameValueParserData pdata; - struct NameValue * nv; - ParseNameValue(buffer, bufsize, &pdata); - for(nv = pdata.head.lh_first; - nv != NULL; - nv = nv->entries.le_next) - { - printf("%s = %s\n", nv->name, nv->value); - } - ClearNameValueList(&pdata); -} -#endif - diff --git a/Externals/miniupnpc/src/upnpreplyparse.h b/Externals/miniupnpc/src/upnpreplyparse.h deleted file mode 100644 index 78949167fa8e..000000000000 --- a/Externals/miniupnpc/src/upnpreplyparse.h +++ /dev/null @@ -1,64 +0,0 @@ -/* $Id: upnpreplyparse.h,v 1.14 2012/09/27 15:42:11 nanard Exp $ */ -/* MiniUPnP project - * http://miniupnp.free.fr/ or http://miniupnp.tuxfamily.org/ - * (c) 2006-2012 Thomas Bernard - * This software is subject to the conditions detailed - * in the LICENCE file provided within the distribution */ - -#ifndef UPNPREPLYPARSE_H_INCLUDED -#define UPNPREPLYPARSE_H_INCLUDED - -#if defined(NO_SYS_QUEUE_H) || defined(_WIN32) || defined(__HAIKU__) -#include "bsdqueue.h" -#else -#include -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -struct NameValue { - LIST_ENTRY(NameValue) entries; - char name[64]; - char value[64]; -}; - -struct NameValueParserData { - LIST_HEAD(listhead, NameValue) head; - char curelt[64]; - char * portListing; - int portListingLength; -}; - -/* ParseNameValue() */ -void -ParseNameValue(const char * buffer, int bufsize, - struct NameValueParserData * data); - -/* ClearNameValueList() */ -void -ClearNameValueList(struct NameValueParserData * pdata); - -/* GetValueFromNameValueList() */ -char * -GetValueFromNameValueList(struct NameValueParserData * pdata, - const char * Name); - -/* GetValueFromNameValueListIgnoreNS() */ -char * -GetValueFromNameValueListIgnoreNS(struct NameValueParserData * pdata, - const char * Name); - -/* DisplayNameValueList() */ -#ifdef DEBUG -void -DisplayNameValueList(char * buffer, int bufsize); -#endif - -#ifdef __cplusplus -} -#endif - -#endif - diff --git a/Source/Core/Common/Common.vcxproj b/Source/Core/Common/Common.vcxproj index 20cf92dfc3e2..105841f637d2 100644 --- a/Source/Core/Common/Common.vcxproj +++ b/Source/Core/Common/Common.vcxproj @@ -114,7 +114,7 @@ - ..\..\..\Externals\polarssl\include + ..\..\..\Externals\polarssl\include;..\..\..\Externals @@ -124,7 +124,7 @@ - ..\..\..\Externals\polarssl\include + ..\..\..\Externals\polarssl\include;..\..\..\Externals @@ -136,7 +136,7 @@ - ..\..\..\Externals\polarssl\include + ..\..\..\Externals\polarssl\include;..\..\..\Externals @@ -148,7 +148,7 @@ - ..\..\..\Externals\polarssl\include + ..\..\..\Externals\polarssl\include;..\..\..\Externals @@ -160,7 +160,7 @@ - ..\..\..\Externals\polarssl\include + ..\..\..\Externals\polarssl\include;..\..\..\Externals @@ -172,7 +172,7 @@ - ..\..\..\Externals\polarssl\include + ..\..\..\Externals\polarssl\include;..\..\..\Externals diff --git a/Source/Core/Core/Core.vcxproj b/Source/Core/Core/Core.vcxproj index d9eca7315a47..f42f2ef097ff 100644 --- a/Source/Core/Core/Core.vcxproj +++ b/Source/Core/Core/Core.vcxproj @@ -115,7 +115,7 @@ - .\Src;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\DiscIO\Src;..\InputCommon\Src;..\wiiuse\Src;..\..\..\Externals\Bochs_disasm;..\..\..\Externals\SFML\include;..\..\..\Externals\LZO;..\..\..\Externals\portaudio\include;..\..\..\Externals\zlib;..\..\..\Externals\miniupnpc\src;..\..\..\Externals\polarssl\include;..\..\..\Externals\libusbx\libusb;%(AdditionalIncludeDirectories) + .\Src;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\DiscIO\Src;..\InputCommon\Src;..\wiiuse\Src;..\..\..\Externals\Bochs_disasm;..\..\..\Externals\SFML\include;..\..\..\Externals\LZO;..\..\..\Externals\portaudio\include;..\..\..\Externals\zlib;..\..\..\Externals;..\..\..\Externals\polarssl\include;..\..\..\Externals\libusbx\libusb;%(AdditionalIncludeDirectories) true @@ -127,7 +127,7 @@ - .\Src;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\DiscIO\Src;..\InputCommon\Src;..\wiiuse\Src;..\..\..\Externals\Bochs_disasm;..\..\..\Externals\SFML\include;..\..\..\Externals\LZO;..\..\..\Externals\portaudio\include;..\..\..\Externals\zlib;..\..\..\Externals\miniupnpc\src;..\..\..\Externals\polarssl\include;..\..\..\Externals\libusbx\libusb;%(AdditionalIncludeDirectories) + .\Src;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\DiscIO\Src;..\InputCommon\Src;..\wiiuse\Src;..\..\..\Externals\Bochs_disasm;..\..\..\Externals\SFML\include;..\..\..\Externals\LZO;..\..\..\Externals\portaudio\include;..\..\..\Externals\zlib;..\..\..\Externals;..\..\..\Externals\polarssl\include;..\..\..\Externals\libusbx\libusb;%(AdditionalIncludeDirectories) true @@ -139,7 +139,7 @@ - .\Src;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\DiscIO\Src;..\InputCommon\Src;..\wiiuse\Src;..\..\..\Externals\Bochs_disasm;..\..\..\Externals\SFML\include;..\..\..\Externals\LZO;..\..\..\Externals\portaudio\include;..\..\..\Externals\zlib;..\..\..\Externals\miniupnpc\src;..\..\..\Externals\polarssl\include;..\..\..\Externals\libusbx\libusb;%(AdditionalIncludeDirectories) + .\Src;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\DiscIO\Src;..\InputCommon\Src;..\wiiuse\Src;..\..\..\Externals\Bochs_disasm;..\..\..\Externals\SFML\include;..\..\..\Externals\LZO;..\..\..\Externals\portaudio\include;..\..\..\Externals\zlib;..\..\..\Externals;..\..\..\Externals\polarssl\include;..\..\..\Externals\libusbx\libusb;%(AdditionalIncludeDirectories) true @@ -153,7 +153,7 @@ - .\Src;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\DiscIO\Src;..\InputCommon\Src;..\wiiuse\Src;..\..\..\Externals\Bochs_disasm;..\..\..\Externals\SFML\include;..\..\..\Externals\LZO;..\..\..\Externals\portaudio\include;..\..\..\Externals\zlib;..\..\..\Externals\miniupnpc\src;..\..\..\Externals\polarssl\include;..\..\..\Externals\libusbx\libusb;%(AdditionalIncludeDirectories) + .\Src;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\DiscIO\Src;..\InputCommon\Src;..\wiiuse\Src;..\..\..\Externals\Bochs_disasm;..\..\..\Externals\SFML\include;..\..\..\Externals\LZO;..\..\..\Externals\portaudio\include;..\..\..\Externals\zlib;..\..\..\Externals;..\..\..\Externals\polarssl\include;..\..\..\Externals\libusbx\libusb;%(AdditionalIncludeDirectories) true @@ -167,7 +167,7 @@ - .\Src;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\DiscIO\Src;..\InputCommon\Src;..\wiiuse\Src;..\..\..\Externals\Bochs_disasm;..\..\..\Externals\SFML\include;..\..\..\Externals\LZO;..\..\..\Externals\portaudio\include;..\..\..\Externals\zlib;..\..\..\Externals\miniupnpc\src;..\..\..\Externals\polarssl\include;..\..\..\Externals\libusbx\libusb;%(AdditionalIncludeDirectories) + .\Src;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\DiscIO\Src;..\InputCommon\Src;..\wiiuse\Src;..\..\..\Externals\Bochs_disasm;..\..\..\Externals\SFML\include;..\..\..\Externals\LZO;..\..\..\Externals\portaudio\include;..\..\..\Externals\zlib;..\..\..\Externals;..\..\..\Externals\polarssl\include;..\..\..\Externals\libusbx\libusb;%(AdditionalIncludeDirectories) true @@ -181,7 +181,7 @@ - .\Src;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\DiscIO\Src;..\InputCommon\Src;..\wiiuse\Src;..\..\..\Externals\Bochs_disasm;..\..\..\Externals\SFML\include;..\..\..\Externals\LZO;..\..\..\Externals\portaudio\include;..\..\..\Externals\zlib;..\..\..\Externals\miniupnpc\src;..\..\..\Externals\polarssl\include;..\..\..\Externals\libusbx\libusb;%(AdditionalIncludeDirectories) + .\Src;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\DiscIO\Src;..\InputCommon\Src;..\wiiuse\Src;..\..\..\Externals\Bochs_disasm;..\..\..\Externals\SFML\include;..\..\..\Externals\LZO;..\..\..\Externals\portaudio\include;..\..\..\Externals\zlib;..\..\..\Externals;..\..\..\Externals\polarssl\include;..\..\..\Externals\libusbx\libusb;%(AdditionalIncludeDirectories) true @@ -611,4 +611,4 @@ - \ No newline at end of file + diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 00fec793c7f4..f9b4fab4807d 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -12,13 +12,6 @@ NetPlayServer::~NetPlayServer() m_thread.join(); m_socket.Close(); } - -#ifdef USE_UPNP - if (m_upnp_thread.joinable()) - m_upnp_thread.join(); - m_upnp_thread = std::thread(&NetPlayServer::unmapPortThread); - m_upnp_thread.join(); -#endif } // called from ---GUI--- thread @@ -552,160 +545,3 @@ void NetPlayServer::SendToClients(sf::Packet& packet, const PlayerId skip_pid) if (i->second.pid && (i->second.pid != skip_pid)) i->second.socket.Send(packet); } - -#ifdef USE_UPNP -#include -#include -#include - -struct UPNPUrls NetPlayServer::m_upnp_urls; -struct IGDdatas NetPlayServer::m_upnp_data; -u16 NetPlayServer::m_upnp_mapped = 0; -bool NetPlayServer::m_upnp_inited = false; -bool NetPlayServer::m_upnp_error = false; -std::thread NetPlayServer::m_upnp_thread; - -// called from ---GUI--- thread -void NetPlayServer::TryPortmapping(u16 port) -{ - if (m_upnp_thread.joinable()) - m_upnp_thread.join(); - m_upnp_thread = std::thread(&NetPlayServer::mapPortThread, port); -} - -// UPnP thread: try to map a port -void NetPlayServer::mapPortThread(const u16 port) -{ - std::string ourIP = sf::IPAddress::GetLocalAddress().ToString(); - - if (!m_upnp_inited) - if (!initUPnP()) - goto fail; - - if (!UPnPMapPort(ourIP, port)) - goto fail; - - NOTICE_LOG(NETPLAY, "Successfully mapped port %d to %s.", port, ourIP.c_str()); - return; -fail: - WARN_LOG(NETPLAY, "Failed to map port %d to %s.", port, ourIP.c_str()); - return; -} - -// UPnP thread: try to unmap a port -void NetPlayServer::unmapPortThread() -{ - if (m_upnp_mapped > 0) - UPnPUnmapPort(m_upnp_mapped); -} - -// called from ---UPnP--- thread -// discovers the IGD -bool NetPlayServer::initUPnP() -{ - UPNPDev *devlist, *dev; - std::vector igds; - int descXMLsize = 0, upnperror = 0; - char *descXML; - - // Don't init if already inited - if (m_upnp_inited) - return true; - - // Don't init if it failed before - if (m_upnp_error) - return false; - - memset(&m_upnp_urls, 0, sizeof(UPNPUrls)); - memset(&m_upnp_data, 0, sizeof(IGDdatas)); - - // Find all UPnP devices - devlist = upnpDiscover(2000, NULL, NULL, 0, 0, &upnperror); - if (!devlist) - { - WARN_LOG(NETPLAY, "An error occured trying to discover UPnP devices."); - - m_upnp_error = true; - m_upnp_inited = false; - - return false; - } - - // Look for the IGD - dev = devlist; - while (dev) - { - if (strstr(dev->st, "InternetGatewayDevice")) - igds.push_back(dev); - dev = dev->pNext; - } - - std::vector::iterator i; - for (i = igds.begin(); i != igds.end(); i++) - { - dev = *i; - descXML = (char *) miniwget(dev->descURL, &descXMLsize, 0); - if (descXML) - { - parserootdesc(descXML, descXMLsize, &m_upnp_data); - free(descXML); - descXML = 0; - GetUPNPUrls(&m_upnp_urls, &m_upnp_data, dev->descURL, 0); - - NOTICE_LOG(NETPLAY, "Got info from IGD at %s.", dev->descURL); - break; - } - else - { - WARN_LOG(NETPLAY, "Error getting info from IGD at %s.", dev->descURL); - } - } - - freeUPNPDevlist(devlist); - - return true; -} - -// called from ---UPnP--- thread -// Attempt to portforward! -bool NetPlayServer::UPnPMapPort(const std::string& addr, const u16 port) -{ - char port_str[6] = { 0 }; - int result; - - if (m_upnp_mapped > 0) - UPnPUnmapPort(m_upnp_mapped); - - sprintf(port_str, "%d", port); - result = UPNP_AddPortMapping(m_upnp_urls.controlURL, m_upnp_data.first.servicetype, - port_str, port_str, addr.c_str(), - (std::string("dolphin-emu TCP on ") + addr).c_str(), - "TCP", NULL, NULL); - - if(result != 0) - return false; - - m_upnp_mapped = port; - - return true; -} - -// called from ---UPnP--- thread -// Attempt to stop portforwarding. -// -- -// NOTE: It is important that this happens! A few very crappy routers -// apparently do not delete UPnP mappings on their own, so if you leave them -// hanging, the NVRAM will fill with portmappings, and eventually all UPnP -// requests will fail silently, with the only recourse being a factory reset. -// -- -bool NetPlayServer::UPnPUnmapPort(const u16 port) -{ - char port_str[6] = { 0 }; - - sprintf(port_str, "%d", port); - UPNP_DeletePortMapping(m_upnp_urls.controlURL, m_upnp_data.first.servicetype, - port_str, "TCP", NULL); - - return true; -} -#endif diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 13acbcfb2b7e..1f0d7a875a14 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -44,10 +44,6 @@ class NetPlayServer bool is_connected; -#ifdef USE_UPNP - void TryPortmapping(u16 port); -#endif - private: class Client { @@ -94,22 +90,6 @@ class NetPlayServer sf::SocketTCP m_socket; std::thread m_thread; sf::Selector m_selector; - -#ifdef USE_UPNP - static void mapPortThread(const u16 port); - static void unmapPortThread(); - - static bool initUPnP(); - static bool UPnPMapPort(const std::string& addr, const u16 port); - static bool UPnPUnmapPort(const u16 port); - - static struct UPNPUrls m_upnp_urls; - static struct IGDdatas m_upnp_data; - static u16 m_upnp_mapped; - static bool m_upnp_inited; - static bool m_upnp_error; - static std::thread m_upnp_thread; -#endif }; #endif diff --git a/Source/Core/DolphinWX/CMakeLists.txt b/Source/Core/DolphinWX/CMakeLists.txt index 2a5fa298203c..6bec0bc19c2c 100644 --- a/Source/Core/DolphinWX/CMakeLists.txt +++ b/Source/Core/DolphinWX/CMakeLists.txt @@ -11,6 +11,7 @@ set(LIBS core audiocommon z sfml-network + enet ${GTK2_LIBRARIES}) if(NOT ANDROID) @@ -170,10 +171,6 @@ else() set(DOLPHIN_EXE ${DOLPHIN_EXE_BASE}-nogui) endif() -if(USE_UPNP) - set(LIBS ${LIBS} miniupnpc) -endif() - include(FindGettext) if(GETTEXT_MSGMERGE_EXECUTABLE AND GETTEXT_MSGFMT_EXECUTABLE AND wxWidgets_FOUND) file(GLOB LINGUAS ${CMAKE_SOURCE_DIR}/Languages/po/*.po) diff --git a/Source/Core/DolphinWX/Dolphin.vcxproj b/Source/Core/DolphinWX/Dolphin.vcxproj index 829a7d9f4e16..4f5091adc2e0 100644 --- a/Source/Core/DolphinWX/Dolphin.vcxproj +++ b/Source/Core/DolphinWX/Dolphin.vcxproj @@ -130,7 +130,7 @@ - ..\..\..\Externals\GLew\include;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\Core\Src;..\Core\Src\PowerPC\JitCommon;..\DebuggerWX\Src;..\..\..\Externals\Bochs_disasm;..\InputCommon\Src;..\DiscIO\Src;..\..\..\Externals\SFML\include;..\..\..\Externals\wxWidgets3;..\..\..\Externals\wxWidgets3\include;..\..\..\Externals\CLRun\include;..\..\..\Externals\miniupnpc\src;..\..\..\Externals\polarssl\include;%(AdditionalIncludeDirectories) + ..\..\..\Externals\GLew\include;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\Core\Src;..\Core\Src\PowerPC\JitCommon;..\DebuggerWX\Src;..\..\..\Externals\Bochs_disasm;..\InputCommon\Src;..\DiscIO\Src;..\..\..\Externals\SFML\include;..\..\..\Externals\wxWidgets3;..\..\..\Externals\wxWidgets3\include;..\..\..\Externals\CLRun\include;..\..\..\Externals\miniupnpc\src;..\..\..\Externals;..\..\..\Externals\polarssl\include;%(AdditionalIncludeDirectories) @@ -145,7 +145,7 @@ xcopy "$(SolutionDir)..\Externals\msvcrt\$(PlatformName)\*.dll" "$(TargetDir)" / - ..\..\..\Externals\GLew\include;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\Core\Src;..\Core\Src\PowerPC\JitCommon;..\DebuggerWX\Src;..\..\..\Externals\Bochs_disasm;..\InputCommon\Src;..\DiscIO\Src;..\..\..\Externals\SFML\include;..\..\..\Externals\wxWidgets3;..\..\..\Externals\wxWidgets3\include;..\..\..\Externals\CLRun\include;..\..\..\Externals\miniupnpc\src;..\..\..\Externals\polarssl\include;%(AdditionalIncludeDirectories) + ..\..\..\Externals\GLew\include;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\Core\Src;..\Core\Src\PowerPC\JitCommon;..\DebuggerWX\Src;..\..\..\Externals\Bochs_disasm;..\InputCommon\Src;..\DiscIO\Src;..\..\..\Externals\SFML\include;..\..\..\Externals\wxWidgets3;..\..\..\Externals\wxWidgets3\include;..\..\..\Externals\CLRun\include;..\..\..\Externals\miniupnpc\src;..\..\..\Externals;..\..\..\Externals\polarssl\include;%(AdditionalIncludeDirectories) @@ -160,7 +160,7 @@ xcopy "$(SolutionDir)..\Externals\msvcrt\$(PlatformName)\*.dll" "$(TargetDir)" / - ..\..\..\Externals\GLew\include;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\Core\Src;..\Core\Src\PowerPC\JitCommon;..\DebuggerWX\Src;..\..\..\Externals\Bochs_disasm;..\InputCommon\Src;..\DiscIO\Src;..\..\..\Externals\SFML\include;..\..\..\Externals\wxWidgets3;..\..\..\Externals\wxWidgets3\include;..\..\..\Externals\CLRun\include;..\..\..\Externals\miniupnpc\src;..\..\..\Externals\polarssl\include;%(AdditionalIncludeDirectories) + ..\..\..\Externals\GLew\include;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\Core\Src;..\Core\Src\PowerPC\JitCommon;..\DebuggerWX\Src;..\..\..\Externals\Bochs_disasm;..\InputCommon\Src;..\DiscIO\Src;..\..\..\Externals\SFML\include;..\..\..\Externals\wxWidgets3;..\..\..\Externals\wxWidgets3\include;..\..\..\Externals\CLRun\include;..\..\..\Externals\miniupnpc\src;..\..\..\Externals;..\..\..\Externals\polarssl\include;%(AdditionalIncludeDirectories) @@ -177,7 +177,7 @@ xcopy "$(SolutionDir)..\Externals\msvcrt\$(PlatformName)\*.dll" "$(TargetDir)" / - ..\..\..\Externals\GLew\include;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\Core\Src;..\Core\Src\PowerPC\JitCommon;..\DebuggerWX\Src;..\..\..\Externals\Bochs_disasm;..\InputCommon\Src;..\DiscIO\Src;..\..\..\Externals\SFML\include;..\..\..\Externals\wxWidgets3;..\..\..\Externals\wxWidgets3\include;..\..\..\Externals\CLRun\include;..\..\..\Externals\miniupnpc\src;..\..\..\Externals\polarssl\include;%(AdditionalIncludeDirectories) + ..\..\..\Externals\GLew\include;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\Core\Src;..\Core\Src\PowerPC\JitCommon;..\DebuggerWX\Src;..\..\..\Externals\Bochs_disasm;..\InputCommon\Src;..\DiscIO\Src;..\..\..\Externals\SFML\include;..\..\..\Externals\wxWidgets3;..\..\..\Externals\wxWidgets3\include;..\..\..\Externals\CLRun\include;..\..\..\Externals\miniupnpc\src;..\..\..\Externals;..\..\..\Externals\polarssl\include;%(AdditionalIncludeDirectories) @@ -192,7 +192,7 @@ xcopy "$(SolutionDir)..\Externals\msvcrt\$(PlatformName)\*.dll" "$(TargetDir)" / - ..\..\..\Externals\GLew\include;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\Core\Src;..\Core\Src\PowerPC\JitCommon;..\DebuggerWX\Src;..\..\..\Externals\Bochs_disasm;..\InputCommon\Src;..\DiscIO\Src;..\..\..\Externals\SFML\include;..\..\..\Externals\wxWidgets3;..\..\..\Externals\wxWidgets3\include;..\..\..\Externals\CLRun\include;..\..\..\Externals\miniupnpc\src;..\..\..\Externals\polarssl\include;%(AdditionalIncludeDirectories) + ..\..\..\Externals\GLew\include;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\Core\Src;..\Core\Src\PowerPC\JitCommon;..\DebuggerWX\Src;..\..\..\Externals\Bochs_disasm;..\InputCommon\Src;..\DiscIO\Src;..\..\..\Externals\SFML\include;..\..\..\Externals\wxWidgets3;..\..\..\Externals\wxWidgets3\include;..\..\..\Externals\CLRun\include;..\..\..\Externals\miniupnpc\src;..\..\..\Externals;..\..\..\Externals\polarssl\include;%(AdditionalIncludeDirectories) @@ -208,10 +208,13 @@ xcopy "$(SolutionDir)..\Externals\msvcrt\$(PlatformName)\*.dll" "$(TargetDir)" / + + ..\..\..\Externals\wxWidgets3\include;%(AdditionalIncludeDirectories) + - ..\..\..\Externals\GLew\include;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\Core\Src;..\Core\Src\PowerPC\JitCommon;..\DebuggerWX\Src;..\..\..\Externals\Bochs_disasm;..\InputCommon\Src;..\DiscIO\Src;..\..\..\Externals\SFML\include;..\..\..\Externals\wxWidgets3;..\..\..\Externals\wxWidgets3\include;..\..\..\Externals\CLRun\include;..\..\..\Externals\miniupnpc\src;..\..\..\Externals\polarssl\include;%(AdditionalIncludeDirectories) + ..\..\..\Externals\GLew\include;..\Common\Src;..\VideoCommon\Src;..\AudioCommon\Src;..\Core\Src;..\Core\Src\PowerPC\JitCommon;..\DebuggerWX\Src;..\..\..\Externals\Bochs_disasm;..\InputCommon\Src;..\DiscIO\Src;..\..\..\Externals\SFML\include;..\..\..\Externals\wxWidgets3;..\..\..\Externals\wxWidgets3\include;..\..\..\Externals\CLRun\include;..\..\..\Externals\miniupnpc\src;..\..\..\Externals;..\..\..\Externals\polarssl\include;%(AdditionalIncludeDirectories) @@ -345,6 +348,9 @@ xcopy "$(SolutionDir)..\Externals\msvcrt\$(PlatformName)\*.dll" "$(TargetDir)" / {cd3d4c3c-1027-4d33-b047-aec7b56d0bf6} + + {d5c805b7-acc6-4260-a7b9-e685ae5e54fc} + {01573c36-ac6e-49f6-94ba-572517eb9740} @@ -408,4 +414,4 @@ xcopy "$(SolutionDir)..\Externals\msvcrt\$(PlatformName)\*.dll" "$(TargetDir)" / - \ No newline at end of file + diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 0de2ee684d1b..fbe92ef538a5 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -153,10 +153,6 @@ NetPlaySetupDiag::NetPlaySetupDiag(wxWindow* const parent, const CGameListCtrl* wxBoxSizer* const top_szr = new wxBoxSizer(wxHORIZONTAL); top_szr->Add(port_lbl, 0, wxCENTER | wxRIGHT, 5); top_szr->Add(m_host_port_text, 0); -#ifdef USE_UPNP - m_upnp_chk = new wxCheckBox(host_tab, wxID_ANY, _("Forward port (UPnP)")); - top_szr->Add(m_upnp_chk, 0, wxALL | wxALIGN_RIGHT, 5); -#endif wxBoxSizer* const host_szr = new wxBoxSizer(wxVERTICAL); host_szr->Add(top_szr, 0, wxALL | wxEXPAND, 5); @@ -250,10 +246,6 @@ void NetPlaySetupDiag::OnHost(wxCommandEvent&) netplay_server->AdjustPadBufferSize(INITIAL_PAD_BUFFER_SIZE); if (netplay_server->is_connected) { -#ifdef USE_UPNP - if(m_upnp_chk->GetValue()) - netplay_server->TryPortmapping(port); -#endif MakeNetPlayDiag(port, game, true); } else diff --git a/Source/Core/DolphinWX/Src/NetWindow.h b/Source/Core/DolphinWX/Src/NetWindow.h index 0e55862259b1..1e7247a23790 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.h +++ b/Source/Core/DolphinWX/Src/NetWindow.h @@ -50,9 +50,6 @@ class NetPlaySetupDiag : public wxFrame *m_connect_ip_text; wxListBox* m_game_lbox; -#ifdef USE_UPNP - wxCheckBox* m_upnp_chk; -#endif const CGameListCtrl* const m_game_list; }; diff --git a/Source/Dolphin_2010.sln b/Source/Dolphin_2010.sln index 207795dd26a2..231572475c18 100644 --- a/Source/Dolphin_2010.sln +++ b/Source/Dolphin_2010.sln @@ -1,6 +1,6 @@ ďťż Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 +# Visual C++ Express 2010 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dolphin", "Core\DolphinWX\Dolphin.vcxproj", "{1B099EF8-6F87-47A2-A3E7-898A24584F49}" ProjectSection(ProjectDependencies) = postProject {8C60E805-0DA5-4E25-8F84-038DB504BB0D} = {8C60E805-0DA5-4E25-8F84-038DB504BB0D} @@ -11,6 +11,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dolphin", "Core\DolphinWX\D {AA862E5E-A993-497A-B6A0-0E8E94B10050} = {AA862E5E-A993-497A-B6A0-0E8E94B10050} {C87A4178-44F6-49B2-B7AA-C79AF1B8C534} = {C87A4178-44F6-49B2-B7AA-C79AF1B8C534} {B39AC394-5DB5-4DA9-9D98-09D46CA3701F} = {B39AC394-5DB5-4DA9-9D98-09D46CA3701F} + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC} = {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC} {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE} = {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE} {349EE8F9-7D25-4909-AAF5-FF3FADE72187} = {349EE8F9-7D25-4909-AAF5-FF3FADE72187} EndProjectSection @@ -120,327 +121,486 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PolarSSL", "..\Externals\po EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UnitTests", "UnitTests\UnitTests.vcxproj", "{40C636FA-B5BF-4D67-ABC8-376B524A7551}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "miniupnpc", "..\Externals\miniupnpc\miniupnpc.vcxproj", "{A680190D-0764-485B-9CF3-A82C5EDD5715}" -EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libusb", "..\Externals\libusbx\msvc\libusb_static_2010.vcxproj", "{349EE8F9-7D25-4909-AAF5-FF3FADE72187}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "enet", "..\Externals\enet\enet.vcxproj", "{D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Mixed Platforms = Debug|Mixed Platforms Debug|Win32 = Debug|Win32 Debug|x64 = Debug|x64 + DebugFast|Mixed Platforms = DebugFast|Mixed Platforms DebugFast|Win32 = DebugFast|Win32 DebugFast|x64 = DebugFast|x64 + Release|Mixed Platforms = Release|Mixed Platforms Release|Win32 = Release|Win32 Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1B099EF8-6F87-47A2-A3E7-898A24584F49}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {1B099EF8-6F87-47A2-A3E7-898A24584F49}.Debug|Mixed Platforms.Build.0 = Debug|x64 {1B099EF8-6F87-47A2-A3E7-898A24584F49}.Debug|Win32.ActiveCfg = Debug|Win32 {1B099EF8-6F87-47A2-A3E7-898A24584F49}.Debug|Win32.Build.0 = Debug|Win32 {1B099EF8-6F87-47A2-A3E7-898A24584F49}.Debug|x64.ActiveCfg = Debug|x64 {1B099EF8-6F87-47A2-A3E7-898A24584F49}.Debug|x64.Build.0 = Debug|x64 + {1B099EF8-6F87-47A2-A3E7-898A24584F49}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {1B099EF8-6F87-47A2-A3E7-898A24584F49}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {1B099EF8-6F87-47A2-A3E7-898A24584F49}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {1B099EF8-6F87-47A2-A3E7-898A24584F49}.DebugFast|Win32.Build.0 = DebugFast|Win32 {1B099EF8-6F87-47A2-A3E7-898A24584F49}.DebugFast|x64.ActiveCfg = DebugFast|x64 {1B099EF8-6F87-47A2-A3E7-898A24584F49}.DebugFast|x64.Build.0 = DebugFast|x64 + {1B099EF8-6F87-47A2-A3E7-898A24584F49}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {1B099EF8-6F87-47A2-A3E7-898A24584F49}.Release|Mixed Platforms.Build.0 = Release|x64 {1B099EF8-6F87-47A2-A3E7-898A24584F49}.Release|Win32.ActiveCfg = Release|Win32 {1B099EF8-6F87-47A2-A3E7-898A24584F49}.Release|Win32.Build.0 = Release|Win32 {1B099EF8-6F87-47A2-A3E7-898A24584F49}.Release|x64.ActiveCfg = Release|x64 {1B099EF8-6F87-47A2-A3E7-898A24584F49}.Release|x64.Build.0 = Release|x64 + {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.Debug|Mixed Platforms.Build.0 = Debug|x64 {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.Debug|Win32.ActiveCfg = Debug|Win32 {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.Debug|Win32.Build.0 = Debug|Win32 {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.Debug|x64.ActiveCfg = Debug|x64 {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.Debug|x64.Build.0 = Debug|x64 + {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.DebugFast|Win32.Build.0 = DebugFast|Win32 {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.DebugFast|x64.ActiveCfg = DebugFast|x64 {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.DebugFast|x64.Build.0 = DebugFast|x64 + {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.Release|Mixed Platforms.Build.0 = Release|x64 {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.Release|Win32.ActiveCfg = Release|Win32 {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.Release|Win32.Build.0 = Release|Win32 {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.Release|x64.ActiveCfg = Release|x64 {C87A4178-44F6-49B2-B7AA-C79AF1B8C534}.Release|x64.Build.0 = Release|x64 + {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.Debug|Mixed Platforms.Build.0 = Debug|x64 {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.Debug|Win32.ActiveCfg = Debug|Win32 {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.Debug|Win32.Build.0 = Debug|Win32 {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.Debug|x64.ActiveCfg = Debug|x64 {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.Debug|x64.Build.0 = Debug|x64 + {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.DebugFast|Win32.Build.0 = DebugFast|Win32 {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.DebugFast|x64.ActiveCfg = DebugFast|x64 {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.DebugFast|x64.Build.0 = DebugFast|x64 + {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.Release|Mixed Platforms.Build.0 = Release|x64 {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.Release|Win32.ActiveCfg = Release|Win32 {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.Release|Win32.Build.0 = Release|Win32 {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.Release|x64.ActiveCfg = Release|x64 {37D007BD-D66C-4EAF-B56C-BD1AAC340A05}.Release|x64.Build.0 = Release|x64 + {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.Debug|Mixed Platforms.Build.0 = Debug|x64 {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.Debug|Win32.ActiveCfg = Debug|Win32 {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.Debug|Win32.Build.0 = Debug|Win32 {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.Debug|x64.ActiveCfg = Debug|x64 {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.Debug|x64.Build.0 = Debug|x64 + {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.DebugFast|Win32.Build.0 = DebugFast|Win32 {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.DebugFast|x64.ActiveCfg = DebugFast|x64 {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.DebugFast|x64.Build.0 = DebugFast|x64 + {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.Release|Mixed Platforms.Build.0 = Release|x64 {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.Release|Win32.ActiveCfg = Release|Win32 {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.Release|Win32.Build.0 = Release|Win32 {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.Release|x64.ActiveCfg = Release|x64 {8544F1FF-F2A5-42D8-A568-C56B5D3B4181}.Release|x64.Build.0 = Release|x64 + {93D73454-2512-424E-9CDA-4BB357FE13DD}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {93D73454-2512-424E-9CDA-4BB357FE13DD}.Debug|Mixed Platforms.Build.0 = Debug|x64 {93D73454-2512-424E-9CDA-4BB357FE13DD}.Debug|Win32.ActiveCfg = Debug|Win32 {93D73454-2512-424E-9CDA-4BB357FE13DD}.Debug|Win32.Build.0 = Debug|Win32 {93D73454-2512-424E-9CDA-4BB357FE13DD}.Debug|x64.ActiveCfg = Debug|x64 {93D73454-2512-424E-9CDA-4BB357FE13DD}.Debug|x64.Build.0 = Debug|x64 + {93D73454-2512-424E-9CDA-4BB357FE13DD}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {93D73454-2512-424E-9CDA-4BB357FE13DD}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {93D73454-2512-424E-9CDA-4BB357FE13DD}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {93D73454-2512-424E-9CDA-4BB357FE13DD}.DebugFast|Win32.Build.0 = DebugFast|Win32 {93D73454-2512-424E-9CDA-4BB357FE13DD}.DebugFast|x64.ActiveCfg = DebugFast|x64 {93D73454-2512-424E-9CDA-4BB357FE13DD}.DebugFast|x64.Build.0 = DebugFast|x64 + {93D73454-2512-424E-9CDA-4BB357FE13DD}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {93D73454-2512-424E-9CDA-4BB357FE13DD}.Release|Mixed Platforms.Build.0 = Release|x64 {93D73454-2512-424E-9CDA-4BB357FE13DD}.Release|Win32.ActiveCfg = Release|Win32 {93D73454-2512-424E-9CDA-4BB357FE13DD}.Release|Win32.Build.0 = Release|Win32 {93D73454-2512-424E-9CDA-4BB357FE13DD}.Release|x64.ActiveCfg = Release|x64 {93D73454-2512-424E-9CDA-4BB357FE13DD}.Release|x64.Build.0 = Release|x64 + {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.Debug|Mixed Platforms.Build.0 = Debug|x64 {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.Debug|Win32.ActiveCfg = Debug|Win32 {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.Debug|Win32.Build.0 = Debug|Win32 {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.Debug|x64.ActiveCfg = Debug|x64 {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.Debug|x64.Build.0 = Debug|x64 + {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.DebugFast|Win32.Build.0 = DebugFast|Win32 {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.DebugFast|x64.ActiveCfg = DebugFast|x64 {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.DebugFast|x64.Build.0 = DebugFast|x64 + {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.Release|Mixed Platforms.Build.0 = Release|x64 {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.Release|Win32.ActiveCfg = Release|Win32 {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.Release|Win32.Build.0 = Release|Win32 {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.Release|x64.ActiveCfg = Release|x64 {D8890B98-26F7-4CFF-BBFB-B95F371B5F20}.Release|x64.Build.0 = Release|x64 + {3E1339F5-9311-4122-9442-369702E8FCAD}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {3E1339F5-9311-4122-9442-369702E8FCAD}.Debug|Mixed Platforms.Build.0 = Debug|x64 {3E1339F5-9311-4122-9442-369702E8FCAD}.Debug|Win32.ActiveCfg = Debug|Win32 {3E1339F5-9311-4122-9442-369702E8FCAD}.Debug|Win32.Build.0 = Debug|Win32 {3E1339F5-9311-4122-9442-369702E8FCAD}.Debug|x64.ActiveCfg = Debug|x64 {3E1339F5-9311-4122-9442-369702E8FCAD}.Debug|x64.Build.0 = Debug|x64 + {3E1339F5-9311-4122-9442-369702E8FCAD}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {3E1339F5-9311-4122-9442-369702E8FCAD}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {3E1339F5-9311-4122-9442-369702E8FCAD}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {3E1339F5-9311-4122-9442-369702E8FCAD}.DebugFast|Win32.Build.0 = DebugFast|Win32 {3E1339F5-9311-4122-9442-369702E8FCAD}.DebugFast|x64.ActiveCfg = DebugFast|x64 {3E1339F5-9311-4122-9442-369702E8FCAD}.DebugFast|x64.Build.0 = DebugFast|x64 + {3E1339F5-9311-4122-9442-369702E8FCAD}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {3E1339F5-9311-4122-9442-369702E8FCAD}.Release|Mixed Platforms.Build.0 = Release|x64 {3E1339F5-9311-4122-9442-369702E8FCAD}.Release|Win32.ActiveCfg = Release|Win32 {3E1339F5-9311-4122-9442-369702E8FCAD}.Release|Win32.Build.0 = Release|Win32 {3E1339F5-9311-4122-9442-369702E8FCAD}.Release|x64.ActiveCfg = Release|x64 {3E1339F5-9311-4122-9442-369702E8FCAD}.Release|x64.Build.0 = Release|x64 + {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.Debug|Mixed Platforms.Build.0 = Debug|x64 {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.Debug|Win32.ActiveCfg = Debug|Win32 {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.Debug|Win32.Build.0 = Debug|Win32 {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.Debug|x64.ActiveCfg = Debug|x64 {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.Debug|x64.Build.0 = Debug|x64 + {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.DebugFast|Win32.Build.0 = DebugFast|Win32 {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.DebugFast|x64.ActiveCfg = DebugFast|x64 {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.DebugFast|x64.Build.0 = DebugFast|x64 + {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.Release|Mixed Platforms.Build.0 = Release|x64 {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.Release|Win32.ActiveCfg = Release|Win32 {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.Release|Win32.Build.0 = Release|Win32 {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.Release|x64.ActiveCfg = Release|x64 {3E5C4E02-1BA9-4776-BDBE-E3F91FFA34CF}.Release|x64.Build.0 = Release|x64 + {AA862E5E-A993-497A-B6A0-0E8E94B10050}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {AA862E5E-A993-497A-B6A0-0E8E94B10050}.Debug|Mixed Platforms.Build.0 = Debug|x64 {AA862E5E-A993-497A-B6A0-0E8E94B10050}.Debug|Win32.ActiveCfg = Debug|Win32 {AA862E5E-A993-497A-B6A0-0E8E94B10050}.Debug|Win32.Build.0 = Debug|Win32 {AA862E5E-A993-497A-B6A0-0E8E94B10050}.Debug|x64.ActiveCfg = Debug|x64 {AA862E5E-A993-497A-B6A0-0E8E94B10050}.Debug|x64.Build.0 = Debug|x64 + {AA862E5E-A993-497A-B6A0-0E8E94B10050}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {AA862E5E-A993-497A-B6A0-0E8E94B10050}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {AA862E5E-A993-497A-B6A0-0E8E94B10050}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {AA862E5E-A993-497A-B6A0-0E8E94B10050}.DebugFast|Win32.Build.0 = DebugFast|Win32 {AA862E5E-A993-497A-B6A0-0E8E94B10050}.DebugFast|x64.ActiveCfg = DebugFast|x64 {AA862E5E-A993-497A-B6A0-0E8E94B10050}.DebugFast|x64.Build.0 = DebugFast|x64 + {AA862E5E-A993-497A-B6A0-0E8E94B10050}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {AA862E5E-A993-497A-B6A0-0E8E94B10050}.Release|Mixed Platforms.Build.0 = Release|x64 {AA862E5E-A993-497A-B6A0-0E8E94B10050}.Release|Win32.ActiveCfg = Release|Win32 {AA862E5E-A993-497A-B6A0-0E8E94B10050}.Release|Win32.Build.0 = Release|Win32 {AA862E5E-A993-497A-B6A0-0E8E94B10050}.Release|x64.ActiveCfg = Release|x64 {AA862E5E-A993-497A-B6A0-0E8E94B10050}.Release|x64.Build.0 = Release|x64 + {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.Debug|Mixed Platforms.Build.0 = Debug|x64 {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.Debug|Win32.ActiveCfg = Debug|Win32 {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.Debug|Win32.Build.0 = Debug|Win32 {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.Debug|x64.ActiveCfg = Debug|x64 {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.Debug|x64.Build.0 = Debug|x64 + {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.DebugFast|Win32.Build.0 = DebugFast|Win32 {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.DebugFast|x64.ActiveCfg = DebugFast|x64 {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.DebugFast|x64.Build.0 = DebugFast|x64 + {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.Release|Mixed Platforms.Build.0 = Release|x64 {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.Release|Win32.ActiveCfg = Release|Win32 {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.Release|Win32.Build.0 = Release|Win32 {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.Release|x64.ActiveCfg = Release|x64 {8C60E805-0DA5-4E25-8F84-038DB504BB0D}.Release|x64.Build.0 = Release|x64 + {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.Debug|Mixed Platforms.Build.0 = Debug|x64 {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.Debug|Win32.ActiveCfg = Debug|Win32 {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.Debug|Win32.Build.0 = Debug|Win32 {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.Debug|x64.ActiveCfg = Debug|x64 {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.Debug|x64.Build.0 = Debug|x64 + {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.DebugFast|Win32.Build.0 = DebugFast|Win32 {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.DebugFast|x64.ActiveCfg = DebugFast|x64 {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.DebugFast|x64.Build.0 = DebugFast|x64 + {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.Release|Mixed Platforms.Build.0 = Release|x64 {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.Release|Win32.ActiveCfg = Release|Win32 {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.Release|Win32.Build.0 = Release|Win32 {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.Release|x64.ActiveCfg = Release|x64 {CD3D4C3C-1027-4D33-B047-AEC7B56D0BF6}.Release|x64.Build.0 = Release|x64 + {B6398059-EBB6-4C34-B547-95F365B71FF4}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {B6398059-EBB6-4C34-B547-95F365B71FF4}.Debug|Mixed Platforms.Build.0 = Debug|x64 {B6398059-EBB6-4C34-B547-95F365B71FF4}.Debug|Win32.ActiveCfg = Debug|Win32 {B6398059-EBB6-4C34-B547-95F365B71FF4}.Debug|Win32.Build.0 = Debug|Win32 {B6398059-EBB6-4C34-B547-95F365B71FF4}.Debug|x64.ActiveCfg = Debug|x64 {B6398059-EBB6-4C34-B547-95F365B71FF4}.Debug|x64.Build.0 = Debug|x64 + {B6398059-EBB6-4C34-B547-95F365B71FF4}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {B6398059-EBB6-4C34-B547-95F365B71FF4}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {B6398059-EBB6-4C34-B547-95F365B71FF4}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {B6398059-EBB6-4C34-B547-95F365B71FF4}.DebugFast|Win32.Build.0 = DebugFast|Win32 {B6398059-EBB6-4C34-B547-95F365B71FF4}.DebugFast|x64.ActiveCfg = DebugFast|x64 {B6398059-EBB6-4C34-B547-95F365B71FF4}.DebugFast|x64.Build.0 = DebugFast|x64 + {B6398059-EBB6-4C34-B547-95F365B71FF4}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {B6398059-EBB6-4C34-B547-95F365B71FF4}.Release|Mixed Platforms.Build.0 = Release|x64 {B6398059-EBB6-4C34-B547-95F365B71FF4}.Release|Win32.ActiveCfg = Release|Win32 {B6398059-EBB6-4C34-B547-95F365B71FF4}.Release|Win32.Build.0 = Release|Win32 {B6398059-EBB6-4C34-B547-95F365B71FF4}.Release|x64.ActiveCfg = Release|x64 {B6398059-EBB6-4C34-B547-95F365B71FF4}.Release|x64.Build.0 = Release|x64 + {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.Debug|Mixed Platforms.Build.0 = Debug|x64 {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.Debug|Win32.ActiveCfg = Debug|Win32 {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.Debug|Win32.Build.0 = Debug|Win32 {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.Debug|x64.ActiveCfg = Debug|x64 {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.Debug|x64.Build.0 = Debug|x64 + {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.DebugFast|Win32.Build.0 = DebugFast|Win32 {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.DebugFast|x64.ActiveCfg = DebugFast|x64 {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.DebugFast|x64.Build.0 = DebugFast|x64 + {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.Release|Mixed Platforms.Build.0 = Release|x64 {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.Release|Win32.ActiveCfg = Release|Win32 {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.Release|Win32.Build.0 = Release|Win32 {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.Release|x64.ActiveCfg = Release|x64 {DC7D7AF4-CE47-49E8-8B63-265CB6233A49}.Release|x64.Build.0 = Release|x64 + {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.Debug|Mixed Platforms.Build.0 = Debug|x64 {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.Debug|Win32.ActiveCfg = Debug|Win32 {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.Debug|Win32.Build.0 = Debug|Win32 {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.Debug|x64.ActiveCfg = Debug|x64 {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.Debug|x64.Build.0 = Debug|x64 + {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.DebugFast|Win32.Build.0 = DebugFast|Win32 {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.DebugFast|x64.ActiveCfg = DebugFast|x64 {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.DebugFast|x64.Build.0 = DebugFast|x64 + {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.Release|Mixed Platforms.Build.0 = Release|x64 {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.Release|Win32.ActiveCfg = Release|Win32 {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.Release|Win32.Build.0 = Release|Win32 {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.Release|x64.ActiveCfg = Release|x64 {9A4C733C-BADE-4AC6-B58A-6E274395E90E}.Release|x64.Build.0 = Release|x64 + {1909CD2D-1707-456F-86CA-0DF42A727C99}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {1909CD2D-1707-456F-86CA-0DF42A727C99}.Debug|Mixed Platforms.Build.0 = Debug|x64 {1909CD2D-1707-456F-86CA-0DF42A727C99}.Debug|Win32.ActiveCfg = Debug|Win32 {1909CD2D-1707-456F-86CA-0DF42A727C99}.Debug|Win32.Build.0 = Debug|Win32 {1909CD2D-1707-456F-86CA-0DF42A727C99}.Debug|x64.ActiveCfg = Debug|x64 {1909CD2D-1707-456F-86CA-0DF42A727C99}.Debug|x64.Build.0 = Debug|x64 + {1909CD2D-1707-456F-86CA-0DF42A727C99}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {1909CD2D-1707-456F-86CA-0DF42A727C99}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {1909CD2D-1707-456F-86CA-0DF42A727C99}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {1909CD2D-1707-456F-86CA-0DF42A727C99}.DebugFast|Win32.Build.0 = DebugFast|Win32 {1909CD2D-1707-456F-86CA-0DF42A727C99}.DebugFast|x64.ActiveCfg = DebugFast|x64 {1909CD2D-1707-456F-86CA-0DF42A727C99}.DebugFast|x64.Build.0 = DebugFast|x64 + {1909CD2D-1707-456F-86CA-0DF42A727C99}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {1909CD2D-1707-456F-86CA-0DF42A727C99}.Release|Mixed Platforms.Build.0 = Release|x64 {1909CD2D-1707-456F-86CA-0DF42A727C99}.Release|Win32.ActiveCfg = Release|Win32 {1909CD2D-1707-456F-86CA-0DF42A727C99}.Release|Win32.Build.0 = Release|Win32 {1909CD2D-1707-456F-86CA-0DF42A727C99}.Release|x64.ActiveCfg = Release|x64 {1909CD2D-1707-456F-86CA-0DF42A727C99}.Release|x64.Build.0 = Release|x64 + {9E9DA440-E9AD-413C-B648-91030E792211}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {9E9DA440-E9AD-413C-B648-91030E792211}.Debug|Mixed Platforms.Build.0 = Debug|x64 {9E9DA440-E9AD-413C-B648-91030E792211}.Debug|Win32.ActiveCfg = Debug|Win32 {9E9DA440-E9AD-413C-B648-91030E792211}.Debug|Win32.Build.0 = Debug|Win32 {9E9DA440-E9AD-413C-B648-91030E792211}.Debug|x64.ActiveCfg = Debug|x64 {9E9DA440-E9AD-413C-B648-91030E792211}.Debug|x64.Build.0 = Debug|x64 + {9E9DA440-E9AD-413C-B648-91030E792211}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {9E9DA440-E9AD-413C-B648-91030E792211}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {9E9DA440-E9AD-413C-B648-91030E792211}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {9E9DA440-E9AD-413C-B648-91030E792211}.DebugFast|Win32.Build.0 = DebugFast|Win32 {9E9DA440-E9AD-413C-B648-91030E792211}.DebugFast|x64.ActiveCfg = DebugFast|x64 {9E9DA440-E9AD-413C-B648-91030E792211}.DebugFast|x64.Build.0 = DebugFast|x64 + {9E9DA440-E9AD-413C-B648-91030E792211}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {9E9DA440-E9AD-413C-B648-91030E792211}.Release|Mixed Platforms.Build.0 = Release|x64 {9E9DA440-E9AD-413C-B648-91030E792211}.Release|Win32.ActiveCfg = Release|Win32 {9E9DA440-E9AD-413C-B648-91030E792211}.Release|Win32.Build.0 = Release|Win32 {9E9DA440-E9AD-413C-B648-91030E792211}.Release|x64.ActiveCfg = Release|x64 {9E9DA440-E9AD-413C-B648-91030E792211}.Release|x64.Build.0 = Release|x64 + {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.Debug|Mixed Platforms.Build.0 = Debug|x64 {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.Debug|Win32.ActiveCfg = Debug|Win32 {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.Debug|Win32.Build.0 = Debug|Win32 {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.Debug|x64.ActiveCfg = Debug|x64 {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.Debug|x64.Build.0 = Debug|x64 + {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.DebugFast|Win32.Build.0 = DebugFast|Win32 {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.DebugFast|x64.ActiveCfg = DebugFast|x64 {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.DebugFast|x64.Build.0 = DebugFast|x64 + {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.Release|Mixed Platforms.Build.0 = Release|x64 {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.Release|Win32.ActiveCfg = Release|Win32 {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.Release|Win32.Build.0 = Release|Win32 {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.Release|x64.ActiveCfg = Release|x64 {B39AC394-5DB5-4DA9-9D98-09D46CA3701F}.Release|x64.Build.0 = Release|x64 + {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.Debug|Mixed Platforms.ActiveCfg = Release|x64 + {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.Debug|Mixed Platforms.Build.0 = Release|x64 {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.Debug|Win32.ActiveCfg = Release|Win32 {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.Debug|Win32.Build.0 = Release|Win32 {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.Debug|x64.ActiveCfg = Release|x64 {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.Debug|x64.Build.0 = Release|x64 + {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.DebugFast|Mixed Platforms.ActiveCfg = Release|x64 + {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.DebugFast|Mixed Platforms.Build.0 = Release|x64 {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.DebugFast|Win32.ActiveCfg = Release|Win32 {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.DebugFast|Win32.Build.0 = Release|Win32 {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.DebugFast|x64.ActiveCfg = Release|x64 {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.DebugFast|x64.Build.0 = Release|x64 + {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.Release|Mixed Platforms.Build.0 = Release|x64 {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.Release|Win32.ActiveCfg = Release|Win32 {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.Release|Win32.Build.0 = Release|Win32 {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.Release|x64.ActiveCfg = Release|x64 {0B8D0A82-C520-46BA-849D-3BB8F637EE0C}.Release|x64.Build.0 = Release|x64 + {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Debug|Mixed Platforms.Build.0 = Debug|x64 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Debug|Win32.ActiveCfg = Debug|Win32 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Debug|Win32.Build.0 = Debug|Win32 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Debug|x64.ActiveCfg = Debug|x64 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Debug|x64.Build.0 = Debug|x64 + {1970D175-3DE8-4738-942A-4D98D1CDBF64}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {1970D175-3DE8-4738-942A-4D98D1CDBF64}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.DebugFast|Win32.Build.0 = DebugFast|Win32 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.DebugFast|x64.ActiveCfg = DebugFast|x64 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.DebugFast|x64.Build.0 = DebugFast|x64 + {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Release|Mixed Platforms.Build.0 = Release|x64 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Release|Win32.ActiveCfg = Release|Win32 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Release|Win32.Build.0 = Release|Win32 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Release|x64.ActiveCfg = Release|x64 {1970D175-3DE8-4738-942A-4D98D1CDBF64}.Release|x64.Build.0 = Release|x64 + {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.Debug|Mixed Platforms.Build.0 = Debug|x64 {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.Debug|Win32.ActiveCfg = Debug|Win32 {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.Debug|Win32.Build.0 = Debug|Win32 {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.Debug|x64.ActiveCfg = Debug|x64 {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.Debug|x64.Build.0 = Debug|x64 + {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.DebugFast|Win32.Build.0 = DebugFast|Win32 {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.DebugFast|x64.ActiveCfg = DebugFast|x64 {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.DebugFast|x64.Build.0 = DebugFast|x64 + {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.Release|Mixed Platforms.Build.0 = Release|x64 {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.Release|Win32.ActiveCfg = Release|Win32 {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.Release|Win32.Build.0 = Release|Win32 {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.Release|x64.ActiveCfg = Release|x64 {1C8436C9-DBAF-42BE-83BC-CF3EC9175ABE}.Release|x64.Build.0 = Release|x64 + {01573C36-AC6E-49F6-94BA-572517EB9740}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {01573C36-AC6E-49F6-94BA-572517EB9740}.Debug|Mixed Platforms.Build.0 = Debug|x64 {01573C36-AC6E-49F6-94BA-572517EB9740}.Debug|Win32.ActiveCfg = Debug|Win32 {01573C36-AC6E-49F6-94BA-572517EB9740}.Debug|Win32.Build.0 = Debug|Win32 {01573C36-AC6E-49F6-94BA-572517EB9740}.Debug|x64.ActiveCfg = Debug|x64 {01573C36-AC6E-49F6-94BA-572517EB9740}.Debug|x64.Build.0 = Debug|x64 + {01573C36-AC6E-49F6-94BA-572517EB9740}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {01573C36-AC6E-49F6-94BA-572517EB9740}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {01573C36-AC6E-49F6-94BA-572517EB9740}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {01573C36-AC6E-49F6-94BA-572517EB9740}.DebugFast|Win32.Build.0 = DebugFast|Win32 {01573C36-AC6E-49F6-94BA-572517EB9740}.DebugFast|x64.ActiveCfg = DebugFast|x64 {01573C36-AC6E-49F6-94BA-572517EB9740}.DebugFast|x64.Build.0 = DebugFast|x64 + {01573C36-AC6E-49F6-94BA-572517EB9740}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {01573C36-AC6E-49F6-94BA-572517EB9740}.Release|Mixed Platforms.Build.0 = Release|x64 {01573C36-AC6E-49F6-94BA-572517EB9740}.Release|Win32.ActiveCfg = Release|Win32 {01573C36-AC6E-49F6-94BA-572517EB9740}.Release|Win32.Build.0 = Release|Win32 {01573C36-AC6E-49F6-94BA-572517EB9740}.Release|x64.ActiveCfg = Release|x64 {01573C36-AC6E-49F6-94BA-572517EB9740}.Release|x64.Build.0 = Release|x64 + {69F00340-5C3D-449F-9A80-958435C6CF06}.Debug|Mixed Platforms.ActiveCfg = Release|x64 + {69F00340-5C3D-449F-9A80-958435C6CF06}.Debug|Mixed Platforms.Build.0 = Release|x64 {69F00340-5C3D-449F-9A80-958435C6CF06}.Debug|Win32.ActiveCfg = Release|x64 {69F00340-5C3D-449F-9A80-958435C6CF06}.Debug|Win32.Build.0 = Release|x64 {69F00340-5C3D-449F-9A80-958435C6CF06}.Debug|x64.ActiveCfg = Release|x64 {69F00340-5C3D-449F-9A80-958435C6CF06}.Debug|x64.Build.0 = Release|x64 + {69F00340-5C3D-449F-9A80-958435C6CF06}.DebugFast|Mixed Platforms.ActiveCfg = Release|x64 + {69F00340-5C3D-449F-9A80-958435C6CF06}.DebugFast|Mixed Platforms.Build.0 = Release|x64 {69F00340-5C3D-449F-9A80-958435C6CF06}.DebugFast|Win32.ActiveCfg = Release|x64 {69F00340-5C3D-449F-9A80-958435C6CF06}.DebugFast|Win32.Build.0 = Release|x64 {69F00340-5C3D-449F-9A80-958435C6CF06}.DebugFast|x64.ActiveCfg = Release|x64 {69F00340-5C3D-449F-9A80-958435C6CF06}.DebugFast|x64.Build.0 = Release|x64 + {69F00340-5C3D-449F-9A80-958435C6CF06}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {69F00340-5C3D-449F-9A80-958435C6CF06}.Release|Mixed Platforms.Build.0 = Release|x64 {69F00340-5C3D-449F-9A80-958435C6CF06}.Release|Win32.ActiveCfg = Release|x64 {69F00340-5C3D-449F-9A80-958435C6CF06}.Release|Win32.Build.0 = Release|x64 {69F00340-5C3D-449F-9A80-958435C6CF06}.Release|x64.ActiveCfg = Release|x64 {69F00340-5C3D-449F-9A80-958435C6CF06}.Release|x64.Build.0 = Release|x64 + {68A5DD20-7057-448B-8FE0-B6AC8D205509}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {68A5DD20-7057-448B-8FE0-B6AC8D205509}.Debug|Mixed Platforms.Build.0 = Debug|x64 {68A5DD20-7057-448B-8FE0-B6AC8D205509}.Debug|Win32.ActiveCfg = Debug|Win32 {68A5DD20-7057-448B-8FE0-B6AC8D205509}.Debug|Win32.Build.0 = Debug|Win32 {68A5DD20-7057-448B-8FE0-B6AC8D205509}.Debug|x64.ActiveCfg = Debug|x64 {68A5DD20-7057-448B-8FE0-B6AC8D205509}.Debug|x64.Build.0 = Debug|x64 + {68A5DD20-7057-448B-8FE0-B6AC8D205509}.DebugFast|Mixed Platforms.ActiveCfg = Debug|x64 + {68A5DD20-7057-448B-8FE0-B6AC8D205509}.DebugFast|Mixed Platforms.Build.0 = Debug|x64 {68A5DD20-7057-448B-8FE0-B6AC8D205509}.DebugFast|Win32.ActiveCfg = Debug|Win32 {68A5DD20-7057-448B-8FE0-B6AC8D205509}.DebugFast|Win32.Build.0 = Debug|Win32 {68A5DD20-7057-448B-8FE0-B6AC8D205509}.DebugFast|x64.ActiveCfg = Release|x64 {68A5DD20-7057-448B-8FE0-B6AC8D205509}.DebugFast|x64.Build.0 = Release|x64 + {68A5DD20-7057-448B-8FE0-B6AC8D205509}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {68A5DD20-7057-448B-8FE0-B6AC8D205509}.Release|Mixed Platforms.Build.0 = Release|x64 {68A5DD20-7057-448B-8FE0-B6AC8D205509}.Release|Win32.ActiveCfg = Release|Win32 {68A5DD20-7057-448B-8FE0-B6AC8D205509}.Release|Win32.Build.0 = Release|Win32 {68A5DD20-7057-448B-8FE0-B6AC8D205509}.Release|x64.ActiveCfg = Release|x64 {68A5DD20-7057-448B-8FE0-B6AC8D205509}.Release|x64.Build.0 = Release|x64 + {46CF2D25-6A36-4189-B59C-E4815388E554}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {46CF2D25-6A36-4189-B59C-E4815388E554}.Debug|Mixed Platforms.Build.0 = Debug|x64 {46CF2D25-6A36-4189-B59C-E4815388E554}.Debug|Win32.ActiveCfg = Debug|Win32 {46CF2D25-6A36-4189-B59C-E4815388E554}.Debug|Win32.Build.0 = Debug|Win32 {46CF2D25-6A36-4189-B59C-E4815388E554}.Debug|x64.ActiveCfg = Debug|x64 {46CF2D25-6A36-4189-B59C-E4815388E554}.Debug|x64.Build.0 = Debug|x64 + {46CF2D25-6A36-4189-B59C-E4815388E554}.DebugFast|Mixed Platforms.ActiveCfg = DebugFast|x64 + {46CF2D25-6A36-4189-B59C-E4815388E554}.DebugFast|Mixed Platforms.Build.0 = DebugFast|x64 {46CF2D25-6A36-4189-B59C-E4815388E554}.DebugFast|Win32.ActiveCfg = DebugFast|Win32 {46CF2D25-6A36-4189-B59C-E4815388E554}.DebugFast|Win32.Build.0 = DebugFast|Win32 {46CF2D25-6A36-4189-B59C-E4815388E554}.DebugFast|x64.ActiveCfg = DebugFast|x64 {46CF2D25-6A36-4189-B59C-E4815388E554}.DebugFast|x64.Build.0 = DebugFast|x64 + {46CF2D25-6A36-4189-B59C-E4815388E554}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {46CF2D25-6A36-4189-B59C-E4815388E554}.Release|Mixed Platforms.Build.0 = Release|x64 {46CF2D25-6A36-4189-B59C-E4815388E554}.Release|Win32.ActiveCfg = Release|Win32 {46CF2D25-6A36-4189-B59C-E4815388E554}.Release|Win32.Build.0 = Release|Win32 {46CF2D25-6A36-4189-B59C-E4815388E554}.Release|x64.ActiveCfg = Release|x64 {46CF2D25-6A36-4189-B59C-E4815388E554}.Release|x64.Build.0 = Release|x64 + {40C636FA-B5BF-4D67-ABC8-376B524A7551}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {40C636FA-B5BF-4D67-ABC8-376B524A7551}.Debug|Mixed Platforms.Build.0 = Debug|x64 {40C636FA-B5BF-4D67-ABC8-376B524A7551}.Debug|Win32.ActiveCfg = Debug|Win32 {40C636FA-B5BF-4D67-ABC8-376B524A7551}.Debug|Win32.Build.0 = Debug|Win32 {40C636FA-B5BF-4D67-ABC8-376B524A7551}.Debug|x64.ActiveCfg = Debug|x64 {40C636FA-B5BF-4D67-ABC8-376B524A7551}.Debug|x64.Build.0 = Debug|x64 + {40C636FA-B5BF-4D67-ABC8-376B524A7551}.DebugFast|Mixed Platforms.ActiveCfg = Debug|x64 + {40C636FA-B5BF-4D67-ABC8-376B524A7551}.DebugFast|Mixed Platforms.Build.0 = Debug|x64 {40C636FA-B5BF-4D67-ABC8-376B524A7551}.DebugFast|Win32.ActiveCfg = Debug|x64 {40C636FA-B5BF-4D67-ABC8-376B524A7551}.DebugFast|x64.ActiveCfg = Debug|x64 {40C636FA-B5BF-4D67-ABC8-376B524A7551}.DebugFast|x64.Build.0 = Debug|x64 + {40C636FA-B5BF-4D67-ABC8-376B524A7551}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {40C636FA-B5BF-4D67-ABC8-376B524A7551}.Release|Mixed Platforms.Build.0 = Release|x64 {40C636FA-B5BF-4D67-ABC8-376B524A7551}.Release|Win32.ActiveCfg = Release|Win32 {40C636FA-B5BF-4D67-ABC8-376B524A7551}.Release|Win32.Build.0 = Release|Win32 {40C636FA-B5BF-4D67-ABC8-376B524A7551}.Release|x64.ActiveCfg = Release|x64 {40C636FA-B5BF-4D67-ABC8-376B524A7551}.Release|x64.Build.0 = Release|x64 + {A680190D-0764-485B-9CF3-A82C5EDD5715}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {A680190D-0764-485B-9CF3-A82C5EDD5715}.Debug|Mixed Platforms.Build.0 = Debug|x64 {A680190D-0764-485B-9CF3-A82C5EDD5715}.Debug|Win32.ActiveCfg = Debug|Win32 {A680190D-0764-485B-9CF3-A82C5EDD5715}.Debug|Win32.Build.0 = Debug|Win32 {A680190D-0764-485B-9CF3-A82C5EDD5715}.Debug|x64.ActiveCfg = Debug|x64 {A680190D-0764-485B-9CF3-A82C5EDD5715}.Debug|x64.Build.0 = Debug|x64 + {A680190D-0764-485B-9CF3-A82C5EDD5715}.DebugFast|Mixed Platforms.ActiveCfg = Debug|x64 + {A680190D-0764-485B-9CF3-A82C5EDD5715}.DebugFast|Mixed Platforms.Build.0 = Debug|x64 {A680190D-0764-485B-9CF3-A82C5EDD5715}.DebugFast|Win32.ActiveCfg = Debug|Win32 {A680190D-0764-485B-9CF3-A82C5EDD5715}.DebugFast|Win32.Build.0 = Debug|Win32 {A680190D-0764-485B-9CF3-A82C5EDD5715}.DebugFast|x64.ActiveCfg = Debug|x64 {A680190D-0764-485B-9CF3-A82C5EDD5715}.DebugFast|x64.Build.0 = Debug|x64 + {A680190D-0764-485B-9CF3-A82C5EDD5715}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {A680190D-0764-485B-9CF3-A82C5EDD5715}.Release|Mixed Platforms.Build.0 = Release|x64 {A680190D-0764-485B-9CF3-A82C5EDD5715}.Release|Win32.ActiveCfg = Release|Win32 {A680190D-0764-485B-9CF3-A82C5EDD5715}.Release|Win32.Build.0 = Release|Win32 {A680190D-0764-485B-9CF3-A82C5EDD5715}.Release|x64.ActiveCfg = Release|x64 @@ -457,6 +617,23 @@ Global {349EE8F9-7D25-4909-AAF5-FF3FADE72187}.Release|Win32.Build.0 = Release|Win32 {349EE8F9-7D25-4909-AAF5-FF3FADE72187}.Release|x64.ActiveCfg = Release|x64 {349EE8F9-7D25-4909-AAF5-FF3FADE72187}.Release|x64.Build.0 = Release|x64 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.Debug|Mixed Platforms.ActiveCfg = Debug|x64 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.Debug|Mixed Platforms.Build.0 = Debug|x64 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.Debug|Win32.ActiveCfg = Debug|Win32 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.Debug|Win32.Build.0 = Debug|Win32 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.Debug|x64.ActiveCfg = Debug|x64 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.Debug|x64.Build.0 = Debug|x64 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.DebugFast|Mixed Platforms.ActiveCfg = Debug|x64 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.DebugFast|Mixed Platforms.Build.0 = Debug|x64 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.DebugFast|Win32.ActiveCfg = Debug|x64 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.DebugFast|x64.ActiveCfg = Debug|x64 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.DebugFast|x64.Build.0 = Debug|x64 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.Release|Mixed Platforms.ActiveCfg = Release|x64 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.Release|Mixed Platforms.Build.0 = Release|x64 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.Release|Win32.ActiveCfg = Release|Win32 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.Release|Win32.Build.0 = Release|Win32 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.Release|x64.ActiveCfg = Release|x64 + {D5C805B7-ACC6-4260-A7B9-E685AE5E54FC}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/VSProps/Base.props b/Source/VSProps/Base.props index e1201912fd24..a36642bdcec1 100644 --- a/Source/VSProps/Base.props +++ b/Source/VSProps/Base.props @@ -8,7 +8,7 @@ - _CRT_SECURE_NO_WARNINGS;_CRT_SECURE_NO_DEPRECATE;USE_UPNP;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions) Level3 true 16Bytes From 36c8bc876ff2186004b3d59c299a2b119e9622f2 Mon Sep 17 00:00:00 2001 From: comex Date: Fri, 27 Sep 2013 02:09:51 -0400 Subject: [PATCH 004/202] Add simple STUN client. --- Source/Core/Common/CMakeLists.txt | 1 + Source/Core/Common/Common.vcxproj | 2 + Source/Core/Common/Common.vcxproj.filters | 2 + Source/Core/Common/Src/STUNClient.cpp | 271 ++++++++++++++++++++++ Source/Core/Common/Src/STUNClient.h | 39 ++++ 5 files changed, 315 insertions(+) create mode 100644 Source/Core/Common/Src/STUNClient.cpp create mode 100644 Source/Core/Common/Src/STUNClient.h diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index 0302ee78c86a..dc8ffdc94a68 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -16,6 +16,7 @@ set(SRCS Src/BreakPoints.cpp Src/SettingsHandler.cpp Src/SDCardUtil.cpp Src/StringUtil.cpp + Src/STUNClient.cpp Src/SymbolDB.cpp Src/SysConf.cpp Src/Thread.cpp diff --git a/Source/Core/Common/Common.vcxproj b/Source/Core/Common/Common.vcxproj index 105841f637d2..7e06603f9cb4 100644 --- a/Source/Core/Common/Common.vcxproj +++ b/Source/Core/Common/Common.vcxproj @@ -206,6 +206,7 @@ Create Create + @@ -258,6 +259,7 @@ + diff --git a/Source/Core/Common/Common.vcxproj.filters b/Source/Core/Common/Common.vcxproj.filters index 05b9edcb14df..b66d3d774d7c 100644 --- a/Source/Core/Common/Common.vcxproj.filters +++ b/Source/Core/Common/Common.vcxproj.filters @@ -18,6 +18,7 @@ + @@ -80,6 +81,7 @@ + diff --git a/Source/Core/Common/Src/STUNClient.cpp b/Source/Core/Common/Src/STUNClient.cpp new file mode 100644 index 000000000000..932529723ff2 --- /dev/null +++ b/Source/Core/Common/Src/STUNClient.cpp @@ -0,0 +1,271 @@ +// Copyright 2013 Dolphin Emulator Project +// Licensed under GPLv2 +// Refer to the license.txt file included. + +#include "STUNClient.h" +#include "enet/time.h" +#include +#include + +struct STUNHeader +{ + u16 type; + u16 length; + u32 cookie; + u8 transactionID[12]; +}; + +struct STUNAttributeHeader +{ + u16 type; + u16 length; +}; + +struct STUNMappedAddress +{ + u8 zero; + u8 family; + u16 port; +}; + +static void GetRandomishBytes(u8 *buf, size_t size) +{ + // We don't need high quality random numbers (which might not be available), + // just non-repeating numbers! + srand(enet_time_get()); + for (size_t i = 0; i < size; i++) + buf[i] = rand() & 0xff; +} + +static bool SendSTUNMessage(ENetSocket socket, const ENetAddress *address, u8 transactionID[12]) +{ + STUNHeader header; + header.type = Common::swap16(0x0001); // Binding Request + header.length = Common::swap16((u16) 0); + header.cookie = Common::swap32(0x2112A442); + memcpy(header.transactionID, transactionID, 12); + + ENetBuffer buf; + buf.data = &header; + buf.dataLength = sizeof(header); + if (enet_socket_send(socket, address, &buf, 1) != sizeof(header)) + { + ERROR_LOG(NETPLAY, "Failed to send STUN message."); + return false; + } + return true; +} + +static bool ParseSTUNMessage(u8 *data, size_t length, ENetAddress *myAddress) +{ + // This is simple! It just has a lot of error checking. + auto header = (STUNHeader *) data; + if (Common::swap16(header->length) + sizeof(STUNHeader) != length) + { + ERROR_LOG(NETPLAY, "Received invalid STUN packet (bad length)."); + return false; + } + + std::unordered_map attributes; + u8 *ptr = (u8 *) (header + 1), *end = ptr + (length - sizeof(STUNHeader)); + while (ptr < end) + { + auto attribHeader = (STUNAttributeHeader *) ptr; + attribHeader->type = Common::swap16(attribHeader->type); + attribHeader->length = Common::swap16(attribHeader->length); + ptr += sizeof(STUNAttributeHeader); + if (attribHeader->length > end - ptr) + { + ERROR_LOG(NETPLAY, "Received invalid STUN packet (bad attribute length %u / %u).", (int) attribHeader->length, (int) (end - ptr)); + return false; + } + attributes[attribHeader->type] = attribHeader; + ptr += attribHeader->length; + } + + u16 type = Common::swap16(header->type); + if (type == 0x0111) // Binding Error Response + { + // search for a reason + auto it = attributes.find(0x0009); // ERROR-CODE + if (it == attributes.end()) + { + ERROR_LOG(NETPLAY, "Received invalid STUN packet (Binding Error Response with no ERROR-CODE)"); + return false; + } + STUNAttributeHeader *attribHeader = it->second; + if (attribHeader->length < 8) + { + ERROR_LOG(NETPLAY, "Received invalid STUN packet (bad ERROR-CODE length)"); + return false; + } + u8 *errorCodeHeader = (u8 *) (attribHeader + 1); + u32 classAndNumber = Common::swap32(*(u32 *) (errorCodeHeader + 4)); + u32 code = ((classAndNumber >> 8) & 7) * 100 + (classAndNumber & 0xff); + char *desc = (char *) (errorCodeHeader + 8); + ERROR_LOG(NETPLAY, "Received STUN Binding Error %u: \"%*s\"", code, attribHeader->length - 8, desc); + return false; + } + else if (type == 0x0101) // Binding Response + { + auto it = attributes.end(); // since MSVC can't stand decltype + bool isXor; + if ((it = attributes.find(0x0020)) != attributes.end()) + { + // XOR-MAPPED-ADDRESS + isXor = true; + } + else if((it = attributes.find(0x0001)) != attributes.end()) + { + // old-fashioned MAPPED-ADDRESS + isXor = false; + } + else + { + ERROR_LOG(NETPLAY, "Received invalid STUN packet (Binding Response with no address)"); + return false; + } + STUNAttributeHeader *attribHeader = it->second; + if (attribHeader->length < sizeof(STUNMappedAddress)) + { + ERROR_LOG(NETPLAY, "Received invalid STUN packet (bad address header)"); + return false; + } + auto mappedAddress = (STUNMappedAddress *) (attribHeader + 1); + u16 port = Common::swap16(mappedAddress->port); + if (isXor) + port ^= (Common::swap32(header->cookie) >> 16); + size_t addrLength; + if (mappedAddress->family == 0x01) // IPv4 + addrLength = 4; + else if (mappedAddress->family == 0x02) // IPV6 + addrLength = 16; + else + { + ERROR_LOG(NETPLAY, "Received invalid STUN packet (unknown family)"); + return false; + } + if (attribHeader->length != sizeof(STUNMappedAddress) + addrLength) + { + ERROR_LOG(NETPLAY, "Received invalid STUN packet (bad address header)"); + return false; + } + u8 *addrBuf = (u8 *) (mappedAddress + 1); + if (isXor) + { + u8 *pad = (u8 *) &header->cookie; + for (size_t i = 0; i < addrLength; i++) + addrBuf[i] ^= pad[i]; + } + if (mappedAddress->family == 0x01) + { + myAddress->host = *(u32 *) addrBuf; + myAddress->port = port; + return true; + } + else + { + ERROR_LOG(NETPLAY, "Received IPv6 address from STUN, but enet doesn't support IPv6 yet :("); + return false; + } + } + else + { + ERROR_LOG(NETPLAY, "Received unknown STUN packet type 0x%02x", type); + return false; + } +} + +STUNClient::STUNClient() +{ + m_Status = Error; +} + +STUNClient::STUNClient(ENetSocket socket, std::vector servers) +: m_Socket(socket), m_Servers(std::move(servers)) +{ + m_Status = Waiting; + TryNextServer(); +} + +void STUNClient::TryNextServer() +{ + while (1) + { + if (m_Servers.empty()) + { + ERROR_LOG(NETPLAY, "No more STUN servers to try."); + m_Status = Timeout; + return; + } + + const char *server = m_Servers.front().c_str(); + if (enet_address_set_host(&m_CurrentServer, server) < 0) + { + ERROR_LOG(NETPLAY, "DNS lookup of %s failed.", server); + continue; + } + m_CurrentServer.port = 3478; + + GetRandomishBytes(m_TransactionID, sizeof(m_TransactionID)); + m_Tries = 0; + DoTry(); + return; + } +} + +void STUNClient::DoTry() +{ + if (++m_Tries > 5) + { + ERROR_LOG(NETPLAY, "Timed out waiting for STUN server %s.", m_Servers.front().c_str()); + TryNextServer(); + return; + } + + m_TryStartTime = enet_time_get(); + if (!SendSTUNMessage(m_Socket, &m_CurrentServer, m_TransactionID)) + { + TryNextServer(); + } +} + +bool STUNClient::ReceivedPacket(u8 *data, size_t length, const ENetAddress* from) +{ + if (from->host != m_CurrentServer.host || from->port != m_CurrentServer.port) + return false; + if (length < sizeof(STUNHeader)) + return false; + + auto header = (STUNHeader *) data; + if (header->cookie != Common::swap32(0x2112A442)) + return false; + if (memcmp(header->transactionID, m_TransactionID, 12)) + return false; + + // At this point we assume that the packet is for us. + if (m_Status != Waiting) + { + // unnecessary packet + return true; + } + + if (ParseSTUNMessage(data, length, &m_MyAddress)) + m_Status = Ok; + else + m_Status = Error; + return true; +} + +void STUNClient::Ping() +{ + // By the way, does this macro's implementation even make sense? + enet_uint32 now = enet_time_get(); + if (ENET_TIME_DIFFERENCE(m_TryStartTime, now) >= 500) + { + m_TryStartTime = now; + DoTry(); + } +} + + diff --git a/Source/Core/Common/Src/STUNClient.h b/Source/Core/Common/Src/STUNClient.h new file mode 100644 index 000000000000..47b92cb17961 --- /dev/null +++ b/Source/Core/Common/Src/STUNClient.h @@ -0,0 +1,39 @@ +// Copyright 2013 Dolphin Emulator Project +// Licensed under GPLv2 +// Refer to the license.txt file included. + +#pragma once + +#include "Common.h" +#include "enet/enet.h" +#include + +class STUNClient +{ +public: + enum Status + { + Waiting, + Ok, + Error, + Timeout + }; + + STUNClient(); + STUNClient(ENetSocket socket, std::vector servers); + // returns whether it was a STUN packet + bool ReceivedPacket(u8 *data, size_t length, const ENetAddress* from); + void Ping(); + ENetAddress m_MyAddress; + Status m_Status; +private: + ENetSocket m_Socket; + std::vector m_Servers; + ENetAddress m_CurrentServer; + int m_Tries; + enet_uint32 m_TryStartTime; + u8 m_TransactionID[12]; + + void TryNextServer(); + void DoTry(); +}; From e8ad6ea60aa3e358f600cdcb5b284f8a5aa6146b Mon Sep 17 00:00:00 2001 From: comex Date: Sun, 29 Sep 2013 16:24:54 -0400 Subject: [PATCH 005/202] Meanwhile... add a copy of the right-click ISO options to the File menu. This complies with HIGs and will help if users search for a replacement for "Start Netplay". --- Source/Core/DolphinWX/Src/Frame.cpp | 20 +++- Source/Core/DolphinWX/Src/Frame.h | 4 +- Source/Core/DolphinWX/Src/FrameTools.cpp | 2 + Source/Core/DolphinWX/Src/GameListCtrl.cpp | 128 ++++++++++++++------- Source/Core/DolphinWX/Src/GameListCtrl.h | 6 + 5 files changed, 114 insertions(+), 46 deletions(-) diff --git a/Source/Core/DolphinWX/Src/Frame.cpp b/Source/Core/DolphinWX/Src/Frame.cpp index a85e20f46deb..3d6f1055f794 100644 --- a/Source/Core/DolphinWX/Src/Frame.cpp +++ b/Source/Core/DolphinWX/Src/Frame.cpp @@ -305,9 +305,6 @@ CFrame::CFrame(wxFrame* parent, if (!SConfig::GetInstance().m_InterfaceStatusbar) GetStatusBar()->Hide(); - // Give it a menu bar - CreateMenu(); - // --------------- // Main panel // This panel is the parent for rendering and it holds the gamelistctrl @@ -322,6 +319,9 @@ CFrame::CFrame(wxFrame* parent, m_Panel->SetSizer(sizerPanel); // --------------- + // Give it a menu bar (must be done after creating the GameListCtrl) + CreateMenu(); + // Manager m_Mgr = new wxAuiManager(this, wxAUI_MGR_DEFAULT | wxAUI_MGR_LIVE_RESIZE); @@ -1067,7 +1067,17 @@ void CFrame::DoFullscreen(bool bF) } } -const CGameListCtrl *CFrame::GetGameListCtrl() const +bool CFrame::ProcessEvent(wxEvent& event) { - return m_GameListCtrl; + // Try us first... + if (wxEvtHandler::ProcessEvent(event)) + return true; + if (event.GetId() >= IDM_LOADSTATE && event.GetId() <= IDM_HOST_MESSAGE && + m_GameListCtrl) + { + // but it might be from the game list control's contextual menu added + // to File. + return m_GameListCtrl->ProcessWindowEventLocally(event); + } + return false; } diff --git a/Source/Core/DolphinWX/Src/Frame.h b/Source/Core/DolphinWX/Src/Frame.h index 376ced5b104e..c36b19661a16 100644 --- a/Source/Core/DolphinWX/Src/Frame.h +++ b/Source/Core/DolphinWX/Src/Frame.h @@ -137,8 +137,6 @@ class CFrame : public CRenderFrame void UpdateWiiMenuChoice(wxMenuItem *WiiMenuItem=NULL); static void ConnectWiimote(int wm_idx, bool connect); - const CGameListCtrl *GetGameListCtrl() const; - #ifdef __WXGTK__ Common::Event panic_event; bool bPanicResult; @@ -166,6 +164,8 @@ class CFrame : public CRenderFrame std::vector Perspectives; u32 ActivePerspective; + virtual bool ProcessEvent(wxEvent& event); + private: CGameListCtrl* m_GameListCtrl; wxPanel* m_Panel; diff --git a/Source/Core/DolphinWX/Src/FrameTools.cpp b/Source/Core/DolphinWX/Src/FrameTools.cpp index 5864c6ae8c04..0ef5dde720ca 100644 --- a/Source/Core/DolphinWX/Src/FrameTools.cpp +++ b/Source/Core/DolphinWX/Src/FrameTools.cpp @@ -111,6 +111,8 @@ void CFrame::CreateMenu() fileMenu->Append(IDM_BROWSE, _("&Browse for ISOs...")); fileMenu->AppendSeparator(); fileMenu->Append(wxID_EXIT, _("E&xit") + wxString(wxT("\tAlt+F4"))); + fileMenu->AppendSeparator(); + m_GameListCtrl->SetFileMenu(fileMenu); m_MenuBar->Append(fileMenu, _("&File")); // Emulation menu diff --git a/Source/Core/DolphinWX/Src/GameListCtrl.cpp b/Source/Core/DolphinWX/Src/GameListCtrl.cpp index 300bf470c085..c0a55ae77973 100644 --- a/Source/Core/DolphinWX/Src/GameListCtrl.cpp +++ b/Source/Core/DolphinWX/Src/GameListCtrl.cpp @@ -160,6 +160,8 @@ BEGIN_EVENT_TABLE(CGameListCtrl, wxListCtrl) EVT_MOTION(CGameListCtrl::OnMouseMotion) EVT_LIST_COL_BEGIN_DRAG(LIST_CTRL, CGameListCtrl::OnColBeginDrag) EVT_LIST_COL_CLICK(LIST_CTRL, CGameListCtrl::OnColumnClick) + EVT_LIST_ITEM_SELECTED(LIST_CTRL, CGameListCtrl::OnItemSelectedDeselected) + EVT_LIST_ITEM_DESELECTED(LIST_CTRL, CGameListCtrl::OnItemSelectedDeselected) EVT_MENU(IDM_PROPERTIES, CGameListCtrl::OnProperties) EVT_MENU(IDM_GAMEWIKI, CGameListCtrl::OnWiki) EVT_MENU(IDM_OPENCONTAININGFOLDER, CGameListCtrl::OnOpenContainingFolder) @@ -724,6 +726,7 @@ void CGameListCtrl::OnKeyPress(wxListEvent& event) SetItemState(i, wxLIST_STATE_SELECTED|wxLIST_STATE_FOCUSED, wxLIST_STATE_SELECTED|wxLIST_STATE_FOCUSED); EnsureVisible(i); + UpdateFileMenu(); break; } @@ -736,6 +739,11 @@ void CGameListCtrl::OnKeyPress(wxListEvent& event) event.Skip(); } +void CGameListCtrl::OnItemSelectedDeselected(wxListEvent& event) +{ + UpdateFileMenu(); +} + // This shows a little tooltip with the current Game's emulation state void CGameListCtrl::OnMouseMotion(wxMouseEvent& event) { @@ -840,59 +848,100 @@ void CGameListCtrl::OnRightClick(wxMouseEvent& event) { UnselectAll(); SetItemState(item, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED ); + UpdateFileMenu(); } SetItemState(item, wxLIST_STATE_FOCUSED, wxLIST_STATE_FOCUSED); } - if (GetSelectedItemCount() == 1) + if (GetSelectedISO()) { - const GameListItem *selected_iso = GetSelectedISO(); - if (selected_iso) - { - wxMenu* popupMenu = new wxMenu; - popupMenu->Append(IDM_PROPERTIES, _("&Properties")); - popupMenu->Append(IDM_GAMEWIKI, _("&Wiki")); - popupMenu->AppendSeparator(); + wxMenu* popupMenu = new wxMenu; + AppendContextMenuOptions(popupMenu); + PopupMenu(popupMenu); + } +} - if (selected_iso->GetPlatform() != GameListItem::GAMECUBE_DISC) +void CGameListCtrl::SetFileMenu(wxMenu* fileMenu) +{ + m_FileMenu = fileMenu; + UpdateFileMenu(); +} + +// Call this after changing the selection. +void CGameListCtrl::UpdateFileMenu() +{ + // Remove the old stuff + wxMenuItemList& list = m_FileMenu->GetMenuItems(); + for (auto it = list.begin(); it != list.end(); ++it) + { + int id = (*it)->GetId(); + if (id == IDM_PROPERTIES || id == IDM_DELETEGCM) + { + for (; it != list.end();) { - popupMenu->Append(IDM_OPENSAVEFOLDER, _("Open Wii &save folder")); - popupMenu->Append(IDM_EXPORTSAVE, _("Export Wii save (Experimental)")); + m_FileMenu->Delete(*it++); } - popupMenu->Append(IDM_OPENCONTAININGFOLDER, _("Open &containing folder")); - popupMenu->AppendCheckItem(IDM_SETDEFAULTGCM, _("Set as &default ISO")); + break; + } + } + AppendContextMenuOptions(m_FileMenu); +} + +void CGameListCtrl::AppendContextMenuOptions(wxMenu* menu) +{ + if (GetSelectedItemCount() <= 1) + { + const GameListItem *selected_iso = GetSelectedISO(); - // First we have to decide a starting value when we append it - if(selected_iso->GetFileName() == SConfig::GetInstance(). - m_LocalCoreStartupParameter.m_strDefaultGCM) - popupMenu->FindItem(IDM_SETDEFAULTGCM)->Check(); + menu->Append(IDM_PROPERTIES, _("&Properties")); + menu->Append(IDM_GAMEWIKI, _("&Wiki")); + menu->AppendSeparator(); - popupMenu->AppendSeparator(); - popupMenu->Append(IDM_DELETEGCM, _("&Delete ISO...")); + if (selected_iso && selected_iso->GetPlatform() != GameListItem::GAMECUBE_DISC) + { + menu->Append(IDM_OPENSAVEFOLDER, _("Open Wii &save folder")); + menu->Append(IDM_EXPORTSAVE, _("Export Wii save (Experimental)")); + } + menu->Append(IDM_OPENCONTAININGFOLDER, _("Open &containing folder")); + menu->AppendCheckItem(IDM_SETDEFAULTGCM, _("Set as &default ISO")); - if (selected_iso->GetPlatform() != GameListItem::WII_WAD) - { - if (selected_iso->IsCompressed()) - popupMenu->Append(IDM_COMPRESSGCM, _("Decompress ISO...")); - else if (selected_iso->GetFileName().substr(selected_iso->GetFileName().find_last_of(".")) != ".ciso" - && selected_iso->GetFileName().substr(selected_iso->GetFileName().find_last_of(".")) != ".wbfs") - popupMenu->Append(IDM_COMPRESSGCM, _("Compress ISO...")); - } - else - { - popupMenu->Append(IDM_LIST_INSTALLWAD, _("Install to Wii Menu")); - } + // First we have to decide a starting value when we append it + if (selected_iso && + selected_iso->GetFileName() == SConfig::GetInstance(). + m_LocalCoreStartupParameter.m_strDefaultGCM) + menu->FindItem(IDM_SETDEFAULTGCM)->Check(); + + menu->AppendSeparator(); + menu->Append(IDM_DELETEGCM, _("&Delete ISO...")); - PopupMenu(popupMenu); + if (selected_iso && selected_iso->GetPlatform() != GameListItem::WII_WAD) + { + if (selected_iso->IsCompressed()) + menu->Append(IDM_COMPRESSGCM, _("Decompress ISO...")); + else if (selected_iso->GetFileName().substr(selected_iso->GetFileName().find_last_of(".")) != ".ciso" + && selected_iso->GetFileName().substr(selected_iso->GetFileName().find_last_of(".")) != ".wbfs") + menu->Append(IDM_COMPRESSGCM, _("Compress ISO...")); + } + else + { + menu->Append(IDM_LIST_INSTALLWAD, _("Install to Wii Menu")); + } + + if (!selected_iso) + { + menu->Enable(IDM_PROPERTIES, false); + menu->Enable(IDM_GAMEWIKI, false); + menu->Enable(IDM_OPENCONTAININGFOLDER, false); + menu->Enable(IDM_SETDEFAULTGCM, false); + menu->Enable(IDM_DELETEGCM, false); + menu->Enable(IDM_LIST_INSTALLWAD, false); } } - else if (GetSelectedItemCount() > 1) + else { - wxMenu* popupMenu = new wxMenu; - popupMenu->Append(IDM_DELETEGCM, _("&Delete selected ISOs...")); - popupMenu->AppendSeparator(); - popupMenu->Append(IDM_MULTICOMPRESSGCM, _("Compress selected ISOs...")); - popupMenu->Append(IDM_MULTIDECOMPRESSGCM, _("Decompress selected ISOs...")); - PopupMenu(popupMenu); + menu->Append(IDM_DELETEGCM, _("&Delete selected ISOs...")); + menu->AppendSeparator(); + menu->Append(IDM_MULTICOMPRESSGCM, _("Compress selected ISOs...")); + menu->Append(IDM_MULTIDECOMPRESSGCM, _("Decompress selected ISOs...")); } } @@ -985,6 +1034,7 @@ void CGameListCtrl::OnSetDefaultGCM(wxCommandEvent& event) SConfig::GetInstance().m_LocalCoreStartupParameter.m_strDefaultGCM = ""; SConfig::GetInstance().SaveSettings(); } + UpdateFileMenu(); } void CGameListCtrl::OnDeleteGCM(wxCommandEvent& WXUNUSED (event)) diff --git a/Source/Core/DolphinWX/Src/GameListCtrl.h b/Source/Core/DolphinWX/Src/GameListCtrl.h index ad46ec6d670d..1c28f09c535e 100644 --- a/Source/Core/DolphinWX/Src/GameListCtrl.h +++ b/Source/Core/DolphinWX/Src/GameListCtrl.h @@ -38,6 +38,8 @@ class CGameListCtrl : public wxListCtrl const GameListItem *GetSelectedISO(); const GameListItem *GetISO(size_t index) const; + void SetFileMenu(wxMenu* fileMenu); + enum { COLUMN_DUMMY = 0, @@ -71,6 +73,7 @@ class CGameListCtrl : public wxListCtrl int last_sort; wxSize lastpos; wxEmuStateTip *toolTip; + wxMenu* m_FileMenu; void InitBitmaps(); void InsertItemInReportView(long _Index); void SetBackgroundColor(); @@ -85,6 +88,7 @@ class CGameListCtrl : public wxListCtrl void OnColumnClick(wxListEvent& event); void OnColBeginDrag(wxListEvent& event); void OnKeyPress(wxListEvent& event); + void OnItemSelectedDeselected(wxListEvent& event); void OnSize(wxSizeEvent& event); void OnProperties(wxCommandEvent& event); void OnWiki(wxCommandEvent& event); @@ -99,9 +103,11 @@ class CGameListCtrl : public wxListCtrl void OnInstallWAD(wxCommandEvent& event); void OnDropFiles(wxDropFilesEvent& event); + void AppendContextMenuOptions(wxMenu* menu); void CompressSelection(bool _compress); void AutomaticColumnWidth(); void UnselectAll(); + void UpdateFileMenu(); static size_t m_currentItem; static std::string m_currentFilename; From 790b198c071e389af6070734bdbf466005d679e2 Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 3 Oct 2013 00:48:25 -0400 Subject: [PATCH 006/202] Replace Tools->Start NetPlay with two separate options: - Tools->Connect to Netplay opens a simple dialog asking for IP:port. These are intentionally one field, for easier copy and paste. (Also, on OS X, the dialog now displays a proper error sheet if the connection failed.) - Right clicking a game, or going to the File menu, and choosing Host Netplay Game opens the netplay window. This way you don't have to use a separate, ugly list to select a game, and it's less clicking. Also stop sending localized strings as unique identifiers over the wire, that's just silly. What's missing: - A way to set your nickname, coming right up. - A way to set the hosting port. UDP hole punching is going to give you a random external port anyway, and mostly obviates the need to have a predictable one. --- Source/Core/Core/Src/ConfigManager.cpp | 4 + Source/Core/Core/Src/CoreParameter.h | 1 + Source/Core/Core/Src/NetPlayClient.cpp | 47 +- Source/Core/Core/Src/NetPlayClient.h | 7 +- Source/Core/Core/Src/NetPlayServer.cpp | 6 +- Source/Core/Core/Src/NetPlayServer.h | 2 +- Source/Core/DolphinWX/Src/Frame.cpp | 2 +- Source/Core/DolphinWX/Src/Frame.h | 2 - Source/Core/DolphinWX/Src/FrameTools.cpp | 14 +- Source/Core/DolphinWX/Src/GameListCtrl.cpp | 61 ++- Source/Core/DolphinWX/Src/GameListCtrl.h | 5 +- Source/Core/DolphinWX/Src/Globals.h | 1 + Source/Core/DolphinWX/Src/ISOFile.cpp | 26 ++ Source/Core/DolphinWX/Src/ISOFile.h | 2 + Source/Core/DolphinWX/Src/NetWindow.cpp | 494 ++++++++------------- Source/Core/DolphinWX/Src/NetWindow.h | 52 +-- 16 files changed, 293 insertions(+), 433 deletions(-) diff --git a/Source/Core/Core/Src/ConfigManager.cpp b/Source/Core/Core/Src/ConfigManager.cpp index fd328e12e23b..4e69f8cee785 100644 --- a/Source/Core/Core/Src/ConfigManager.cpp +++ b/Source/Core/Core/Src/ConfigManager.cpp @@ -190,6 +190,8 @@ void SConfig::SaveSettings() ini.Set("Interface", "ShowLogConfigWindow", m_InterfaceLogConfigWindow); ini.Set("Interface", "ShowConsole", m_InterfaceConsole); ini.Set("Interface", "ThemeName40", m_LocalCoreStartupParameter.theme_name); + ini.Set("NetPlay", "LastHost", m_LocalCoreStartupParameter.strNetplayHost); + ini.Set("NetPlay", "Nickname", m_LocalCoreStartupParameter.strNetplayNickname); // Hotkeys for (int i = 0; i < NUM_HOTKEYS; i++) @@ -343,6 +345,8 @@ void SConfig::LoadSettings() ini.Get("Interface", "ShowLogConfigWindow", &m_InterfaceLogConfigWindow, false); ini.Get("Interface", "ShowConsole", &m_InterfaceConsole, false); ini.Get("Interface", "ThemeName40", &m_LocalCoreStartupParameter.theme_name, "Clean"); + ini.Get("NetPlay", "LastHost", &m_LocalCoreStartupParameter.strNetplayHost, "8.8.8.8:1234"); + ini.Get("NetPlay", "Nickname", &m_LocalCoreStartupParameter.strNetplayNickname, ""); // Hotkeys for (int i = 0; i < NUM_HOTKEYS; i++) diff --git a/Source/Core/Core/Src/CoreParameter.h b/Source/Core/Core/Src/CoreParameter.h index caef1d4bbb3e..bb43a6d45af2 100644 --- a/Source/Core/Core/Src/CoreParameter.h +++ b/Source/Core/Core/Src/CoreParameter.h @@ -149,6 +149,7 @@ struct SCoreStartupParameter // Interface settings bool bConfirmStop, bHideCursor, bAutoHideCursor, bUsePanicHandlers, bOnScreenDisplayMessages; std::string theme_name; + std::string strNetplayHost, strNetplayNickname; // Hotkeys int iHotkey[NUM_HOTKEYS]; diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 7e71584c35b0..8048487f7efa 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -52,20 +52,22 @@ NetPlayClient::~NetPlayClient() if (m_is_running) StopGame(); - if (is_connected) + if (m_IsConnected) { m_do_loop = false; - m_thread.join(); + if (m_dialog) + m_thread.join(); } } // called from ---GUI--- thread -NetPlayClient::NetPlayClient(const std::string& address, const u16 port, NetPlayUI* dialog, const std::string& name) : m_dialog(dialog), m_is_running(false), m_do_loop(true) +NetPlayClient::NetPlayClient(const std::string& address, const u16 port, const std::string& name) : m_is_running(false), m_do_loop(true) { m_target_buffer_size = 20; ClearBuffers(); - is_connected = false; + m_IsConnected = false; + m_dialog = NULL; // why is false successful? documentation says true is if (0 == m_socket.Connect(port, address, 5)) @@ -80,27 +82,11 @@ NetPlayClient::NetPlayClient(const std::string& address, const u16 port, NetPlay sf::Packet rpac; // TODO: make this not hang m_socket.Receive(rpac); - MessageId error; - rpac >> error; + rpac >> m_ServerError; // got error message - if (error) + if (m_ServerError) { - switch (error) - { - case CON_ERR_SERVER_FULL : - PanicAlertT("The server is full!"); - break; - case CON_ERR_VERSION_MISMATCH : - PanicAlertT("The server and client's NetPlay versions are incompatible!"); - break; - case CON_ERR_GAME_RUNNING : - PanicAlertT("The server responded: the game is currently running!"); - break; - default : - PanicAlertT("The server sent an unknown error message!"); - break; - } m_socket.Close(); } else @@ -116,18 +102,23 @@ NetPlayClient::NetPlayClient(const std::string& address, const u16 port, NetPlay m_players[m_pid] = player; m_local_player = &m_players[m_pid]; - m_dialog->Update(); - //PanicAlertT("Connection successful: assigned player id: %d", m_pid); - is_connected = true; + m_IsConnected = true; m_selector.Add(m_socket); - m_thread = std::thread(std::mem_fun(&NetPlayClient::ThreadFunc), this); } } - else - PanicAlertT("Failed to Connect!"); +} +void NetPlayClient::SetDialog(NetPlayUI* dialog) +{ + bool hadDialog = m_dialog; + m_dialog = dialog; + if (!hadDialog) + { + // don't start receive messages until we have a dialog + m_thread = std::thread(std::mem_fun(&NetPlayClient::ThreadFunc), this); + } } // called from ---NETPLAY--- thread diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 18509cf8c04d..310c73e57786 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -63,13 +63,14 @@ class NetPlayClient public: void ThreadFunc(); - NetPlayClient(const std::string& address, const u16 port, NetPlayUI* dialog, const std::string& name); + NetPlayClient(const std::string& address, const u16 port, const std::string& name); ~NetPlayClient(); void GetPlayerList(std::string& list, std::vector& pid_list); void GetPlayers(std::vector& player_list); - bool is_connected; + bool m_IsConnected; + MessageId m_ServerError; bool StartGame(const std::string &path); bool StopGame(); @@ -86,6 +87,8 @@ class NetPlayClient u8 LocalWiimoteToInGameWiimote(u8 local_pad); + void SetDialog(NetPlayUI* dialog); + protected: void ClearBuffers(); diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index f9b4fab4807d..d1162e32d9c5 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -6,7 +6,7 @@ NetPlayServer::~NetPlayServer() { - if (is_connected) + if (m_IsConnected) { m_do_loop = false; m_thread.join(); @@ -15,13 +15,13 @@ NetPlayServer::~NetPlayServer() } // called from ---GUI--- thread -NetPlayServer::NetPlayServer(const u16 port) : is_connected(false), m_is_running(false) +NetPlayServer::NetPlayServer(const u16 port) : m_IsConnected(false), m_is_running(false) { memset(m_pad_map, -1, sizeof(m_pad_map)); memset(m_wiimote_map, -1, sizeof(m_wiimote_map)); if (m_socket.Listen(port)) { - is_connected = true; + m_IsConnected = true; m_do_loop = true; m_selector.Add(m_socket); m_thread = std::thread(std::mem_fun(&NetPlayServer::ThreadFunc), this); diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 1f0d7a875a14..d362ced30ff5 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -42,7 +42,7 @@ class NetPlayServer void AdjustPadBufferSize(unsigned int size); - bool is_connected; + bool m_IsConnected; private: class Client diff --git a/Source/Core/DolphinWX/Src/Frame.cpp b/Source/Core/DolphinWX/Src/Frame.cpp index 3d6f1055f794..885b35c80b13 100644 --- a/Source/Core/DolphinWX/Src/Frame.cpp +++ b/Source/Core/DolphinWX/Src/Frame.cpp @@ -269,7 +269,7 @@ CFrame::CFrame(wxFrame* parent, bool ShowLogWindow, long style) : CRenderFrame(parent, id, title, pos, size, style) - , g_pCodeWindow(NULL), g_NetPlaySetupDiag(NULL), g_CheatsWindow(NULL) + , g_pCodeWindow(NULL), g_CheatsWindow(NULL) , m_ToolBar(NULL), m_ToolBarDebug(NULL), m_ToolBarAui(NULL) , m_GameListCtrl(NULL), m_Panel(NULL) , m_RenderFrame(NULL), m_RenderParent(NULL) diff --git a/Source/Core/DolphinWX/Src/Frame.h b/Source/Core/DolphinWX/Src/Frame.h index c36b19661a16..714bb8ace2b9 100644 --- a/Source/Core/DolphinWX/Src/Frame.h +++ b/Source/Core/DolphinWX/Src/Frame.h @@ -42,7 +42,6 @@ class CGameListCtrl; class GameListItem; class CLogWindow; class FifoPlayerDlg; -class NetPlaySetupDiag; class wxCheatsWindow; // The CPanel class to receive MSWWindowProc messages from the video backend. @@ -110,7 +109,6 @@ class CFrame : public CRenderFrame // These have to be public CCodeWindow* g_pCodeWindow; - NetPlaySetupDiag* g_NetPlaySetupDiag; wxCheatsWindow* g_CheatsWindow; TASInputDlg* g_TASInputDlg[4]; diff --git a/Source/Core/DolphinWX/Src/FrameTools.cpp b/Source/Core/DolphinWX/Src/FrameTools.cpp index 0ef5dde720ca..66c76e4c21a9 100644 --- a/Source/Core/DolphinWX/Src/FrameTools.cpp +++ b/Source/Core/DolphinWX/Src/FrameTools.cpp @@ -196,7 +196,7 @@ void CFrame::CreateMenu() toolsMenu->Append(IDM_EXPORTALLSAVE, _("Export All Wii Saves")); toolsMenu->Append(IDM_CHEATS, _("&Cheats Manager")); - toolsMenu->Append(IDM_NETPLAY, _("Start &NetPlay")); + toolsMenu->Append(IDM_NETPLAY, _("Connect to &Netplay")); toolsMenu->Append(IDM_MENU_INSTALLWAD, _("Install WAD")); UpdateWiiMenuChoice(toolsMenu->Append(IDM_LOAD_WII_MENU, wxT("Dummy string to keep wxw happy"))); @@ -1306,17 +1306,7 @@ void CFrame::StatusBarMessage(const char * Text, ...) // NetPlay stuff void CFrame::OnNetPlay(wxCommandEvent& WXUNUSED (event)) { - if (!g_NetPlaySetupDiag) - { - if (NetPlayDiag::GetInstance() != NULL) - NetPlayDiag::GetInstance()->Raise(); - else - g_NetPlaySetupDiag = new NetPlaySetupDiag(this, m_GameListCtrl); - } - else - { - g_NetPlaySetupDiag->Raise(); - } + NetPlay::ShowConnectDialog(this); } void CFrame::OnMemcard(wxCommandEvent& WXUNUSED (event)) diff --git a/Source/Core/DolphinWX/Src/GameListCtrl.cpp b/Source/Core/DolphinWX/Src/GameListCtrl.cpp index c0a55ae77973..eeafe700d2fe 100644 --- a/Source/Core/DolphinWX/Src/GameListCtrl.cpp +++ b/Source/Core/DolphinWX/Src/GameListCtrl.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include "Globals.h" +#include "NetWindow.h" #include #include @@ -44,6 +45,7 @@ size_t CGameListCtrl::m_currentItem = 0; size_t CGameListCtrl::m_numberItem = 0; std::string CGameListCtrl::m_currentFilename; +CGameListCtrl* CGameListCtrl::s_Instance; bool sorted = false; extern CFrame* main_frame; @@ -59,30 +61,8 @@ static int CompareGameListItems(const GameListItem* iso1, const GameListItem* is sortData = -sortData; } - int indexOne = 0; - int indexOther = 0; - - - // index only matters for WADS and PAL GC games, but invalid indicies for the others - // will return the (only) language in the list - if (iso1->GetPlatform() == GameListItem::WII_WAD) - { - indexOne = SConfig::GetInstance().m_SYSCONF->GetData("IPL.LNG"); - } - else - { // GC - indexOne = SConfig::GetInstance().m_LocalCoreStartupParameter.SelectedLanguage; - } - - if (iso2->GetPlatform() == GameListItem::WII_WAD) - { - indexOther = SConfig::GetInstance().m_SYSCONF->GetData("IPL.LNG"); - } - else - { // GC - indexOther = SConfig::GetInstance().m_LocalCoreStartupParameter.SelectedLanguage; - } - + int indexOne = iso1->GetLang(); + int indexOther = iso2->GetLang(); switch(sortData) { case CGameListCtrl::COLUMN_TITLE: @@ -164,6 +144,7 @@ BEGIN_EVENT_TABLE(CGameListCtrl, wxListCtrl) EVT_LIST_ITEM_DESELECTED(LIST_CTRL, CGameListCtrl::OnItemSelectedDeselected) EVT_MENU(IDM_PROPERTIES, CGameListCtrl::OnProperties) EVT_MENU(IDM_GAMEWIKI, CGameListCtrl::OnWiki) + EVT_MENU(IDM_HOSTNETPLAY, CGameListCtrl::OnHostNetplay) EVT_MENU(IDM_OPENCONTAININGFOLDER, CGameListCtrl::OnOpenContainingFolder) EVT_MENU(IDM_OPENSAVEFOLDER, CGameListCtrl::OnOpenSaveFolder) EVT_MENU(IDM_EXPORTSAVE, CGameListCtrl::OnExportSave) @@ -180,6 +161,7 @@ CGameListCtrl::CGameListCtrl(wxWindow* parent, const wxWindowID id, const { DragAcceptFiles(true); Connect(wxEVT_DROP_FILES, wxDropFilesEventHandler(CGameListCtrl::OnDropFiles), NULL, this); + s_Instance = this; } CGameListCtrl::~CGameListCtrl() @@ -188,6 +170,7 @@ CGameListCtrl::~CGameListCtrl() delete m_imageListSmall; ClearIsoFiles(); + s_Instance = NULL; } void CGameListCtrl::InitBitmaps() @@ -412,11 +395,7 @@ void CGameListCtrl::InsertItemInReportView(long _Index) int SelectedLanguage = SConfig::GetInstance().m_LocalCoreStartupParameter.SelectedLanguage; - // Is this sane? - if (rISOFile.GetPlatform() == GameListItem::WII_WAD) - { - SelectedLanguage = SConfig::GetInstance().m_SYSCONF->GetData("IPL.LNG"); - } + SelectedLanguage = rISOFile.GetLang(); std::string const name = rISOFile.GetName(SelectedLanguage); SetItem(_Index, COLUMN_TITLE, StrToWxStr(name), -1); @@ -607,11 +586,7 @@ void CGameListCtrl::ScanForISOs() for (std::vector::const_iterator iter = drives.begin(); iter != drives.end(); ++iter) { - #ifdef __APPLE__ - std::auto_ptr gli(new GameListItem(*iter)); - #else std::unique_ptr gli(new GameListItem(*iter)); - #endif if (gli->IsValid()) m_ISOFiles.push_back(gli.release()); @@ -627,10 +602,10 @@ void CGameListCtrl::OnColBeginDrag(wxListEvent& event) event.Veto(); } -const GameListItem *CGameListCtrl::GetISO(size_t index) const +const GameListItem *CGameListCtrl::GetISO(size_t index) { - if (index < m_ISOFiles.size()) - return m_ISOFiles[index]; + if (s_Instance && index < s_Instance->m_ISOFiles.size()) + return s_Instance->m_ISOFiles[index]; else return NULL; } @@ -894,8 +869,13 @@ void CGameListCtrl::AppendContextMenuOptions(wxMenu* menu) menu->Append(IDM_PROPERTIES, _("&Properties")); menu->Append(IDM_GAMEWIKI, _("&Wiki")); + if (NetPlayDiag::GetInstance() != NULL) + menu->Append(IDM_HOSTNETPLAY, _("Change &Netplay Game")); + else + menu->Append(IDM_HOSTNETPLAY, _("Host &Netplay Game")); menu->AppendSeparator(); + if (selected_iso && selected_iso->GetPlatform() != GameListItem::GAMECUBE_DISC) { menu->Append(IDM_OPENSAVEFOLDER, _("Open Wii &save folder")); @@ -1095,6 +1075,15 @@ void CGameListCtrl::OnWiki(wxCommandEvent& WXUNUSED (event)) WxUtils::Launch(wikiUrl.c_str()); } +void CGameListCtrl::OnHostNetplay(wxCommandEvent& WXUNUSED (event)) +{ + const GameListItem *iso = GetSelectedISO(); + if (iso) + { + NetPlay::StartHosting(iso->GetRevisionSpecificUniqueID(), this); + } +} + void CGameListCtrl::MultiCompressCB(const char* text, float percent, void* arg) { percent = (((float)m_currentItem) + percent) / (float)m_numberItem; diff --git a/Source/Core/DolphinWX/Src/GameListCtrl.h b/Source/Core/DolphinWX/Src/GameListCtrl.h index 1c28f09c535e..07dd57089578 100644 --- a/Source/Core/DolphinWX/Src/GameListCtrl.h +++ b/Source/Core/DolphinWX/Src/GameListCtrl.h @@ -36,7 +36,8 @@ class CGameListCtrl : public wxListCtrl void BrowseForDirectory(); const GameListItem *GetSelectedISO(); - const GameListItem *GetISO(size_t index) const; + // todo: put this functionality somewhere saner for god's sake + static const GameListItem *GetISO(size_t index); void SetFileMenu(wxMenu* fileMenu); @@ -59,6 +60,7 @@ class CGameListCtrl : public wxListCtrl std::vector m_PlatformImageIndex; std::vector m_EmuStateImageIndex; std::vector m_ISOFiles; + static CGameListCtrl* s_Instance; void ClearIsoFiles() { @@ -92,6 +94,7 @@ class CGameListCtrl : public wxListCtrl void OnSize(wxSizeEvent& event); void OnProperties(wxCommandEvent& event); void OnWiki(wxCommandEvent& event); + void OnHostNetplay(wxCommandEvent& event); void OnOpenContainingFolder(wxCommandEvent& event); void OnOpenSaveFolder(wxCommandEvent& event); void OnExportSave(wxCommandEvent& event); diff --git a/Source/Core/DolphinWX/Src/Globals.h b/Source/Core/DolphinWX/Src/Globals.h index 42c0154405de..f1b50d2022eb 100644 --- a/Source/Core/DolphinWX/Src/Globals.h +++ b/Source/Core/DolphinWX/Src/Globals.h @@ -93,6 +93,7 @@ enum IDM_CHANGEDISC, IDM_PROPERTIES, IDM_GAMEWIKI, + IDM_HOSTNETPLAY, IDM_LOAD_WII_MENU, IDM_MENU_INSTALLWAD, IDM_LIST_INSTALLWAD, diff --git a/Source/Core/DolphinWX/Src/ISOFile.cpp b/Source/Core/DolphinWX/Src/ISOFile.cpp index 2a70a550f72c..33fd76cec5a3 100644 --- a/Source/Core/DolphinWX/Src/ISOFile.cpp +++ b/Source/Core/DolphinWX/Src/ISOFile.cpp @@ -145,6 +145,20 @@ GameListItem::~GameListItem() { } +int GameListItem::GetLang() const +{ + // index only matters for WADS and PAL GC games, but invalid indicies for the others + // will return the (only) language in the list + if (GetPlatform() == GameListItem::WII_WAD) + { + return SConfig::GetInstance().m_SYSCONF->GetData("IPL.LNG"); + } + else + { // GC + return SConfig::GetInstance().m_LocalCoreStartupParameter.SelectedLanguage; + } +} + bool GameListItem::LoadFromCache() { return CChunkFileReader::Load(CreateCacheFilename(), CACHE_REVISION, *this); @@ -266,6 +280,18 @@ std::string GameListItem::GetName(int _index) const return name; } +std::string GameListItem::GetRevisionSpecificUniqueID() const +{ + std::string id = m_UniqueID; + if (m_Platform == GAMECUBE_DISC) + { + char rev[16]; + sprintf(rev, "r%d", m_Revision); + id += rev; + } + return id; +} + const std::string GameListItem::GetWiiFSPath() const { DiscIO::IVolume *Iso = DiscIO::CreateVolumeFromFilename(m_FileName); diff --git a/Source/Core/DolphinWX/Src/ISOFile.h b/Source/Core/DolphinWX/Src/ISOFile.h index ffab089a1dd8..286a9def0d7f 100644 --- a/Source/Core/DolphinWX/Src/ISOFile.h +++ b/Source/Core/DolphinWX/Src/ISOFile.h @@ -24,6 +24,7 @@ class GameListItem : NonCopyable bool IsValid() const {return m_Valid;} const std::string& GetFileName() const {return m_FileName;} + int GetLang() const; std::string GetBannerName(int index) const; std::string GetVolumeName(int index) const; std::string GetName(int index) const; @@ -31,6 +32,7 @@ class GameListItem : NonCopyable std::string GetDescription(int index = 0) const; int GetRevision() const { return m_Revision; } const std::string& GetUniqueID() const {return m_UniqueID;} + std::string GetRevisionSpecificUniqueID() const; const std::string GetWiiFSPath() const; DiscIO::IVolume::ECountry GetCountry() const {return m_Country;} int GetPlatform() const {return m_Platform;} diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index fbe92ef538a5..056994f0c54c 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -23,275 +23,23 @@ BEGIN_EVENT_TABLE(NetPlayDiag, wxFrame) EVT_COMMAND(wxID_ANY, wxEVT_THREAD, NetPlayDiag::OnThread) END_EVENT_TABLE() -static NetPlayServer* netplay_server = NULL; -static NetPlayClient* netplay_client = NULL; +static std::unique_ptr netplay_server; +static std::unique_ptr netplay_client; extern CFrame* main_frame; NetPlayDiag *NetPlayDiag::npd = NULL; -std::string BuildGameName(const GameListItem& game) -{ - // Lang needs to be consistent - auto const lang = 0; - - std::string name(game.GetBannerName(lang)); - if (name.empty()) - name = game.GetVolumeName(lang); - - if (game.GetRevision() != 0) - return name + " (" + game.GetUniqueID() + ", Revision " + std::to_string((long long)game.GetRevision()) + ")"; - else - return name + " (" + game.GetUniqueID() + ")"; -} - -void FillWithGameNames(wxListBox* game_lbox, const CGameListCtrl& game_list) -{ - for (u32 i = 0 ; auto game = game_list.GetISO(i); ++i) - game_lbox->Append(StrToWxStr(BuildGameName(*game))); -} - -NetPlaySetupDiag::NetPlaySetupDiag(wxWindow* const parent, const CGameListCtrl* const game_list) - : wxFrame(parent, wxID_ANY, wxT(NETPLAY_TITLEBAR), wxDefaultPosition, wxDefaultSize) - , m_game_list(game_list) -{ - IniFile inifile; - inifile.Load(File::GetUserPath(D_CONFIG_IDX) + "Dolphin.ini"); - IniFile::Section& netplay_section = *inifile.GetOrCreateSection("NetPlay"); - - wxPanel* const panel = new wxPanel(this); - - // top row - wxStaticText* const nick_lbl = new wxStaticText(panel, wxID_ANY, _("Nickname :"), - wxDefaultPosition, wxDefaultSize); - - std::string nickname; - netplay_section.Get("Nickname", &nickname, "Player"); - m_nickname_text = new wxTextCtrl(panel, wxID_ANY, StrToWxStr(nickname)); - - wxBoxSizer* const nick_szr = new wxBoxSizer(wxHORIZONTAL); - nick_szr->Add(nick_lbl, 0, wxCENTER); - nick_szr->Add(m_nickname_text, 0, wxALL, 5); - - - // tabs - wxNotebook* const notebook = new wxNotebook(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize); - wxPanel* const connect_tab = new wxPanel(notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize); - notebook->AddPage(connect_tab, _("Connect")); - wxPanel* const host_tab = new wxPanel(notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize); - notebook->AddPage(host_tab, _("Host")); - - - // connect tab - { - wxStaticText* const ip_lbl = new wxStaticText(connect_tab, wxID_ANY, _("Address :"), - wxDefaultPosition, wxDefaultSize); - - std::string address; - netplay_section.Get("Address", &address, "localhost"); - m_connect_ip_text = new wxTextCtrl(connect_tab, wxID_ANY, StrToWxStr(address)); - - wxStaticText* const port_lbl = new wxStaticText(connect_tab, wxID_ANY, _("Port :"), - wxDefaultPosition, wxDefaultSize); - - // string? w/e - std::string port; - netplay_section.Get("ConnectPort", &port, "2626"); - m_connect_port_text = new wxTextCtrl(connect_tab, wxID_ANY, StrToWxStr(port)); - - wxButton* const connect_btn = new wxButton(connect_tab, wxID_ANY, _("Connect")); - connect_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &NetPlaySetupDiag::OnJoin, this); - - wxStaticText* const alert_lbl = new wxStaticText(connect_tab, wxID_ANY, - _("ALERT:\n\n" - "Netplay will only work with the following settings:\n" - " - Enable Dual Core [OFF]\n" - " - DSP Emulator Engine Must be the same on all computers!\n" - " - DSP on Dedicated Thread [OFF]\n" - " - Framelimit NOT set to [Audio]\n" - " - Manually set the extensions for each wiimote\n" - "\n" - "All players should use the same Dolphin version and settings.\n" - "All memory cards must be identical between players or disabled.\n" - "Wiimote support is probably terrible. Don't use it.\n" - "\n" - "The host must have the chosen TCP port open/forwarded!\n"), - wxDefaultPosition, wxDefaultSize); - - wxBoxSizer* const top_szr = new wxBoxSizer(wxHORIZONTAL); - top_szr->Add(ip_lbl, 0, wxCENTER | wxRIGHT, 5); - top_szr->Add(m_connect_ip_text, 3); - top_szr->Add(port_lbl, 0, wxCENTER | wxRIGHT | wxLEFT, 5); - top_szr->Add(m_connect_port_text, 1); - - wxBoxSizer* const con_szr = new wxBoxSizer(wxVERTICAL); - con_szr->Add(top_szr, 0, wxALL | wxEXPAND, 5); - con_szr->AddStretchSpacer(1); - con_szr->Add(alert_lbl, 0, wxLEFT | wxRIGHT | wxEXPAND, 5); - con_szr->AddStretchSpacer(1); - con_szr->Add(connect_btn, 0, wxALL | wxALIGN_RIGHT, 5); - - connect_tab->SetSizerAndFit(con_szr); - } - - // host tab - { - wxStaticText* const port_lbl = new wxStaticText(host_tab, wxID_ANY, _("Port :"), - wxDefaultPosition, wxDefaultSize); - - // string? w/e - std::string port; - netplay_section.Get("HostPort", &port, "2626"); - m_host_port_text = new wxTextCtrl(host_tab, wxID_ANY, StrToWxStr(port)); - - wxButton* const host_btn = new wxButton(host_tab, wxID_ANY, _("Host")); - host_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &NetPlaySetupDiag::OnHost, this); - - m_game_lbox = new wxListBox(host_tab, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxLB_SORT); - m_game_lbox->Bind(wxEVT_COMMAND_LISTBOX_DOUBLECLICKED, &NetPlaySetupDiag::OnHost, this); - - FillWithGameNames(m_game_lbox, *game_list); - - wxBoxSizer* const top_szr = new wxBoxSizer(wxHORIZONTAL); - top_szr->Add(port_lbl, 0, wxCENTER | wxRIGHT, 5); - top_szr->Add(m_host_port_text, 0); - - wxBoxSizer* const host_szr = new wxBoxSizer(wxVERTICAL); - host_szr->Add(top_szr, 0, wxALL | wxEXPAND, 5); - host_szr->Add(m_game_lbox, 1, wxLEFT | wxRIGHT | wxEXPAND, 5); - host_szr->Add(host_btn, 0, wxALL | wxALIGN_RIGHT, 5); - - host_tab->SetSizerAndFit(host_szr); - } - - // bottom row - wxButton* const quit_btn = new wxButton(panel, wxID_ANY, _("Quit")); - quit_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &NetPlaySetupDiag::OnQuit, this); - - // main sizer - wxBoxSizer* const main_szr = new wxBoxSizer(wxVERTICAL); - main_szr->Add(nick_szr, 0, wxALL | wxALIGN_RIGHT, 5); - main_szr->Add(notebook, 1, wxLEFT | wxRIGHT | wxEXPAND, 5); - main_szr->Add(quit_btn, 0, wxALL | wxALIGN_RIGHT, 5); - - panel->SetSizerAndFit(main_szr); - - //wxBoxSizer* const diag_szr = new wxBoxSizer(wxVERTICAL); - //diag_szr->Add(panel, 1, wxEXPAND); - //SetSizerAndFit(diag_szr); - - main_szr->SetSizeHints(this); - - Center(); - Show(); -} - -NetPlaySetupDiag::~NetPlaySetupDiag() -{ - IniFile inifile; - const std::string dolphin_ini = File::GetUserPath(D_CONFIG_IDX) + "Dolphin.ini"; - inifile.Load(dolphin_ini); - IniFile::Section& netplay_section = *inifile.GetOrCreateSection("NetPlay"); - - netplay_section.Set("Nickname", WxStrToStr(m_nickname_text->GetValue())); - netplay_section.Set("Address", WxStrToStr(m_connect_ip_text->GetValue())); - netplay_section.Set("ConnectPort", WxStrToStr(m_connect_port_text->GetValue())); - netplay_section.Set("HostPort", WxStrToStr(m_host_port_text->GetValue())); - - inifile.Save(dolphin_ini); - main_frame->g_NetPlaySetupDiag = NULL; -} - -void NetPlaySetupDiag::MakeNetPlayDiag(int port, const std::string &game, bool is_hosting) -{ - NetPlayDiag *&npd = NetPlayDiag::GetInstance(); - std::string ip; - npd = new NetPlayDiag(m_parent, m_game_list, game, is_hosting); - if (is_hosting) - ip = "127.0.0.1"; - else - ip = WxStrToStr(m_connect_ip_text->GetValue()); - - netplay_client = new NetPlayClient(ip, (u16)port, npd, WxStrToStr(m_nickname_text->GetValue())); - if (netplay_client->is_connected) - { - npd->Show(); - Destroy(); - } - else - { - npd->Destroy(); - } -} - -void NetPlaySetupDiag::OnHost(wxCommandEvent&) -{ - NetPlayDiag *&npd = NetPlayDiag::GetInstance(); - if (npd) - { - PanicAlertT("A NetPlay window is already open!!"); - return; - } - - if (-1 == m_game_lbox->GetSelection()) - { - PanicAlertT("You must choose a game!!"); - return; - } - - std::string game(WxStrToStr(m_game_lbox->GetStringSelection())); - - unsigned long port = 0; - m_host_port_text->GetValue().ToULong(&port); - netplay_server = new NetPlayServer(u16(port)); - netplay_server->ChangeGame(game); - netplay_server->AdjustPadBufferSize(INITIAL_PAD_BUFFER_SIZE); - if (netplay_server->is_connected) - { - MakeNetPlayDiag(port, game, true); - } - else - { - PanicAlertT("Failed to listen. Is another instance of the NetPlay server running?"); - } -} - -void NetPlaySetupDiag::OnJoin(wxCommandEvent&) -{ - NetPlayDiag *&npd = NetPlayDiag::GetInstance(); - if (npd) - { - PanicAlertT("A NetPlay window is already open!!"); - return; - } - - unsigned long port = 0; - m_connect_port_text->GetValue().ToULong(&port); - MakeNetPlayDiag(port, "", false); -} - -void NetPlaySetupDiag::OnQuit(wxCommandEvent&) -{ - Destroy(); -} - -NetPlayDiag::NetPlayDiag(wxWindow* const parent, const CGameListCtrl* const game_list, - const std::string& game, const bool is_hosting) - : wxFrame(parent, wxID_ANY, wxT(NETPLAY_TITLEBAR), wxDefaultPosition, wxDefaultSize) +NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const bool is_hosting) + : wxFrame(parent, wxID_ANY, wxT(NETPLAY_TITLEBAR), wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_STYLE | wxTAB_TRAVERSAL) , m_selected_game(game) , m_start_btn(NULL) - , m_game_list(game_list) { + npd = this; wxPanel* const panel = new wxPanel(this); // top crap - m_game_btn = new wxButton(panel, wxID_ANY, - StrToWxStr(m_selected_game).Prepend(_(" Game : ")), - wxDefaultPosition, wxDefaultSize, wxBU_LEFT); + m_game_label = new wxStaticText(panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxBU_LEFT); + UpdateGameName(); - if (is_hosting) - m_game_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &NetPlayDiag::OnChangeGame, this); - else - m_game_btn->Disable(); - // middle crap // chat @@ -358,7 +106,7 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const CGameListCtrl* const game // main sizer wxBoxSizer* const main_szr = new wxBoxSizer(wxVERTICAL); - main_szr->Add(m_game_btn, 0, wxEXPAND | wxALL, 5); + main_szr->Add(m_game_label, 0, wxEXPAND | wxALL, 5); main_szr->Add(mid_szr, 1, wxEXPAND | wxLEFT | wxRIGHT, 5); main_szr->Add(bottom_szr, 0, wxEXPAND | wxALL, 5); @@ -368,20 +116,46 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const CGameListCtrl* const game SetSize(512, 512-128); Center(); + Show(); } -NetPlayDiag::~NetPlayDiag() +const GameListItem* NetPlayDiag::FindISO(const std::string& id) { - if (netplay_client) + for (size_t i = 0; const GameListItem* item = CGameListCtrl::GetISO(i); i++) { - delete netplay_client; - netplay_client = NULL; + if (item->GetRevisionSpecificUniqueID() == id) + return item; } - if (netplay_server) + + return NULL; +} + +void NetPlayDiag::UpdateGameName() +{ + auto item = FindISO(m_selected_game); + wxString name; + if (!item) { - delete netplay_server; - netplay_server = NULL; + name = wxString::Format(_("Unknown (%s)"), m_selected_game); } + else + { + std::string gameName = item->GetName(item->GetLang()); + std::string uniqueId = item->GetUniqueID(); + int rev = item->GetRevision(); + if (rev) + name = wxString::Format(_("%s (%s, Revision %d)"), gameName, uniqueId, rev); + else + name = wxString::Format(_("%s (%s)"), gameName, uniqueId); + } + m_game_label->SetLabel(_(" Game : ") + name); + +} + +NetPlayDiag::~NetPlayDiag() +{ + netplay_client.reset(); + netplay_server.reset(); npd = NULL; } @@ -408,23 +182,12 @@ void NetPlayDiag::GetNetSettings(NetSettings &settings) settings.m_EXIDevice[1] = instance.m_EXIDevice[1]; } -std::string NetPlayDiag::FindGame() -{ - // find path for selected game, sloppy.. - for (u32 i = 0 ; auto game = m_game_list->GetISO(i); ++i) - if (m_selected_game == BuildGameName(*game)) - return game->GetFileName(); - - PanicAlertT("Game not found!"); - return ""; -} - void NetPlayDiag::OnStart(wxCommandEvent&) { NetSettings settings; GetNetSettings(settings); netplay_server->SetNetSettings(settings); - netplay_server->StartGame(FindGame()); + netplay_server->StartGame(FindISO(m_selected_game)->GetFileName()); } void NetPlayDiag::BootGame(const std::string& filename) @@ -514,13 +277,22 @@ void NetPlayDiag::OnThread(wxCommandEvent& event) // update selected game :/ { m_selected_game.assign(WxStrToStr(event.GetString())); - m_game_btn->SetLabel(event.GetString().Prepend(_(" Game : "))); + UpdateGameName(); } break; case NP_GUI_EVT_START_GAME : // client start game :/ { - netplay_client->StartGame(FindGame()); + auto iso = FindISO(m_selected_game); + if (iso) + { + netplay_client->StartGame(iso->GetFileName()); + } + else + { + PanicAlertT("The host chose a game that was not found locally."); + netplay_client.reset(); + } } break; case NP_GUI_EVT_STOP_GAME : @@ -541,20 +313,6 @@ void NetPlayDiag::OnThread(wxCommandEvent& event) } } -void NetPlayDiag::OnChangeGame(wxCommandEvent&) -{ - wxString game_name; - ChangeGameDiag* const cgd = new ChangeGameDiag(this, m_game_list, game_name); - cgd->ShowModal(); - - if (game_name.length()) - { - m_selected_game = WxStrToStr(game_name); - netplay_server->ChangeGame(m_selected_game); - m_game_btn->SetLabel(game_name.Prepend(_(" Game : "))); - } -} - void NetPlayDiag::OnConfigPads(wxCommandEvent&) { PadMapping mapping[4]; @@ -574,31 +332,101 @@ bool NetPlayDiag::IsRecording() return m_record_chkbox->GetValue(); } -ChangeGameDiag::ChangeGameDiag(wxWindow* const parent, const CGameListCtrl* const game_list, wxString& game_name) - : wxDialog(parent, wxID_ANY, _("Change Game"), wxDefaultPosition, wxDefaultSize) - , m_game_name(game_name) +ConnectDiag::ConnectDiag(wxWindow* parent) + : wxDialog(parent, wxID_ANY, _("Connect to Netplay"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { - m_game_lbox = new wxListBox(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxLB_SORT); - m_game_lbox->Bind(wxEVT_COMMAND_LISTBOX_DOUBLECLICKED, &ChangeGameDiag::OnPick, this); + wxBoxSizer* sizerTop = new wxBoxSizer(wxVERTICAL); + std::string host = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayHost; + wxStaticText* hostLabel = new wxStaticText(this, wxID_ANY, _("Host :")); + m_HostCtrl = new wxTextCtrl(this, wxID_ANY, StrToWxStr(host)); + // focus and select all + m_HostCtrl->SetFocus(); + m_HostCtrl->SetSelection(-1, -1); + m_HostCtrl->Bind(wxEVT_COMMAND_TEXT_UPDATED, &ConnectDiag::OnChange, this); + wxBoxSizer* sizerHost = new wxBoxSizer(wxHORIZONTAL); + sizerHost->Add(hostLabel, 0, wxLEFT, 5); + sizerHost->Add(m_HostCtrl, 1, wxEXPAND | wxLEFT | wxRIGHT, 5); + wxStdDialogButtonSizer* sizerButtons = CreateStdDialogButtonSizer(wxOK | wxCANCEL); + m_ConnectBtn = sizerButtons->GetAffirmativeButton(); + m_ConnectBtn->SetLabel(_("Connect")); + m_ConnectBtn->Enable(IsHostOk()); + sizerTop->Add(sizerHost, 0, wxTOP | wxBOTTOM | wxEXPAND, 5); + sizerTop->Add(sizerButtons, 0, wxEXPAND); + SetSizerAndFit(sizerTop); + SetMaxSize(wxSize(10000, GetBestSize().GetHeight())); +} + - FillWithGameNames(m_game_lbox, *game_list); +bool ConnectDiag::Validate() +{ + if (netplay_client) + { + // shouldn't be possible, just in case + return false; + } + std::string hostSpec = GetHost(); + size_t pos = hostSpec.find(':'); + std::string host = hostSpec.substr(0, pos); + int port = std::stoi(hostSpec.substr(pos + 1).c_str()); + std::string nickname = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayNickname; + netplay_client.reset(new NetPlayClient(host, (u16)port, nickname)); + if (!netplay_client->m_IsConnected) + { + wxString err; + switch (netplay_client->m_ServerError) + { + case 0: + // there was no server error. + err = _("Failed to connect."); + break; + case CON_ERR_SERVER_FULL : + err = _("The server is full."); + break; + case CON_ERR_VERSION_MISMATCH : + err = _("The server and client's NetPlay versions are incompatible."); + break; + case CON_ERR_GAME_RUNNING : + err = _("The server responded: the game is currently running."); + break; + default : + err = _("The server sent an unknown error message."); + break; + } + netplay_client.reset(); + // connection failure + auto complain = new wxMessageDialog(this, err); + complain->ShowWindowModal(); + // We leak the message dialog because of a wx bug. + return false; + } + netplay_client->SetDialog(new NetPlayDiag(GetParent(), "", false)); + EndModal(0); + return true; +} - wxButton* const ok_btn = new wxButton(this, wxID_OK, _("Change")); - ok_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &ChangeGameDiag::OnPick, this); +void ConnectDiag::OnChange(wxCommandEvent& event) +{ + m_ConnectBtn->Enable(IsHostOk()); +} - wxBoxSizer* const szr = new wxBoxSizer(wxVERTICAL); - szr->Add(m_game_lbox, 1, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, 5); - szr->Add(ok_btn, 0, wxALL | wxALIGN_RIGHT, 5); +std::string ConnectDiag::GetHost() +{ + return WxStrToStr(m_HostCtrl->GetValue()); +} - SetSizerAndFit(szr); - SetFocus(); +bool ConnectDiag::IsHostOk() +{ + std::string host = GetHost(); + size_t pos = host.find(':'); + return pos != std::string::npos && + pos + 1 < host.size() && + host.find_first_not_of("0123456789", pos + 1) == std::string::npos; } -void ChangeGameDiag::OnPick(wxCommandEvent& event) +ConnectDiag::~ConnectDiag() { - // return the selected game name - m_game_name = m_game_lbox->GetStringSelection(); - EndModal(wxID_OK); + SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayHost = WxStrToStr(m_HostCtrl->GetValue()); + SConfig::GetInstance().SaveSettings(); } PadMapDiag::PadMapDiag(wxWindow* const parent, PadMapping map[], PadMapping wiimotemap[], std::vector& player_list) @@ -695,3 +523,45 @@ void NetPlay::StopGame() if (netplay_client != NULL) netplay_client->Stop(); } + +void NetPlay::ShowConnectDialog(wxWindow* parent) +{ + if (NetPlayDiag::GetInstance() != NULL) + { + NetPlayDiag::GetInstance()->Raise(); + return; + } + ConnectDiag diag(parent); + // it'll open the window itself + diag.ShowModal(); +} + +void NetPlay::StartHosting(std::string id, wxWindow* parent) +{ + if (NetPlayDiag::GetInstance() != NULL) + { + NetPlayDiag::GetInstance()->Raise(); + netplay_server->ChangeGame(id); + return; + } + + netplay_server.reset(new NetPlayServer(2626)); + netplay_server->ChangeGame(id); + netplay_server->AdjustPadBufferSize(INITIAL_PAD_BUFFER_SIZE); + if (!netplay_server->m_IsConnected) + { + wxMessageBox(_("Failed to host. This shouldn't happen..."), _("Error"), wxOK, parent); + netplay_server.reset(); + return; + } + std::string nickname = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayNickname; + netplay_client.reset(new NetPlayClient("127.0.0.1", 2626, nickname)); + if (!netplay_client->m_IsConnected) + { + wxMessageBox(_("Failed to connect to localhost. This shouldn't happen..."), _("Error"), wxOK, parent); + netplay_client.reset(); + netplay_server.reset(); + return; + } + netplay_client->SetDialog(new NetPlayDiag(parent, id, true)); +} diff --git a/Source/Core/DolphinWX/Src/NetWindow.h b/Source/Core/DolphinWX/Src/NetWindow.h index 1e7247a23790..769934d70354 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.h +++ b/Source/Core/DolphinWX/Src/NetWindow.h @@ -32,33 +32,10 @@ enum NP_GUI_EVT_STOP_GAME, }; -class NetPlaySetupDiag : public wxFrame -{ -public: - NetPlaySetupDiag(wxWindow* const parent, const CGameListCtrl* const game_list); - ~NetPlaySetupDiag(); -private: - void OnJoin(wxCommandEvent& event); - void OnHost(wxCommandEvent& event); - void OnQuit(wxCommandEvent& event); - - void MakeNetPlayDiag(int port, const std::string &game, bool is_hosting); - - wxTextCtrl *m_nickname_text, - *m_host_port_text, - *m_connect_port_text, - *m_connect_ip_text; - - wxListBox* m_game_lbox; - - const CGameListCtrl* const m_game_list; -}; - class NetPlayDiag : public wxFrame, public NetPlayUI { public: - NetPlayDiag(wxWindow* const parent, const CGameListCtrl* const game_list - , const std::string& game, const bool is_hosting = false); + NetPlayDiag(wxWindow* const parent, const std::string& game, const bool is_hosting = false); ~NetPlayDiag(); Common::FifoQueue chat_msgs; @@ -80,17 +57,18 @@ class NetPlayDiag : public wxFrame, public NetPlayUI bool IsRecording(); + static const GameListItem* FindISO(const std::string& id); + void UpdateGameName(); + private: DECLARE_EVENT_TABLE() void OnChat(wxCommandEvent& event); void OnQuit(wxCommandEvent& event); void OnThread(wxCommandEvent& event); - void OnChangeGame(wxCommandEvent& event); void OnAdjustBuffer(wxCommandEvent& event); void OnConfigPads(wxCommandEvent& event); void GetNetSettings(NetSettings &settings); - std::string FindGame(); wxListBox* m_player_lbox; wxTextCtrl* m_chat_text; @@ -99,26 +77,28 @@ class NetPlayDiag : public wxFrame, public NetPlayUI wxCheckBox* m_record_chkbox; std::string m_selected_game; - wxButton* m_game_btn; + wxStaticText* m_game_label; wxButton* m_start_btn; std::vector m_playerids; - const CGameListCtrl* const m_game_list; - static NetPlayDiag* npd; }; -class ChangeGameDiag : public wxDialog +class ConnectDiag : public wxDialog { public: - ChangeGameDiag(wxWindow* const parent, const CGameListCtrl* const game_list, wxString& game_name); + ConnectDiag(wxWindow* parent); + ~ConnectDiag(); + std::string GetHost(); + bool Validate(); + void OnThread(wxCommandEvent& event); private: - void OnPick(wxCommandEvent& event); - - wxListBox* m_game_lbox; - wxString& m_game_name; + void OnChange(wxCommandEvent& event); + bool IsHostOk(); + wxTextCtrl* m_HostCtrl; + wxButton* m_ConnectBtn; }; class PadMapDiag : public wxDialog @@ -138,6 +118,8 @@ class PadMapDiag : public wxDialog namespace NetPlay { void StopGame(); + void ShowConnectDialog(wxWindow* parent); + void StartHosting(std::string id, wxWindow* parent); } #endif // _NETWINDOW_H_ From 6c1526e2102ef8980cc0dd7a710c7088c887b2cb Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 3 Oct 2013 01:26:14 -0400 Subject: [PATCH 007/202] In ChunkFile, don't switch on the mode for every byte. The compiler can't optimize it away because of the potential for aliasing, so it ends up pretty gross. --- Source/Core/Common/Src/ChunkFile.h | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index 6fed73a7ed25..c359b6b21dfa 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -255,8 +255,30 @@ class PointerWrap void DoVoid(void *data, u32 size) { - for(u32 i = 0; i != size; ++i) - DoByte(reinterpret_cast(data)[i]); + switch (mode) + { + case MODE_READ: + memcpy(data, *ptr, size); + break; + + case MODE_WRITE: + memcpy(*ptr, data, size); + break; + + case MODE_MEASURE: + break; + + case MODE_VERIFY: + _dbg_assert_msg_(COMMON, !memcmp(data, *ptr, size), + "Savestate verification failure at %p\n", + *ptr); + break; + + default: + break; + } + + *ptr += size; } }; From c3ac6d8d4037d840bac38aba3bc41e1b6b116e6e Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 3 Oct 2013 01:51:36 -0400 Subject: [PATCH 008/202] Ditto for every element of a POD array. --- Source/Core/Common/Src/ChunkFile.h | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index c359b6b21dfa..f87f09343952 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -121,8 +121,15 @@ class PointerWrap template void DoArray(T* x, u32 count) { - for (u32 i = 0; i != count; ++i) - Do(x[i]); + if (std::is_pod::value) + { + DoVoid(x, count * sizeof(T)); + } + else + { + for (u32 i = 0; i != count; ++i) + Do(x[i]); + } } template From a62d6083d3328944947aa997fdd1b2a197c31862 Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 3 Oct 2013 02:22:44 -0400 Subject: [PATCH 009/202] Make ChunkFile use "vectors" for storage rather than looking for a size ahead of time. This is so that it can be used for netplay too. Not that it really saves much to get rid of 400 lines of SFML code, but it feels a little silly to have two different mechanisms, and ChunkFile would be more suitable when more types of data need to be sent over the socket soon(tm). Instead of using an actual vector it uses a PWBuffer, which is like a std::vector but without initialization of elements to 0, avoiding some preexisting slowdown. May as well use that everywhere. --- Source/Core/Common/Src/ChunkFile.h | 194 +++++++++++++++++++--------- Source/Core/Common/Src/Common.h | 4 +- Source/Core/Core/Src/HW/Wiimote.cpp | 5 +- Source/Core/Core/Src/HW/Wiimote.h | 2 +- Source/Core/Core/Src/State.cpp | 99 ++++++-------- 5 files changed, 175 insertions(+), 129 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index f87f09343952..0ae0f4dfb472 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -25,12 +25,100 @@ #include "Common.h" #include "FileUtil.h" +#ifdef __GNUC__ +#define NO_WARN_UNINIT_POINTER(data) asm("" :: "X"(data)); +#else +#define NO_WARN_UNINIT_POINTER(data) +#endif + template struct LinkedListItem : public T { LinkedListItem *next; }; +// Like std::vector but without initialization to 0 and some extra methods. +class PWBuffer : public NonCopyable +{ +public: + PWBuffer() + { + init(); + } + PWBuffer(void* inData, size_t _size) + { + init(); + append(inData, _size); + } + PWBuffer(size_t _size) + { + init(); + resize(_size); + } + PWBuffer(PWBuffer&& buffer) + { + init(); + swap(buffer); + } + void operator=(PWBuffer&& buffer) + { + swap(buffer); + } + ~PWBuffer() + { + free(m_Data); + } + void swap(PWBuffer& other) + { + std::swap(m_Data, other.m_Data); + std::swap(m_Size, other.m_Size); + std::swap(m_Capacity, other.m_Capacity); + } + void resize(size_t newSize) + { + reserve(newSize); + m_Size = newSize; + } + void reserve(size_t newSize) + { + if (newSize > m_Capacity) + { + reallocMe(std::max(newSize, m_Capacity * 2)); + } + else if (newSize * 4 < m_Capacity) + { + reallocMe(newSize); + } + } + void clear() { resize(0); } + void append(void* inData, size_t _size) + { + size_t old = m_Size; + resize(old + _size); + memcpy(&m_Data[old], inData, _size); + } + u8* data() { return m_Data; } + const u8* data() const { return m_Data; } + u8& operator[](size_t i) { return m_Data[i]; } + const u8& operator[](size_t i) const { return m_Data[i]; } + size_t size() const { return m_Size; } + bool empty() const { return m_Size == 0; } +private: + void reallocMe(size_t newSize) + { + m_Data = (u8*) realloc(m_Data, newSize); + m_Capacity = newSize; + } + void init() + { + m_Data = NULL; + m_Size = m_Capacity = 0; + } + u8* m_Data; + size_t m_Size; + size_t m_Capacity; +}; + // Wrapper class class PointerWrap { @@ -39,19 +127,26 @@ class PointerWrap { MODE_READ = 1, // load MODE_WRITE, // save - MODE_MEASURE, // calculate size MODE_VERIFY, // compare }; - u8 **ptr; + PWBuffer* vec; + size_t readOff; + bool failure; Mode mode; public: - PointerWrap(u8 **ptr_, Mode mode_) : ptr(ptr_), mode(mode_) {} + PointerWrap(PWBuffer* vec_, Mode mode_) : vec(vec_) { SetMode(mode_); } - void SetMode(Mode mode_) { mode = mode_; } + void SetMode(Mode mode_) + { + mode = mode_; + readOff = 0; + failure = false; + if (mode_ == MODE_WRITE && vec) + vec->clear(); + } Mode GetMode() const { return mode; } - u8** GetPPtr() { return ptr; } template void Do(std::map& x) @@ -72,7 +167,6 @@ class PointerWrap break; case MODE_WRITE: - case MODE_MEASURE: case MODE_VERIFY: for (auto itr = x.begin(); itr != x.end(); ++itr) { @@ -88,7 +182,8 @@ class PointerWrap { u32 size = (u32)x.size(); Do(size); - x.resize(size); + if (mode == MODE_READ) + x.resize(size); for (auto itr = x.begin(); itr != x.end(); ++itr) Do(*itr); @@ -97,7 +192,18 @@ class PointerWrap template void Do(std::vector& x) { - DoContainer(x); + if (std::is_pod::value) + { + u32 size = (u32)x.size(); + Do(size); + if (mode == MODE_READ) + x.resize(size); + DoArray(x.data(), size); + } + else + { + DoContainer(x); + } } template @@ -227,65 +333,40 @@ class PointerWrap { PanicAlertT("Error: After \"%s\", found %d (0x%X) instead of save marker %d (0x%X). Aborting savestate load...", prevName, cookie, cookie, arbitraryNumber, arbitraryNumber); - mode = PointerWrap::MODE_MEASURE; + failure = true; } } private: - __forceinline void DoByte(u8& x) - { - switch (mode) - { - case MODE_READ: - x = **ptr; - break; - - case MODE_WRITE: - **ptr = x; - break; - - case MODE_MEASURE: - break; - - case MODE_VERIFY: - _dbg_assert_msg_(COMMON, (x == **ptr), - "Savestate verification failure: %d (0x%X) (at %p) != %d (0x%X) (at %p).\n", - x, x, &x, **ptr, **ptr, *ptr); - break; - - default: - break; - } - - ++(*ptr); - } - void DoVoid(void *data, u32 size) { + NO_WARN_UNINIT_POINTER(data); switch (mode) { case MODE_READ: - memcpy(data, *ptr, size); + if (size > vec->size() - readOff) + failure = true; + else + memcpy(data, vec->data() + readOff, size); break; case MODE_WRITE: - memcpy(*ptr, data, size); - break; - - case MODE_MEASURE: + vec->append(data, size); break; case MODE_VERIFY: - _dbg_assert_msg_(COMMON, !memcmp(data, *ptr, size), - "Savestate verification failure at %p\n", - *ptr); + if (size > vec->size() - readOff) + failure = true; + else + _dbg_assert_msg_(COMMON, !memcmp(data, vec->data() + readOff, size), + "Savestate verification failure at %u\n", (unsigned) readOff); break; default: break; } - *ptr += size; + readOff += size; } }; @@ -343,15 +424,14 @@ class CChunkFileReader } // read the state - std::vector buffer(sz); - if (!pFile.ReadArray(&buffer[0], sz)) + PWBuffer buffer(sz); + if (!pFile.ReadBytes(buffer.data(), sz)) { ERROR_LOG(COMMON,"ChunkReader: Error reading file"); return false; } - u8* ptr = &buffer[0]; - PointerWrap p(&ptr, PointerWrap::MODE_READ); + PointerWrap p(&buffer, PointerWrap::MODE_READ); _class.DoState(p); INFO_LOG(COMMON, "ChunkReader: Done loading %s" , _rFilename.c_str()); @@ -370,20 +450,14 @@ class CChunkFileReader return false; } - // Get data - u8 *ptr = 0; - PointerWrap p(&ptr, PointerWrap::MODE_MEASURE); - _class.DoState(p); - size_t const sz = (size_t)ptr; - std::vector buffer(sz); - ptr = &buffer[0]; - p.SetMode(PointerWrap::MODE_WRITE); + PWBuffer buffer; + PointerWrap p(&buffer, PointerWrap::MODE_WRITE); _class.DoState(p); // Create header SChunkHeader header; header.Revision = _Revision; - header.ExpectedSize = (u32)sz; + header.ExpectedSize = (u32)buffer.size(); // Write to file if (!pFile.WriteArray(&header, 1)) @@ -392,7 +466,7 @@ class CChunkFileReader return false; } - if (!pFile.WriteArray(&buffer[0], sz)) + if (!pFile.WriteBytes(buffer.data(), buffer.size())) { ERROR_LOG(COMMON,"ChunkReader: Failed writing data"); return false; diff --git a/Source/Core/Common/Src/Common.h b/Source/Core/Common/Src/Common.h index 6556f1fd177c..c4bf6a605a81 100644 --- a/Source/Core/Common/Src/Common.h +++ b/Source/Core/Common/Src/Common.h @@ -41,8 +41,8 @@ class NonCopyable NonCopyable(const NonCopyable&&) {} void operator=(const NonCopyable&&) {} private: - NonCopyable(NonCopyable&); - NonCopyable& operator=(NonCopyable& other); + NonCopyable(const NonCopyable&); + NonCopyable& operator=(const NonCopyable& other); }; #endif diff --git a/Source/Core/Core/Src/HW/Wiimote.cpp b/Source/Core/Core/Src/HW/Wiimote.cpp index 2061e80f2e3a..175988adb66e 100644 --- a/Source/Core/Core/Src/HW/Wiimote.cpp +++ b/Source/Core/Core/Src/HW/Wiimote.cpp @@ -152,11 +152,8 @@ unsigned int GetAttached() // input/output: ptr: [Description Needed] // input: mode [Description needed] // -void DoState(u8 **ptr, PointerWrap::Mode mode) +void DoState(PointerWrap& p) { - // TODO: - - PointerWrap p(ptr, mode); for (unsigned int i=0; iDoState(p); } diff --git a/Source/Core/Core/Src/HW/Wiimote.h b/Source/Core/Core/Src/HW/Wiimote.h index be1de18bc9ea..9f37c329956a 100644 --- a/Source/Core/Core/Src/HW/Wiimote.h +++ b/Source/Core/Core/Src/HW/Wiimote.h @@ -40,7 +40,7 @@ void Resume(); void Pause(); unsigned int GetAttached(); -void DoState(u8 **ptr, PointerWrap::Mode mode); +void DoState(PointerWrap& p); void EmuStateChange(EMUSTATE_CHANGE newState); InputPlugin *GetPlugin(); diff --git a/Source/Core/Core/Src/State.cpp b/Source/Core/Core/Src/State.cpp index 24647c143ad0..eb721c6b610d 100644 --- a/Source/Core/Core/Src/State.cpp +++ b/Source/Core/Core/Src/State.cpp @@ -48,8 +48,8 @@ static std::string g_last_filename; static CallbackFunc g_onAfterLoadCb = NULL; // Temporary undo state buffer -static std::vector g_undo_load_buffer; -static std::vector g_current_buffer; +static PWBuffer g_undo_load_buffer; +static PWBuffer g_current_buffer; static int g_loadDepth = 0; static std::mutex g_cs_undo_load_buffer; @@ -90,7 +90,7 @@ void DoState(PointerWrap &p) // if the version doesn't match, fail. // this will trigger a message like "Can't load state from other revisions" // we could use the version numbers to maintain some level of backward compatibility, but currently don't. - p.SetMode(PointerWrap::MODE_MEASURE); + p.failure = true; return; } @@ -101,7 +101,7 @@ void DoState(PointerWrap &p) p.DoMarker("video_backend"); if (Core::g_CoreStartupParameter.bWii) - Wiimote::DoState(p.GetPPtr(), p.GetMode()); + Wiimote::DoState(p); p.DoMarker("Wiimote"); PowerPC::DoState(p); @@ -114,41 +114,32 @@ void DoState(PointerWrap &p) p.DoMarker("Movie"); } -void LoadFromBuffer(std::vector& buffer) +void LoadFromBuffer(PWBuffer& buffer) { bool wasUnpaused = Core::PauseAndLock(true); - u8* ptr = &buffer[0]; - PointerWrap p(&ptr, PointerWrap::MODE_READ); + PointerWrap p(&buffer, PointerWrap::MODE_READ); DoState(p); Core::PauseAndLock(false, wasUnpaused); } -void SaveToBuffer(std::vector& buffer) +void SaveToBuffer(PWBuffer& buffer) { bool wasUnpaused = Core::PauseAndLock(true); - u8* ptr = NULL; - PointerWrap p(&ptr, PointerWrap::MODE_MEASURE); - - DoState(p); - const size_t buffer_size = reinterpret_cast(ptr); - buffer.resize(buffer_size); - - ptr = &buffer[0]; - p.SetMode(PointerWrap::MODE_WRITE); + buffer.reserve(80000000); + PointerWrap p(&buffer, PointerWrap::MODE_WRITE); DoState(p); Core::PauseAndLock(false, wasUnpaused); } -void VerifyBuffer(std::vector& buffer) +void VerifyBuffer(PWBuffer& buffer) { bool wasUnpaused = Core::PauseAndLock(true); - u8* ptr = &buffer[0]; - PointerWrap p(&ptr, PointerWrap::MODE_VERIFY); + PointerWrap p(&buffer, PointerWrap::MODE_VERIFY); DoState(p); Core::PauseAndLock(false, wasUnpaused); @@ -198,7 +189,7 @@ std::map GetSavedStates() struct CompressAndDumpState_args { - std::vector* buffer_vector; + PWBuffer* buffer_vector; std::mutex* buffer_mutex; std::string filename; bool wait; @@ -293,42 +284,28 @@ void SaveAs(const std::string& filename, bool wait) // Pause the core while we save the state bool wasUnpaused = Core::PauseAndLock(true); - // Measure the size of the buffer. - u8 *ptr = NULL; - PointerWrap p(&ptr, PointerWrap::MODE_MEASURE); - DoState(p); - const size_t buffer_size = reinterpret_cast(ptr); - - // Then actually do the write. { std::lock_guard lk(g_cs_current_buffer); - g_current_buffer.resize(buffer_size); - ptr = &g_current_buffer[0]; + g_current_buffer.reserve(80000000); + PointerWrap p(&g_current_buffer, PointerWrap::MODE_WRITE); p.SetMode(PointerWrap::MODE_WRITE); DoState(p); + printf("new size is %zd\n", g_current_buffer.size()); } - if (p.GetMode() == PointerWrap::MODE_WRITE) - { - Core::DisplayMessage("Saving State...", 1000); + Core::DisplayMessage("Saving State...", 1000); - CompressAndDumpState_args save_args; - save_args.buffer_vector = &g_current_buffer; - save_args.buffer_mutex = &g_cs_current_buffer; - save_args.filename = filename; - save_args.wait = wait; + CompressAndDumpState_args save_args; + save_args.buffer_vector = &g_current_buffer; + save_args.buffer_mutex = &g_cs_current_buffer; + save_args.filename = filename; + save_args.wait = wait; - Flush(); - g_save_thread = std::thread(CompressAndDumpState, save_args); - g_compressAndDumpStateSyncEvent.Wait(); + Flush(); + g_save_thread = std::thread(CompressAndDumpState, save_args); + g_compressAndDumpStateSyncEvent.Wait(); - g_last_filename = filename; - } - else - { - // someone aborted the save by changing the mode? - Core::DisplayMessage("Unable to Save : Internal DoState Error", 4000); - } + g_last_filename = filename; // Resume the core and disable stepping Core::PauseAndLock(false, wasUnpaused); @@ -348,7 +325,7 @@ bool ReadHeader(const std::string filename, StateHeader& header) return true; } -void LoadFileStateData(const std::string& filename, std::vector& ret_data) +void LoadFileStateData(const std::string& filename, PWBuffer& ret_data) { Flush(); File::IOFile f(filename, "rb"); @@ -368,7 +345,7 @@ void LoadFileStateData(const std::string& filename, std::vector& ret_data) return; } - std::vector buffer; + PWBuffer buffer; if (0 != header.size) // non-zero size means the state is compressed { @@ -386,7 +363,7 @@ void LoadFileStateData(const std::string& filename, std::vector& ret_data) break; f.ReadBytes(out, cur_len); - const int res = lzo1x_decompress(out, cur_len, &buffer[i], &new_len, NULL); + const int res = lzo1x_decompress(out, cur_len, (u8*) buffer.data() + i, &new_len, NULL); if (res != LZO_E_OK) { // This doesn't seem to happen anymore. @@ -403,7 +380,7 @@ void LoadFileStateData(const std::string& filename, std::vector& ret_data) const size_t size = (size_t)(f.GetSize() - sizeof(StateHeader)); buffer.resize(size); - if (!f.ReadBytes(&buffer[0], size)) + if (!f.ReadBytes(buffer.data(), size)) { PanicAlert("wtf? reading bytes: %i", (int)size); return; @@ -447,16 +424,15 @@ void LoadAs(const std::string& filename) // brackets here are so buffer gets freed ASAP { - std::vector buffer; + PWBuffer buffer; LoadFileStateData(filename, buffer); if (!buffer.empty()) { - u8 *ptr = &buffer[0]; - PointerWrap p(&ptr, PointerWrap::MODE_READ); + PointerWrap p(&buffer, PointerWrap::MODE_READ); DoState(p); loaded = true; - loadedSuccessfully = (p.GetMode() == PointerWrap::MODE_READ); + loadedSuccessfully = !p.failure; } } @@ -505,16 +481,15 @@ void VerifyAt(const std::string& filename) { bool wasUnpaused = Core::PauseAndLock(true); - std::vector buffer; + PWBuffer buffer; LoadFileStateData(filename, buffer); if (!buffer.empty()) { - u8 *ptr = &buffer[0]; - PointerWrap p(&ptr, PointerWrap::MODE_VERIFY); + PointerWrap p(&buffer, PointerWrap::MODE_VERIFY); DoState(p); - if (p.GetMode() == PointerWrap::MODE_VERIFY) + if (!p.failure) Core::DisplayMessage(StringFromFormat("Verified state at %s", filename.c_str()).c_str(), 2000); else Core::DisplayMessage("Unable to Verify : Can't verify state from other revisions !", 4000); @@ -538,12 +513,12 @@ void Shutdown() // this gives a better guarantee to free the allocated memory right NOW (as opposed to, actually, never) { std::lock_guard lk(g_cs_current_buffer); - std::vector().swap(g_current_buffer); + PWBuffer().swap(g_current_buffer); } { std::lock_guard lk(g_cs_undo_load_buffer); - std::vector().swap(g_undo_load_buffer); + PWBuffer().swap(g_undo_load_buffer); } } From 81c38b04b7e4b4f85f7582502e7dfd6b8e208936 Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 3 Oct 2013 18:33:35 -0400 Subject: [PATCH 010/202] Add Packet class. --- Source/Core/Common/Src/ChunkFile.h | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index 0ae0f4dfb472..2a664b468fcc 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -370,6 +370,43 @@ class PointerWrap } }; +// Convenience methods for packets. +class Packet : public PointerWrap +{ +public: + Packet() : PointerWrap(NULL, MODE_WRITE) + { + vec = &store; + } + + // c++ + Packet(Packet&& other_) : PointerWrap(std::move(other_)), store(std::move(other_.store)) + { + vec = &store; + } + void operator=(Packet&& other_) + { + PointerWrap::operator=(std::move(other_)); + store = std::move(other_.store); + vec = &store; + } + + Packet(PWBuffer&& vec_) : PointerWrap(NULL, MODE_READ), store(std::move(vec_)) + { + vec = &store; + } + + // Write an rvalue. + template + void W(T t) + { + PointerWrap::Do(t); + } + +private: + PWBuffer store; +}; + class CChunkFileReader { public: From dce7dbf360307ed842f30d475f51b7a98de7218d Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 3 Oct 2013 18:38:14 -0400 Subject: [PATCH 011/202] Bump Netplay version. --- Source/Core/Core/Src/NetPlayProto.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/Core/Src/NetPlayProto.h b/Source/Core/Core/Src/NetPlayProto.h index 6fe6899b870a..f3d4363e8413 100644 --- a/Source/Core/Core/Src/NetPlayProto.h +++ b/Source/Core/Core/Src/NetPlayProto.h @@ -27,7 +27,7 @@ struct Rpt : public std::vector typedef std::vector NetWiimote; -#define NETPLAY_VERSION "Dolphin NetPlay 2013-09-22" +#define NETPLAY_VERSION "Dolphin NetPlay 2013-10-03" const int NETPLAY_INITIAL_GCTIME = 1272737767; From 54cb5b39f4f6f3d74db3c1985c2282f09a6d6173 Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 3 Oct 2013 18:56:36 -0400 Subject: [PATCH 012/202] Simplify Netplay locking. --- Source/Core/Core/Src/NetPlayClient.cpp | 121 ++++++++++++------------- Source/Core/Core/Src/NetPlayClient.h | 7 +- Source/Core/Core/Src/NetPlayServer.cpp | 45 ++------- Source/Core/Core/Src/NetPlayServer.h | 7 +- 4 files changed, 71 insertions(+), 109 deletions(-) diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 8048487f7efa..2e6078d769b5 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -137,8 +137,8 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) packet >> player.revision; { - std::lock_guard lkp(m_crit.players); - m_players[player.pid] = player; + std::lock_guard lk(m_crit); + m_players[player.pid] = player; } m_dialog->Update(); @@ -151,8 +151,8 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) packet >> pid; { - std::lock_guard lkp(m_crit.players); - m_players.erase(m_players.find(pid)); + std::lock_guard lk(m_crit); + m_players.erase(m_players.find(pid)); } m_dialog->Update(); @@ -242,8 +242,8 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case NP_MSG_CHANGE_GAME : { { - std::lock_guard lkg(m_crit.game); - packet >> m_selected_game; + std::lock_guard lk(m_crit); + packet >> m_selected_game; } // update gui @@ -254,17 +254,17 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case NP_MSG_START_GAME : { { - std::lock_guard lkg(m_crit.game); - packet >> m_current_game; - packet >> g_NetPlaySettings.m_CPUthread; - packet >> g_NetPlaySettings.m_DSPEnableJIT; - packet >> g_NetPlaySettings.m_DSPHLE; - packet >> g_NetPlaySettings.m_WriteToMemcard; - int tmp; - packet >> tmp; - g_NetPlaySettings.m_EXIDevice[0] = (TEXIDevices) tmp; - packet >> tmp; - g_NetPlaySettings.m_EXIDevice[1] = (TEXIDevices) tmp; + std::lock_guard lk(m_crit); + packet >> m_current_game; + packet >> g_NetPlaySettings.m_CPUthread; + packet >> g_NetPlaySettings.m_DSPEnableJIT; + packet >> g_NetPlaySettings.m_DSPHLE; + packet >> g_NetPlaySettings.m_WriteToMemcard; + int tmp; + packet >> tmp; + g_NetPlaySettings.m_EXIDevice[0] = (TEXIDevices) tmp; + packet >> tmp; + g_NetPlaySettings.m_EXIDevice[1] = (TEXIDevices) tmp; } m_dialog->OnMsgStartGame(); @@ -280,7 +280,7 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case NP_MSG_DISABLE_GAME : { PanicAlertT("Other client disconnected while game is running!! NetPlay is disabled. You manually stop the game."); - std::lock_guard lkg(m_crit.game); + std::lock_guard lk(m_crit); m_is_running = false; NetPlay_Disable(); } @@ -295,7 +295,7 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) spac << (MessageId)NP_MSG_PONG; spac << ping_key; - std::lock_guard lks(m_crit.send); + std::lock_guard lk(m_crit); m_socket.Send(spac); } break; @@ -306,9 +306,9 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) packet >> pid; { - std::lock_guard lkp(m_crit.players); - Player& player = m_players[pid]; - packet >> player.ping; + std::lock_guard lk(m_crit); + Player& player = m_players[pid]; + packet >> player.ping; } m_dialog->Update(); @@ -358,7 +358,7 @@ void NetPlayClient::ThreadFunc() // called from ---GUI--- thread void NetPlayClient::GetPlayerList(std::string& list, std::vector& pid_list) { - std::lock_guard lkp(m_crit.players); + std::lock_guard lk(m_crit); std::ostringstream ss; @@ -393,7 +393,7 @@ void NetPlayClient::GetPlayerList(std::string& list, std::vector& pid_list) // called from ---GUI--- thread void NetPlayClient::GetPlayers(std::vector &player_list) { - std::lock_guard lkp(m_crit.players); + std::lock_guard lk(m_crit); std::map::const_iterator i = m_players.begin(), e = m_players.end(); @@ -412,7 +412,7 @@ void NetPlayClient::SendChatMessage(const std::string& msg) spac << (MessageId)NP_MSG_CHAT_MESSAGE; spac << msg; - std::lock_guard lks(m_crit.send); + std::lock_guard lk(m_crit); m_socket.Send(spac); } @@ -425,7 +425,7 @@ void NetPlayClient::SendPadState(const PadMapping in_game_pad, const NetPad& np) spac << in_game_pad; spac << np.nHi << np.nLo; - std::lock_guard lks(m_crit.send); + std::lock_guard lk(m_crit); m_socket.Send(spac); } @@ -441,14 +441,14 @@ void NetPlayClient::SendWiimoteState(const PadMapping in_game_pad, const NetWiim for (unsigned int i = 0; i < size; ++i) spac << nw.data()[i]; - std::lock_guard lks(m_crit.send); + std::lock_guard lk(m_crit); m_socket.Send(spac); } // called from ---GUI--- thread bool NetPlayClient::StartGame(const std::string &path) { - std::lock_guard lkg(m_crit.game); + std::lock_guard lk(m_crit); // tell server i started the game sf::Packet spac; @@ -456,7 +456,6 @@ bool NetPlayClient::StartGame(const std::string &path) spac << m_current_game; spac << (char *)&g_NetPlaySettings; - std::lock_guard lks(m_crit.send); m_socket.Send(spac); if (m_is_running) @@ -636,42 +635,42 @@ bool NetPlayClient::WiimoteUpdate(int _number, u8* data, const u8 size) NetWiimote nw; static u8 previousSize[4] = {4,4,4,4}; { - std::lock_guard lkp(m_crit.players); + std::lock_guard lk(m_crit); - // in game mapping for this local wiimote - unsigned int in_game_num = LocalWiimoteToInGameWiimote(_number); - // does this local wiimote map in game? - if (in_game_num < 4) - { - if (previousSize[in_game_num] == size) + // in game mapping for this local wiimote + unsigned int in_game_num = LocalWiimoteToInGameWiimote(_number); + // does this local wiimote map in game? + if (in_game_num < 4) { - nw.assign(data, data + size); - do + if (previousSize[in_game_num] == size) { - // add to buffer - m_wiimote_buffer[in_game_num].Push(nw); - - SendWiimoteState(in_game_num, nw); - } while (m_wiimote_buffer[in_game_num].Size() <= m_target_buffer_size * 200 / 120); // TODO: add a seperate setting for wiimote buffer? - } - else - { - while (m_wiimote_buffer[in_game_num].Size() > 0) + nw.assign(data, data + size); + do + { + // add to buffer + m_wiimote_buffer[in_game_num].Push(nw); + + SendWiimoteState(in_game_num, nw); + } while (m_wiimote_buffer[in_game_num].Size() <= m_target_buffer_size * 200 / 120); // TODO: add a seperate setting for wiimote buffer? + } + else { - // Reporting mode changed, so previous buffer is no good. - m_wiimote_buffer[in_game_num].Pop(); + while (m_wiimote_buffer[in_game_num].Size() > 0) + { + // Reporting mode changed, so previous buffer is no good. + m_wiimote_buffer[in_game_num].Pop(); + } + nw.resize(size, 0); + + m_wiimote_buffer[in_game_num].Push(nw); + m_wiimote_buffer[in_game_num].Push(nw); + m_wiimote_buffer[in_game_num].Push(nw); + m_wiimote_buffer[in_game_num].Push(nw); + m_wiimote_buffer[in_game_num].Push(nw); + m_wiimote_buffer[in_game_num].Push(nw); + previousSize[in_game_num] = size; } - nw.resize(size, 0); - - m_wiimote_buffer[in_game_num].Push(nw); - m_wiimote_buffer[in_game_num].Push(nw); - m_wiimote_buffer[in_game_num].Push(nw); - m_wiimote_buffer[in_game_num].Push(nw); - m_wiimote_buffer[in_game_num].Push(nw); - m_wiimote_buffer[in_game_num].Push(nw); - previousSize[in_game_num] = size; } - } } // unlock players @@ -728,9 +727,9 @@ bool NetPlayClient::WiimoteUpdate(int _number, u8* data, const u8 size) // called from ---GUI--- thread and ---NETPLAY--- thread (client side) bool NetPlayClient::StopGame() { - std::lock_guard lkg(m_crit.game); + std::lock_guard lk(m_crit); - if (false == m_is_running) + if (!m_is_running) { PanicAlertT("Game isn't running!"); return false; diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 310c73e57786..93279c08b34e 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -92,12 +92,7 @@ class NetPlayClient protected: void ClearBuffers(); - struct - { - std::recursive_mutex game; - // lock order - std::recursive_mutex players, send; - } m_crit; + std::recursive_mutex m_crit; Common::FifoQueue m_pad_buffer[4]; Common::FifoQueue m_wiimote_buffer[4]; diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index d1162e32d9c5..96674b2c36ad 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -45,7 +45,6 @@ void NetPlayServer::ThreadFunc() spac << (MessageId)NP_MSG_PING; spac << m_ping_key; - std::lock_guard lks(m_crit.send); m_ping_timer.Start(); SendToClients(spac); @@ -66,7 +65,7 @@ void NetPlayServer::ThreadFunc() unsigned int error; { - std::lock_guard lkg(m_crit.game); + std::lock_guard lk(m_crit); error = OnConnect(accept_socket); } @@ -95,7 +94,7 @@ void NetPlayServer::ThreadFunc() //case sf::Socket::Disconnected : default : { - std::lock_guard lkg(m_crit.game); + std::lock_guard lk(m_crit); OnDisconnect(ready_socket); break; } @@ -158,8 +157,7 @@ unsigned int NetPlayServer::OnConnect(sf::SocketTCP& socket) } } - { - std::lock_guard lks(m_crit.send); + std::lock_guard lk(m_crit); // send join message to already connected clients sf::Packet spac; @@ -200,17 +198,9 @@ unsigned int NetPlayServer::OnConnect(sf::SocketTCP& socket) socket.Send(spac); } - } // unlock send - - // add client to the player list - { - std::lock_guard lkp(m_crit.players); m_players[socket] = player; - std::lock_guard lks(m_crit.send); UpdatePadMapping(); // sync pad mappings with everyone UpdateWiimoteMapping(); - } - // add client to selector/ used for receiving m_selector.Add(socket); @@ -224,13 +214,12 @@ unsigned int NetPlayServer::OnDisconnect(sf::SocketTCP& socket) if (m_is_running) { PanicAlertT("Client disconnect while game is running!! NetPlay is disabled. You must manually stop the game."); - std::lock_guard lkg(m_crit.game); + std::lock_guard lk(m_crit); m_is_running = false; sf::Packet spac; spac << (MessageId)NP_MSG_DISABLE_GAME; // this thread doesn't need players lock - std::lock_guard lks(m_crit.send); SendToClients(spac); } @@ -242,11 +231,10 @@ unsigned int NetPlayServer::OnDisconnect(sf::SocketTCP& socket) m_selector.Remove(socket); - std::lock_guard lkp(m_crit.players); + std::lock_guard lk(m_crit); m_players.erase(m_players.find(socket)); // alert other players of disconnect - std::lock_guard lks(m_crit.send); SendToClients(spac); for (int i = 0; i < 4; i++) @@ -314,8 +302,6 @@ void NetPlayServer::UpdateWiimoteMapping() // called from ---GUI--- thread and ---NETPLAY--- thread void NetPlayServer::AdjustPadBufferSize(unsigned int size) { - std::lock_guard lkg(m_crit.game); - m_target_buffer_size = size; // tell clients to change buffer size @@ -323,8 +309,6 @@ void NetPlayServer::AdjustPadBufferSize(unsigned int size) spac << (MessageId)NP_MSG_PAD_BUFFER; spac << (u32)m_target_buffer_size; - std::lock_guard lkp(m_crit.players); - std::lock_guard lks(m_crit.send); SendToClients(spac); } @@ -352,8 +336,7 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, sf::SocketTCP& socket) spac << msg; { - std::lock_guard lks(m_crit.send); - SendToClients(spac, player.pid); + SendToClients(spac, player.pid); } } break; @@ -378,7 +361,6 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, sf::SocketTCP& socket) spac << (MessageId)NP_MSG_PAD_DATA; spac << map << hi << lo; - std::lock_guard lks(m_crit.send); SendToClients(spac, player.pid); } break; @@ -414,7 +396,6 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, sf::SocketTCP& socket) delete[] data; - std::lock_guard lks(m_crit.send); SendToClients(spac, player.pid); } break; @@ -433,7 +414,6 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, sf::SocketTCP& socket) spac << player.pid; spac << player.ping; - std::lock_guard lks(m_crit.send); SendToClients(spac); } break; @@ -450,8 +430,6 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, sf::SocketTCP& socket) sf::Packet spac; spac << (MessageId)NP_MSG_STOP_GAME; - std::lock_guard lkp(m_crit.players); - std::lock_guard lks(m_crit.send); SendToClients(spac); m_is_running = false; @@ -476,15 +454,13 @@ void NetPlayServer::SendChatMessage(const std::string& msg) spac << (PlayerId)0; // server id always 0 spac << msg; - std::lock_guard lkp(m_crit.players); - std::lock_guard lks(m_crit.send); SendToClients(spac); } // called from ---GUI--- thread bool NetPlayServer::ChangeGame(const std::string &game) { - std::lock_guard lkg(m_crit.game); + std::lock_guard lk(m_crit); m_selected_game = game; @@ -493,8 +469,6 @@ bool NetPlayServer::ChangeGame(const std::string &game) spac << (MessageId)NP_MSG_CHANGE_GAME; spac << game; - std::lock_guard lkp(m_crit.players); - std::lock_guard lks(m_crit.send); SendToClients(spac); return true; @@ -509,7 +483,7 @@ void NetPlayServer::SetNetSettings(const NetSettings &settings) // called from ---GUI--- thread bool NetPlayServer::StartGame(const std::string &path) { - std::lock_guard lkg(m_crit.game); + std::lock_guard lk(m_crit); m_current_game = Common::Timer::GetTimeMs(); // no change, just update with clients @@ -526,8 +500,6 @@ bool NetPlayServer::StartGame(const std::string &path) spac << m_settings.m_EXIDevice[0]; spac << m_settings.m_EXIDevice[1]; - std::lock_guard lkp(m_crit.players); - std::lock_guard lks(m_crit.send); SendToClients(spac); m_is_running = true; @@ -538,6 +510,7 @@ bool NetPlayServer::StartGame(const std::string &path) // called from multiple threads void NetPlayServer::SendToClients(sf::Packet& packet, const PlayerId skip_pid) { + std::lock_guard lk(m_crit); std::map::iterator i = m_players.begin(), e = m_players.end(); diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index d362ced30ff5..8794f0e5d401 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -78,12 +78,7 @@ class NetPlayServer std::map m_players; - struct - { - std::recursive_mutex game; - // lock order - std::recursive_mutex players, send; - } m_crit; + std::recursive_mutex m_crit; std::string m_selected_game; From 76870d142eca7d9e9889cea255cfebabfae15c80 Mon Sep 17 00:00:00 2001 From: comex Date: Fri, 4 Oct 2013 03:08:47 -0400 Subject: [PATCH 013/202] Switch NetPlay from SFML to enet. operator=s abound, ahoy. --- Source/Core/Core/Src/NetPlayClient.cpp | 376 ++++++++++------- Source/Core/Core/Src/NetPlayClient.h | 19 +- Source/Core/Core/Src/NetPlayServer.cpp | 514 ++++++++++++------------ Source/Core/Core/Src/NetPlayServer.h | 36 +- Source/Core/DolphinWX/Src/NetWindow.cpp | 4 +- 5 files changed, 531 insertions(+), 418 deletions(-) diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 2e6078d769b5..1af28d9e9bdd 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -27,6 +27,52 @@ NetSettings g_NetPlaySettings; #define RPT_SIZE_HACK (1 << 16) +void ENetUtil::BroadcastPacket(ENetHost* host, const Packet& pac) +{ + enet_host_broadcast(host, 0, enet_packet_create((u8*) pac.vec->data(), pac.vec->size(), ENET_PACKET_FLAG_RELIABLE)); +} + +void ENetUtil::SendPacket(ENetPeer* peer, const Packet& pac) +{ + enet_peer_send(peer, 0, enet_packet_create((u8*) pac.vec->data(), pac.vec->size(), ENET_PACKET_FLAG_RELIABLE)); +} + +Packet ENetUtil::MakePacket(ENetPacket* epacket) +{ + Packet pac(PWBuffer(epacket->data, epacket->dataLength)); + enet_packet_destroy(epacket); + return pac; +} + +void ENetUtil::Wakeup(ENetHost* host) +{ + // Send ourselves a spurious message. This is hackier than it should be. + // I reported this as https://github.com/lsalzman/enet/issues/23, so + // hopefully there will be a better way to do it soon. + ENetAddress address; + if (host->address.port != 0) + address.port = host->address.port; + else + enet_socket_get_address(host->socket, &address); + address.host = 0x0100007f; // localhost + + u8 byte = 0; + ENetBuffer buf; + buf.data = &byte; + buf.dataLength = 1; + enet_socket_send(host->socket, &address, &buf, 1); +} + +int ENET_CALLBACK ENetUtil::InterceptCallback(ENetHost* host, ENetEvent* event) +{ + if (host->receivedDataLength == 1) + { + event->type = (ENetEventType) 42; + return 1; + } + return 0; +} + NetPad::NetPad() { nHi = 0x00808080; @@ -55,58 +101,84 @@ NetPlayClient::~NetPlayClient() if (m_IsConnected) { m_do_loop = false; + ENetUtil::Wakeup(m_host); if (m_dialog) m_thread.join(); } + + if (m_host) + enet_host_destroy(m_host); } // called from ---GUI--- thread -NetPlayClient::NetPlayClient(const std::string& address, const u16 port, const std::string& name) : m_is_running(false), m_do_loop(true) +NetPlayClient::NetPlayClient(const std::string& address, const u16 port, const std::string& name) { + m_is_running = false; + m_do_loop = true; m_target_buffer_size = 20; - ClearBuffers(); - m_IsConnected = false; m_dialog = NULL; + m_host = NULL; + m_ServerError = 0; - // why is false successful? documentation says true is - if (0 == m_socket.Connect(port, address, 5)) - { - // send connect message - sf::Packet spac; - spac << NETPLAY_VERSION; - spac << netplay_dolphin_ver; - spac << name; - m_socket.Send(spac); - - sf::Packet rpac; - // TODO: make this not hang - m_socket.Receive(rpac); - rpac >> m_ServerError; - - // got error message - if (m_ServerError) - { - m_socket.Close(); - } - else - { - rpac >> m_pid; + ClearBuffers(); - Player player; - player.name = name; - player.pid = m_pid; - player.revision = netplay_dolphin_ver; + ENetAddress addr; - // add self to player list - m_players[m_pid] = player; - m_local_player = &m_players[m_pid]; + if (enet_address_set_host(&addr, address.c_str())) + return; + addr.port = port; - //PanicAlertT("Connection successful: assigned player id: %d", m_pid); - m_IsConnected = true; + m_host = enet_host_create( + NULL, // address + 1, // peerCount + 1, // channelLimit + 0, // incomingBandwidth + 0); // outgoingBandwidth - m_selector.Add(m_socket); - } + m_host->intercept = ENetUtil::InterceptCallback; + + ENetEvent event; + enet_host_connect(m_host, &addr, /*channelCount=*/0, /*data=*/0); + int count = enet_host_service(m_host, &event, 1500); + if (count <= 0) + { + // The connection failed or timed out. + return; + } + + // send connect message + Packet hello; + hello.W(std::string(NETPLAY_VERSION)); + hello.W(std::string(netplay_dolphin_ver)); + hello.W(name); + ENetUtil::BroadcastPacket(m_host, hello); + + count = enet_host_service(m_host, &event, 1000); + if (count <= 0 || + event.type != ENET_EVENT_TYPE_RECEIVE) + { + // They disconnected or timed out. TODO: better error reporting. + return; + } + + Packet resp = ENetUtil::MakePacket(event.packet); + resp.Do(m_ServerError); + resp.Do(m_pid); + if (!resp.failure && !m_ServerError) + { + Player player; + player.name = name; + player.pid = m_pid; + player.revision = netplay_dolphin_ver; + + // add self to player list + m_players[m_pid] = player; + m_local_player = &m_players[m_pid]; + + //PanicAlertT("Connection successful: assigned player id: %d", m_pid); + printf("Connected!\n"); + m_IsConnected = true; } } @@ -121,20 +193,31 @@ void NetPlayClient::SetDialog(NetPlayUI* dialog) } } +// called from multiple threads +void NetPlayClient::SendPacket(Packet& packet) +{ + m_queue.Push(std::move(packet)); + ENetUtil::Wakeup(m_host); +} + // called from ---NETPLAY--- thread -unsigned int NetPlayClient::OnData(sf::Packet& packet) +void NetPlayClient::OnData(Packet&& packet) { MessageId mid; - packet >> mid; + packet.Do(mid); + if (packet.failure) + return OnDisconnect(); switch (mid) { case NP_MSG_PLAYER_JOIN : { Player player; - packet >> player.pid; - packet >> player.name; - packet >> player.revision; + packet.Do(player.pid); + packet.Do(player.name); + packet.Do(player.revision); + if (packet.failure) + return OnDisconnect(); { std::lock_guard lk(m_crit); @@ -148,7 +231,9 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case NP_MSG_PLAYER_LEAVE : { PlayerId pid; - packet >> pid; + packet.Do(pid); + if (packet.failure) + return OnDisconnect(); { std::lock_guard lk(m_crit); @@ -162,9 +247,11 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case NP_MSG_CHAT_MESSAGE : { PlayerId pid; - packet >> pid; std::string msg; - packet >> msg; + packet.Do(pid); + packet.Do(msg); + if (packet.failure) + return OnDisconnect(); // don't need lock to read in this thread const Player& player = m_players[pid]; @@ -179,8 +266,10 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case NP_MSG_PAD_MAPPING : { - for (PadMapping i = 0; i < 4; i++) - packet >> m_pad_map[i]; + packet.DoArray(m_pad_map, 4); + + if (packet.failure) + return OnDisconnect(); UpdateDevices(); @@ -190,8 +279,10 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case NP_MSG_WIIMOTE_MAPPING : { - for (PadMapping i = 0; i < 4; i++) - packet >> m_wiimote_map[i]; + packet.DoArray(m_wiimote_map, 4); + + if (packet.failure) + return OnDisconnect(); m_dialog->Update(); } @@ -199,11 +290,14 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case NP_MSG_PAD_DATA : { - PadMapping map = 0; + PadMapping map; NetPad np; - packet >> map >> np.nHi >> np.nLo; + packet.Do(map); + packet.Do(np.nHi); + packet.Do(np.nLo); + if (packet.failure || map < 0 || map >= 4) + return OnDisconnect(); - // trusting server for good map value (>=0 && <4) // add to pad buffer m_pad_buffer[map].Push(np); } @@ -211,21 +305,15 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case NP_MSG_WIIMOTE_DATA : { - PadMapping map = 0; + PadMapping map; NetWiimote nw; - u8 size; - packet >> map >> size; - - nw.resize(size); - u8* data = new u8[size]; - for (unsigned int i = 0; i < size; ++i) - packet >> data[i]; - nw.assign(data,data+size); - delete[] data; + packet.Do(map); + packet.Do(nw); + if (packet.failure || map < 0 || map >= 4) + return OnDisconnect(); - // trusting server for good map value (>=0 && <4) // add to wiimote buffer - m_wiimote_buffer[(unsigned)map].Push(nw); + m_wiimote_buffer[(unsigned)map].Push(std::move(nw)); } break; @@ -233,7 +321,9 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case NP_MSG_PAD_BUFFER : { u32 size = 0; - packet >> size; + packet.Do(size); + if (packet.failure) + return OnDisconnect(); m_target_buffer_size = size; } @@ -243,7 +333,7 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) { { std::lock_guard lk(m_crit); - packet >> m_selected_game; + packet.Do(m_selected_game); } // update gui @@ -255,16 +345,18 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) { { std::lock_guard lk(m_crit); - packet >> m_current_game; - packet >> g_NetPlaySettings.m_CPUthread; - packet >> g_NetPlaySettings.m_DSPEnableJIT; - packet >> g_NetPlaySettings.m_DSPHLE; - packet >> g_NetPlaySettings.m_WriteToMemcard; + packet.Do(m_current_game); + packet.Do(g_NetPlaySettings.m_CPUthread); + packet.Do(g_NetPlaySettings.m_DSPEnableJIT); + packet.Do(g_NetPlaySettings.m_DSPHLE); + packet.Do(g_NetPlaySettings.m_WriteToMemcard); int tmp; - packet >> tmp; + packet.Do(tmp); g_NetPlaySettings.m_EXIDevice[0] = (TEXIDevices) tmp; - packet >> tmp; + packet.Do(tmp); g_NetPlaySettings.m_EXIDevice[1] = (TEXIDevices) tmp; + if (packet.failure) + return OnDisconnect(); } m_dialog->OnMsgStartGame(); @@ -289,26 +381,32 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) case NP_MSG_PING : { u32 ping_key = 0; - packet >> ping_key; + packet.Do(ping_key); + if (packet.failure) + return OnDisconnect(); - sf::Packet spac; - spac << (MessageId)NP_MSG_PONG; - spac << ping_key; + Packet pong; + pong.W((MessageId)NP_MSG_PONG); + pong.W(ping_key); std::lock_guard lk(m_crit); - m_socket.Send(spac); + ENetUtil::BroadcastPacket(m_host, pong); } break; case NP_MSG_PLAYER_PING_DATA: { PlayerId pid; - packet >> pid; + u32 ping; + packet.Do(pid); + packet.Do(ping); + if (packet.failure) + return OnDisconnect(); { std::lock_guard lk(m_crit); Player& player = m_players[pid]; - packet >> player.ping; + player.ping = ping; } m_dialog->Update(); @@ -320,8 +418,16 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) break; } +} - return 0; +// called from ---NETPLAY--- thread +void NetPlayClient::OnDisconnect() +{ + m_is_running = false; + NetPlay_Disable(); + m_dialog->AppendChat("< LOST CONNECTION TO SERVER >"); + PanicAlertT("Lost connection to server."); + m_do_loop = false; } // called from ---NETPLAY--- thread @@ -329,30 +435,32 @@ void NetPlayClient::ThreadFunc() { while (m_do_loop) { - if (m_selector.Wait(0.01f)) + while (!m_queue.Empty()) { - sf::Packet rpac; - switch (m_socket.Receive(rpac)) + Packet& opacket = m_queue.Front(); + ENetUtil::BroadcastPacket(m_host, opacket); + m_queue.Pop(); + } + + ENetEvent event; + int count = enet_host_service(m_host, &event, 10000); + if (count < 0) + return OnDisconnect(); + if (count > 0) + { + switch (event.type) { - case sf::Socket::Done : - OnData(rpac); + case ENET_EVENT_TYPE_DISCONNECT: + OnDisconnect(); break; - - //case sf::Socket::Disconnected : - default : - m_is_running = false; - NetPlay_Disable(); - m_dialog->AppendChat("< LOST CONNECTION TO SERVER >"); - PanicAlertT("Lost connection to server!"); - m_do_loop = false; + case ENET_EVENT_TYPE_RECEIVE: + OnData(ENetUtil::MakePacket(event.packet)); + break; + default: break; } } } - - m_socket.Close(); - - return; } // called from ---GUI--- thread @@ -408,65 +516,61 @@ void NetPlayClient::GetPlayers(std::vector &player_list) // called from ---GUI--- thread void NetPlayClient::SendChatMessage(const std::string& msg) { - sf::Packet spac; - spac << (MessageId)NP_MSG_CHAT_MESSAGE; - spac << msg; + Packet packet; + packet.W((MessageId)NP_MSG_CHAT_MESSAGE); + packet.W(msg); - std::lock_guard lk(m_crit); - m_socket.Send(spac); + SendPacket(packet); } // called from ---CPU--- thread void NetPlayClient::SendPadState(const PadMapping in_game_pad, const NetPad& np) { // send to server - sf::Packet spac; - spac << (MessageId)NP_MSG_PAD_DATA; - spac << in_game_pad; - spac << np.nHi << np.nLo; + Packet packet; + packet.W((MessageId)NP_MSG_PAD_DATA); + packet.W(in_game_pad); + packet.W(np.nHi); + packet.W(np.nLo); - std::lock_guard lk(m_crit); - m_socket.Send(spac); + SendPacket(packet); } // called from ---CPU--- thread void NetPlayClient::SendWiimoteState(const PadMapping in_game_pad, const NetWiimote& nw) { // send to server - sf::Packet spac; - spac << (MessageId)NP_MSG_WIIMOTE_DATA; - spac << in_game_pad; - u8 size = nw.size(); - spac << size; - for (unsigned int i = 0; i < size; ++i) - spac << nw.data()[i]; + Packet packet; + packet.W((MessageId)NP_MSG_WIIMOTE_DATA); + packet.W(in_game_pad); + packet.W(nw); - std::lock_guard lk(m_crit); - m_socket.Send(spac); + SendPacket(packet); } // called from ---GUI--- thread bool NetPlayClient::StartGame(const std::string &path) { - std::lock_guard lk(m_crit); - - // tell server i started the game - sf::Packet spac; - spac << (MessageId)NP_MSG_START_GAME; - spac << m_current_game; - spac << (char *)&g_NetPlaySettings; - - m_socket.Send(spac); - if (m_is_running) { PanicAlertT("Game is already running!"); return false; } + m_is_running = true; + + // tell server i started the game + Packet packet; + packet.W((MessageId)NP_MSG_START_GAME); + packet.W(m_current_game); + packet.W(g_NetPlaySettings); + + SendPacket(packet); + + std::lock_guard lk(m_crit); + m_dialog->AppendChat(" -- STARTING GAME -- "); - m_is_running = true; NetPlay_Enable(this); ClearBuffers(); @@ -480,9 +584,9 @@ bool NetPlayClient::StartGame(const std::string &path) u8 controllers_mask = 0; for (unsigned int i = 0; i < 4; ++i) { - if (m_pad_map[i] > 0) + if (m_pad_map[i] != -1) controllers_mask |= (1 << i); - if (m_wiimote_map[i] > 0) + if (m_wiimote_map[i] != -1) controllers_mask |= (1 << (i + 4)); } Movie::BeginRecordingInput(controllers_mask); @@ -497,7 +601,7 @@ bool NetPlayClient::StartGame(const std::string &path) if (SConfig::GetInstance().m_LocalCoreStartupParameter.bWii) { for (unsigned int i = 0; i < 4; ++i) - WiimoteReal::ChangeWiimoteSource(i, m_wiimote_map[i] > 0 ? WIIMOTE_SRC_EMU : WIIMOTE_SRC_NONE); + WiimoteReal::ChangeWiimoteSource(i, m_wiimote_map[i] != -1 ? WIIMOTE_SRC_EMU : WIIMOTE_SRC_NONE); // Needed to prevent locking up at boot if (when) the wiimotes connect out of order. NetWiimote nw; @@ -527,7 +631,7 @@ void NetPlayClient::UpdateDevices() for (PadMapping i = 0; i < 4; i++) { // XXX: add support for other device types? does it matter? - SerialInterface::AddDevice(m_pad_map[i] > 0 ? SIDEVICE_GC_CONTROLLER : SIDEVICE_NONE, i); + SerialInterface::AddDevice(m_pad_map[i] != -1 ? SIDEVICE_GC_CONTROLLER : SIDEVICE_NONE, i); } } @@ -764,9 +868,9 @@ void NetPlayClient::Stop() // tell the server to stop if we have a pad mapped in game. if (isPadMapped) { - sf::Packet spac; - spac << (MessageId)NP_MSG_STOP_GAME; - m_socket.Send(spac); + Packet packet; + packet.W((MessageId)NP_MSG_STOP_GAME); + ENetUtil::BroadcastPacket(m_host, packet); } } diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 93279c08b34e..9ce78400e2be 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -10,7 +10,7 @@ #include "Thread.h" #include "Timer.h" -#include +#include "enet/enet.h" #include "NetPlayProto.h" #include "GCPadStatus.h" @@ -22,6 +22,15 @@ #include "FifoQueue.h" +namespace ENetUtil +{ + void BroadcastPacket(ENetHost* host, const Packet& pac); + void SendPacket(ENetPeer* peer, const Packet& pac); + Packet MakePacket(ENetPacket* epacket); + void Wakeup(ENetHost* host); + int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event); +} + class NetPad { public: @@ -98,9 +107,8 @@ class NetPlayClient Common::FifoQueue m_wiimote_buffer[4]; NetPlayUI* m_dialog; - sf::SocketTCP m_socket; + ENetHost* m_host; std::thread m_thread; - sf::Selector m_selector; std::string m_selected_game; volatile bool m_is_running; @@ -121,10 +129,13 @@ class NetPlayClient void UpdateDevices(); void SendPadState(const PadMapping in_game_pad, const NetPad& np); void SendWiimoteState(const PadMapping in_game_pad, const NetWiimote& nw); - unsigned int OnData(sf::Packet& packet); + void OnData(Packet&& packet); + void OnDisconnect(); + void SendPacket(Packet& packet); PlayerId m_pid; std::map m_players; + Common::FifoQueue m_queue; }; void NetPlay_Enable(NetPlayClient* const np); diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 96674b2c36ad..d6d00fa70ca1 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -3,30 +3,51 @@ // Refer to the license.txt file included. #include "NetPlayServer.h" +// for ENetUtils +#include "NetPlayClient.h" + +#define MAX_CLIENTS 200 NetPlayServer::~NetPlayServer() { - if (m_IsConnected) + if (m_host) { m_do_loop = false; + ENetUtil::Wakeup(m_host); m_thread.join(); - m_socket.Close(); + enet_host_destroy(m_host); } } // called from ---GUI--- thread -NetPlayServer::NetPlayServer(const u16 port) : m_IsConnected(false), m_is_running(false) +NetPlayServer::NetPlayServer() { + m_IsConnected = false; + m_is_running = false; + m_num_players = 0; memset(m_pad_map, -1, sizeof(m_pad_map)); memset(m_wiimote_map, -1, sizeof(m_wiimote_map)); - if (m_socket.Listen(port)) - { - m_IsConnected = true; - m_do_loop = true; - m_selector.Add(m_socket); - m_thread = std::thread(std::mem_fun(&NetPlayServer::ThreadFunc), this); - m_target_buffer_size = 20; - } + + ENetAddress address; + address.host = ENET_HOST_ANY; + address.port = ENET_PORT_ANY; + // leave some spaces free for server full notification + m_host = enet_host_create( + &address, // address + MAX_CLIENTS + 16, // peerCount + 1, // channelLimit + 0, // incomingBandwidth + 0); // outgoingBandwidth + + if (m_host == NULL) + return; + + m_host->intercept = ENetUtil::InterceptCallback; + + m_do_loop = true; + m_target_buffer_size = 20; + m_thread = std::thread(std::mem_fun(&NetPlayServer::ThreadFunc), this); + m_IsConnected = true; } // called from ---NETPLAY--- thread @@ -41,91 +62,63 @@ void NetPlayServer::ThreadFunc() m_ping_key = Common::Timer::GetTimeMs(); - sf::Packet spac; - spac << (MessageId)NP_MSG_PING; - spac << m_ping_key; - + Packet ping; + ping.W((MessageId)NP_MSG_PING); + ping.W(m_ping_key); + m_ping_timer.Start(); - SendToClients(spac); + SendToClientsOnThread(ping); m_update_pings = false; } - // check which sockets need attention - const unsigned int num = m_selector.Wait(0.01f); - for (unsigned int i=0; i lk(m_crit); - error = OnConnect(accept_socket); - } - - if (error) - { - sf::Packet spac; - spac << (MessageId)error; - // don't need to lock, this client isn't in the client map - accept_socket.Send(spac); - - // TODO: not sure if client gets the message if i close right away - accept_socket.Close(); - } - } - // client socket - else - { - sf::Packet rpac; - switch (ready_socket.Receive(rpac)) - { - case sf::Socket::Done : - // if a bad packet is received, disconnect the client - if (0 == OnData(rpac, ready_socket)) - break; - - //case sf::Socket::Disconnected : - default : - { - std::lock_guard lk(m_crit); - OnDisconnect(ready_socket); - break; - } - } - } + auto& out = m_queue.Front(); + SendToClientsOnThread(out.first, out.second); + m_queue.Pop(); } - } - // close listening socket and client sockets - { - std::map::reverse_iterator - i = m_players.rbegin(), - e = m_players.rend(); - for ( ; i!=e; ++i) - i->second.socket.Close(); - } + ENetEvent event; + int count = enet_host_service(m_host, &event, 10000); + if (count < 0) + { + PanicAlert("Wait failed... do something about this."); + continue; + } + + if (count == 0) + continue; - return; + PlayerId pid = event.peer - m_host->peers; + switch (event.type) + { + case ENET_EVENT_TYPE_DISCONNECT: + OnDisconnect(pid); + break; + case ENET_EVENT_TYPE_RECEIVE: + OnData(pid, ENetUtil::MakePacket(event.packet)); + break; + default: + // notably, ignore connects until we get a hello message + break; + } + } } // called from ---NETPLAY--- thread -unsigned int NetPlayServer::OnConnect(sf::SocketTCP& socket) +MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) { - sf::Packet rpac; - // TODO: make this not hang / check if good packet - socket.Receive(rpac); + Client& player = m_players[pid]; + ENetPeer* peer = &m_host->peers[pid]; std::string npver; - rpac >> npver; + hello.Do(npver); + hello.Do(player.revision); + hello.Do(player.name); // dolphin netplay version - if (npver != NETPLAY_VERSION) + if (hello.failure || npver != NETPLAY_VERSION) return CON_ERR_VERSION_MISMATCH; // game is currently running @@ -133,109 +126,107 @@ unsigned int NetPlayServer::OnConnect(sf::SocketTCP& socket) return CON_ERR_GAME_RUNNING; // too many players - if (m_players.size() >= 255) + if (m_num_players >= MAX_CLIENTS) return CON_ERR_SERVER_FULL; // cause pings to be updated m_update_pings = true; - Client player; - player.socket = socket; - rpac >> player.revision; - rpac >> player.name; - - // give new client first available id - player.pid = m_players.size() + 1; - // try to automatically assign new user a pad for (unsigned int m = 0; m < 4; ++m) { if (m_pad_map[m] == -1) { - m_pad_map[m] = player.pid; + m_pad_map[m] = pid; break; } } - std::lock_guard lk(m_crit); - // send join message to already connected clients - sf::Packet spac; - spac << (MessageId)NP_MSG_PLAYER_JOIN; - spac << player.pid << player.name << player.revision; - SendToClients(spac); + { + Packet opacket; + opacket.W((MessageId)NP_MSG_PLAYER_JOIN); + opacket.W(pid); + opacket.W(player.name); + opacket.W(player.revision); + SendToClientsOnThread(opacket); + } // send new client success message with their id - spac.Clear(); - spac << (MessageId)0; - spac << player.pid; - socket.Send(spac); + { + Packet opacket; + opacket.W((MessageId)0); + opacket.W(pid); + ENetUtil::SendPacket(peer, opacket); + } // send new client the selected game - if (m_selected_game != "") { - spac.Clear(); - spac << (MessageId)NP_MSG_CHANGE_GAME; - spac << m_selected_game; - socket.Send(spac); + std::lock_guard lk(m_crit); + if (m_selected_game != "") + { + Packet opacket; + opacket.W((MessageId)NP_MSG_CHANGE_GAME); + opacket.W(m_selected_game); + ENetUtil::SendPacket(peer, opacket); + } + } + + { + // send the pad buffer value + Packet opacket; + opacket.W((MessageId)NP_MSG_PAD_BUFFER); + opacket.W((u32)m_target_buffer_size); + ENetUtil::SendPacket(peer, opacket); } - // send the pad buffer value - spac.Clear(); - spac << (MessageId)NP_MSG_PAD_BUFFER; - spac << (u32)m_target_buffer_size; - socket.Send(spac); - - // sync values with new client - std::map::const_iterator - i, - e = m_players.end(); - for (i = m_players.begin(); i!=e; ++i) + // send players + for (auto it = m_players.begin(); it != m_players.end(); ++it) { - spac.Clear(); - spac << (MessageId)NP_MSG_PLAYER_JOIN; - spac << i->second.pid << i->second.name << i->second.revision; - socket.Send(spac); + Client& oplayer = *it; + if (oplayer.connected) + { + Packet opacket; + opacket.W((MessageId)NP_MSG_PLAYER_JOIN); + opacket.W(pid); + opacket.W(oplayer.name); + opacket.W(oplayer.revision); + ENetUtil::SendPacket(peer, opacket); + } } - m_players[socket] = player; UpdatePadMapping(); // sync pad mappings with everyone UpdateWiimoteMapping(); - // add client to selector/ used for receiving - m_selector.Add(socket); - return 0; } // called from ---NETPLAY--- thread -unsigned int NetPlayServer::OnDisconnect(sf::SocketTCP& socket) +void NetPlayServer::OnDisconnect(PlayerId pid) { + Client& player = m_players[pid]; + + if (!player.connected) + return; + if (m_is_running) { - PanicAlertT("Client disconnect while game is running!! NetPlay is disabled. You must manually stop the game."); - std::lock_guard lk(m_crit); + PanicAlertT("A client disconnected while game is running. NetPlay is disabled. You must manually stop the game."); m_is_running = false; - sf::Packet spac; - spac << (MessageId)NP_MSG_DISABLE_GAME; - // this thread doesn't need players lock - SendToClients(spac); + Packet opacket; + opacket.W((MessageId)NP_MSG_DISABLE_GAME); + SendToClientsOnThread(opacket); } - PlayerId pid = m_players[socket].pid; - - sf::Packet spac; - spac << (MessageId)NP_MSG_PLAYER_LEAVE; - spac << pid; + player.connected = false; - m_selector.Remove(socket); - - std::lock_guard lk(m_crit); - m_players.erase(m_players.find(socket)); + Packet opacket; + opacket.W((MessageId)NP_MSG_PLAYER_LEAVE); + opacket.W(pid); // alert other players of disconnect - SendToClients(spac); + SendToClientsOnThread(opacket); for (int i = 0; i < 4; i++) if (m_pad_map[i] == pid) @@ -246,8 +237,6 @@ unsigned int NetPlayServer::OnDisconnect(sf::SocketTCP& socket) if (m_wiimote_map[i] == pid) m_wiimote_map[i] = -1; UpdateWiimoteMapping(); - - return 0; } // called from ---GUI--- thread @@ -282,21 +271,19 @@ void NetPlayServer::SetWiimoteMapping(const PadMapping map[4]) // called from ---GUI--- thread and ---NETPLAY--- thread void NetPlayServer::UpdatePadMapping() { - sf::Packet spac; - spac << (MessageId)NP_MSG_PAD_MAPPING; - for (int i = 0; i < 4; i++) - spac << m_pad_map[i]; - SendToClients(spac); + Packet opacket; + opacket.W((MessageId)NP_MSG_PAD_MAPPING); + opacket.DoArray(m_pad_map, 4); + SendToClients(opacket); } -// called from ---NETPLAY--- thread +// called from ---GUI--- thread and ---NETPLAY--- thread void NetPlayServer::UpdateWiimoteMapping() { - sf::Packet spac; - spac << (MessageId)NP_MSG_WIIMOTE_MAPPING; - for (int i = 0; i < 4; i++) - spac << m_wiimote_map[i]; - SendToClients(spac); + Packet opacket; + opacket.W((MessageId)NP_MSG_WIIMOTE_MAPPING); + opacket.DoArray(m_wiimote_map, 4); + SendToClients(opacket); } // called from ---GUI--- thread and ---NETPLAY--- thread @@ -305,39 +292,60 @@ void NetPlayServer::AdjustPadBufferSize(unsigned int size) m_target_buffer_size = size; // tell clients to change buffer size - sf::Packet spac; - spac << (MessageId)NP_MSG_PAD_BUFFER; - spac << (u32)m_target_buffer_size; - - SendToClients(spac); + Packet opacket; + opacket.W((MessageId)NP_MSG_PAD_BUFFER); + opacket.W(m_target_buffer_size); + SendToClients(opacket); } // called from ---NETPLAY--- thread -unsigned int NetPlayServer::OnData(sf::Packet& packet, sf::SocketTCP& socket) +void NetPlayServer::OnData(PlayerId pid, Packet&& packet) { - MessageId mid; - packet >> mid; + ENetPeer* peer = &m_host->peers[pid]; + if (pid >= m_players.size()) + m_players.resize(pid + 1); + Client& player = m_players[pid]; + if (!player.connected) + { + // new client + MessageId error = OnConnect(pid, packet); + if (error) + { + Packet opacket; + opacket.W(error); + opacket.W((PlayerId)0); + ENetUtil::SendPacket(peer, opacket); + enet_peer_disconnect_later(peer, 0); + } + else + { + player.connected = true; + m_num_players++; + } + return; + } - // don't need lock because this is the only thread that modifies the players - // only need locks for writes to m_players in this thread - Client& player = m_players[socket]; + MessageId mid; + packet.Do(mid); + if (packet.failure) + return OnDisconnect(pid); switch (mid) { case NP_MSG_CHAT_MESSAGE : { std::string msg; - packet >> msg; + packet.Do(msg); + if (packet.failure) + return OnDisconnect(pid); // send msg to other clients - sf::Packet spac; - spac << (MessageId)NP_MSG_CHAT_MESSAGE; - spac << player.pid; - spac << msg; - - { - SendToClients(spac, player.pid); - } + Packet opacket; + opacket.W((MessageId)NP_MSG_CHAT_MESSAGE); + opacket.W(pid); + opacket.W(msg); + + SendToClientsOnThread(opacket, pid); } break; @@ -347,21 +355,21 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, sf::SocketTCP& socket) if (player.current_game != m_current_game) break; - PadMapping map = 0; - int hi, lo; - packet >> map >> hi >> lo; + PadMapping map; + u32 hi, lo; + packet.Do(map); + packet.Do(hi); + packet.Do(lo); + if (packet.failure) + return OnDisconnect(pid); // If the data is not from the correct player, // then disconnect them. - if (m_pad_map[map] != player.pid) - return 1; - - // Relay to clients - sf::Packet spac; - spac << (MessageId)NP_MSG_PAD_DATA; - spac << map << hi << lo; + if (m_pad_map[map] != pid) + return OnDisconnect(pid); - SendToClients(spac, player.pid); + // Relay to clients + SendToClients(packet, pid); } break; @@ -371,32 +379,17 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, sf::SocketTCP& socket) if (player.current_game != m_current_game) break; - PadMapping map = 0; - u8 size; - packet >> map >> size; - u8* data = new u8[size]; - for (unsigned int i = 0; i < size; ++i) - packet >> data[i]; - + PadMapping map; + NetWiimote nw; + packet.Do(map); + packet.Do(nw); // If the data is not from the correct player, // then disconnect them. - if (m_wiimote_map[map] != player.pid) - { - delete[] data; - return 1; - } + if (packet.failure || m_wiimote_map[map] != pid) + return OnDisconnect(pid); // relay to clients - sf::Packet spac; - spac << (MessageId)NP_MSG_WIIMOTE_DATA; - spac << map; - spac << size; - for (unsigned int i = 0; i < size; ++i) - spac << data[i]; - - delete[] data; - - SendToClients(spac, player.pid); + SendToClients(packet, pid); } break; @@ -404,57 +397,47 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, sf::SocketTCP& socket) { const u32 ping = m_ping_timer.GetTimeElapsed(); u32 ping_key = 0; - packet >> ping_key; + packet.Do(ping_key); + if (packet.failure) + return OnDisconnect(pid); if (m_ping_key == ping_key) player.ping = ping; - sf::Packet spac; - spac << (MessageId)NP_MSG_PLAYER_PING_DATA; - spac << player.pid; - spac << player.ping; + Packet opacket; + opacket.W((MessageId)NP_MSG_PLAYER_PING_DATA); + opacket.W(pid); + opacket.W(player.ping); - SendToClients(spac); + SendToClientsOnThread(opacket); } break; case NP_MSG_START_GAME : { - packet >> player.current_game; + packet.Do(player.current_game); + if (packet.failure) + return OnDisconnect(pid); } break; case NP_MSG_STOP_GAME: { // tell clients to stop game - sf::Packet spac; - spac << (MessageId)NP_MSG_STOP_GAME; + Packet opacket; + opacket.W((MessageId)NP_MSG_STOP_GAME); - SendToClients(spac); + SendToClientsOnThread(opacket); m_is_running = false; } break; default : - PanicAlertT("Unknown message with id:%d received from player:%d Kicking player!", mid, player.pid); + PanicAlertT("Unknown message with id:%d received from player:%d Kicking player!", mid, pid); // unknown message, kick the client - return 1; - break; + return OnDisconnect(pid); } - - return 0; -} - -// called from ---GUI--- thread / and ---NETPLAY--- thread -void NetPlayServer::SendChatMessage(const std::string& msg) -{ - sf::Packet spac; - spac << (MessageId)NP_MSG_CHAT_MESSAGE; - spac << (PlayerId)0; // server id always 0 - spac << msg; - - SendToClients(spac); } // called from ---GUI--- thread @@ -465,11 +448,10 @@ bool NetPlayServer::ChangeGame(const std::string &game) m_selected_game = game; // send changed game to clients - sf::Packet spac; - spac << (MessageId)NP_MSG_CHANGE_GAME; - spac << game; - - SendToClients(spac); + Packet opacket; + opacket.W((MessageId)NP_MSG_CHANGE_GAME); + opacket.W(game); + SendToClients(opacket); return true; } @@ -483,24 +465,23 @@ void NetPlayServer::SetNetSettings(const NetSettings &settings) // called from ---GUI--- thread bool NetPlayServer::StartGame(const std::string &path) { - std::lock_guard lk(m_crit); m_current_game = Common::Timer::GetTimeMs(); // no change, just update with clients AdjustPadBufferSize(m_target_buffer_size); // tell clients to start game - sf::Packet spac; - spac << (MessageId)NP_MSG_START_GAME; - spac << m_current_game; - spac << m_settings.m_CPUthread; - spac << m_settings.m_DSPEnableJIT; - spac << m_settings.m_DSPHLE; - spac << m_settings.m_WriteToMemcard; - spac << m_settings.m_EXIDevice[0]; - spac << m_settings.m_EXIDevice[1]; - - SendToClients(spac); + Packet opacket; + opacket.W((MessageId)NP_MSG_START_GAME); + opacket.W(m_current_game); + opacket.W(m_settings.m_CPUthread); + opacket.W(m_settings.m_DSPEnableJIT); + opacket.W(m_settings.m_DSPHLE); + opacket.W(m_settings.m_WriteToMemcard); + opacket.W((int) m_settings.m_EXIDevice[0]); + opacket.W((int) m_settings.m_EXIDevice[1]); + + SendToClients(opacket); m_is_running = true; @@ -508,13 +489,30 @@ bool NetPlayServer::StartGame(const std::string &path) } // called from multiple threads -void NetPlayServer::SendToClients(sf::Packet& packet, const PlayerId skip_pid) +void NetPlayServer::SendToClients(Packet& packet, const PlayerId skip_pid) { - std::lock_guard lk(m_crit); - std::map::iterator - i = m_players.begin(), - e = m_players.end(); - for ( ; i!=e; ++i) - if (i->second.pid && (i->second.pid != skip_pid)) - i->second.socket.Send(packet); + m_queue.Push(std::make_pair(std::move(packet), skip_pid)); + ENetUtil::Wakeup(m_host); +} + + +// called from ---NETPLAY--- thread +void NetPlayServer::SendToClientsOnThread(const Packet& packet, const PlayerId skip_pid) +{ + ENetPacket* epacket = NULL; + for (size_t pid = 0; pid < m_players.size(); pid++) + { + if (pid != skip_pid && + m_players[pid].connected) + { + if (!epacket) + epacket = enet_packet_create((u8*) packet.vec->data(), packet.vec->size(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(&m_host->peers[pid], 0, epacket); + } + } +} + +u16 NetPlayServer::GetPort() +{ + return m_host->address.port; } diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 8794f0e5d401..9f05c5e6354f 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -10,25 +10,21 @@ #include "Thread.h" #include "Timer.h" -#include - #include "NetPlayProto.h" +#include "enet/enet.h" +#include "FifoQueue.h" #include -#include -#include -#include class NetPlayServer { public: void ThreadFunc(); - NetPlayServer(const u16 port); + NetPlayServer(); ~NetPlayServer(); bool ChangeGame(const std::string& game); - void SendChatMessage(const std::string& msg); void SetNetSettings(const NetSettings &settings); @@ -44,23 +40,25 @@ class NetPlayServer bool m_IsConnected; + u16 GetPort(); private: class Client { public: - PlayerId pid; + Client() { connected = false; } std::string name; std::string revision; - sf::SocketTCP socket; u32 ping; u32 current_game; + bool connected; }; - void SendToClients(sf::Packet& packet, const PlayerId skip_pid = 0); - unsigned int OnConnect(sf::SocketTCP& socket); - unsigned int OnDisconnect(sf::SocketTCP& socket); - unsigned int OnData(sf::Packet& packet, sf::SocketTCP& socket); + void SendToClients(Packet& packet, const PlayerId skip_pid = -1); + void SendToClientsOnThread(const Packet& packet, const PlayerId skip_pid = -1); + MessageId OnConnect(PlayerId pid, Packet& hello); + void OnDisconnect(PlayerId pid); + void OnData(PlayerId pid, Packet&& packet); void UpdatePadMapping(); void UpdateWiimoteMapping(); @@ -72,19 +70,21 @@ class NetPlayServer u32 m_ping_key; bool m_update_pings; u32 m_current_game; - unsigned int m_target_buffer_size; + u32 m_target_buffer_size; PadMapping m_pad_map[4]; PadMapping m_wiimote_map[4]; - std::map m_players; + std::vector m_players; + unsigned m_num_players; + // only protects m_selected_game std::recursive_mutex m_crit; std::string m_selected_game; - sf::SocketTCP m_socket; - std::thread m_thread; - sf::Selector m_selector; + ENetHost* m_host; + std::thread m_thread; + Common::FifoQueue, false> m_queue; }; #endif diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 056994f0c54c..9bfb236f15fd 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -545,7 +545,7 @@ void NetPlay::StartHosting(std::string id, wxWindow* parent) return; } - netplay_server.reset(new NetPlayServer(2626)); + netplay_server.reset(new NetPlayServer()); netplay_server->ChangeGame(id); netplay_server->AdjustPadBufferSize(INITIAL_PAD_BUFFER_SIZE); if (!netplay_server->m_IsConnected) @@ -555,7 +555,7 @@ void NetPlay::StartHosting(std::string id, wxWindow* parent) return; } std::string nickname = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayNickname; - netplay_client.reset(new NetPlayClient("127.0.0.1", 2626, nickname)); + netplay_client.reset(new NetPlayClient("127.0.0.1", netplay_server->GetPort(), nickname)); if (!netplay_client->m_IsConnected) { wxMessageBox(_("Failed to connect to localhost. This shouldn't happen..."), _("Error"), wxOK, parent); From a972d95923ee5859ae72a58d0f9afdda6fd8117c Mon Sep 17 00:00:00 2001 From: comex Date: Fri, 4 Oct 2013 23:09:48 -0400 Subject: [PATCH 014/202] Add the ability to change name from within the main Netplay dialog, since it can't be done elsewhere anymore. --- Source/Core/Core/Src/NetPlayClient.cpp | 33 +++++++++++++++++++++++++ Source/Core/Core/Src/NetPlayClient.h | 1 + Source/Core/Core/Src/NetPlayProto.h | 1 + Source/Core/Core/Src/NetPlayServer.cpp | 18 ++++++++++++++ Source/Core/DolphinWX/Src/NetWindow.cpp | 29 +++++++++++++++++++--- Source/Core/DolphinWX/Src/NetWindow.h | 3 +++ 6 files changed, 81 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 1af28d9e9bdd..2e5a68d6bf02 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -264,6 +264,25 @@ void NetPlayClient::OnData(Packet&& packet) } break; + case NP_MSG_CHANGE_NAME : + { + PlayerId pid; + std::string name; + packet.Do(pid); + packet.Do(name); + if (packet.failure) + return OnDisconnect(); + + { + std::lock_guard lk(m_crit); + Player& player = m_players[pid]; + player.name = name; + } + + m_dialog->Update(); + } + break; + case NP_MSG_PAD_MAPPING : { packet.DoArray(m_pad_map, 4); @@ -523,6 +542,20 @@ void NetPlayClient::SendChatMessage(const std::string& msg) SendPacket(packet); } +// called from ---GUI--- thread +void NetPlayClient::ChangeName(const std::string& name) +{ + { + std::lock_guard lk(m_crit); + m_local_player->name = name; + } + Packet packet; + packet.W((MessageId)NP_MSG_CHANGE_NAME); + packet.W(name); + + SendPacket(packet); +} + // called from ---CPU--- thread void NetPlayClient::SendPadState(const PadMapping in_game_pad, const NetPad& np) { diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 9ce78400e2be..da2de6240697 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -86,6 +86,7 @@ class NetPlayClient void Stop(); bool ChangeGame(const std::string& game); void SendChatMessage(const std::string& msg); + void ChangeName(const std::string& name); // Send and receive pads values bool WiimoteUpdate(int _number, u8* data, const u8 size); diff --git a/Source/Core/Core/Src/NetPlayProto.h b/Source/Core/Core/Src/NetPlayProto.h index f3d4363e8413..16181150b57b 100644 --- a/Source/Core/Core/Src/NetPlayProto.h +++ b/Source/Core/Core/Src/NetPlayProto.h @@ -39,6 +39,7 @@ enum NP_MSG_PLAYER_LEAVE = 0x11, NP_MSG_CHAT_MESSAGE = 0x30, + NP_MSG_CHANGE_NAME = 0x31, NP_MSG_PAD_DATA = 0x60, NP_MSG_PAD_MAPPING = 0x61, diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index d6d00fa70ca1..66a8da6fc98f 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -349,6 +349,24 @@ void NetPlayServer::OnData(PlayerId pid, Packet&& packet) } break; + case NP_MSG_CHANGE_NAME: + { + std::string name; + packet.Do(name); + if (packet.failure) + return OnDisconnect(pid); + + player.name = name; + Packet opacket; + + opacket.W((MessageId)NP_MSG_CHANGE_NAME); + opacket.W(pid); + opacket.W(name); + + SendToClientsOnThread(opacket, pid); + } + break; + case NP_MSG_PAD_DATA : { // if this is pad data from the last game still being received, ignore it diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 9bfb236f15fd..6544eb6ab59a 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -37,12 +37,18 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const wxPanel* const panel = new wxPanel(this); // top crap - m_game_label = new wxStaticText(panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxBU_LEFT); + m_game_label = new wxStaticText(panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize); UpdateGameName(); - + // middle crap // chat + wxBoxSizer* const nickname_szr = new wxBoxSizer(wxHORIZONTAL); + nickname_szr->Add(new wxStaticText(panel, wxID_ANY, _("Nickname: ")), 0, wxCENTER); + m_name_text = new wxTextCtrl(panel, wxID_ANY, StrToWxStr(SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayNickname)); + m_name_text->Bind(wxEVT_KILL_FOCUS, &NetPlayDiag::OnDefocusName, this); + nickname_szr->Add(m_name_text, 1, wxCENTER); + m_chat_text = new wxTextCtrl(panel, wxID_ANY, wxEmptyString , wxDefaultPosition, wxDefaultSize, wxTE_READONLY | wxTE_MULTILINE); @@ -58,12 +64,14 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const chat_msg_szr->Add(chat_msg_btn, 0); wxStaticBoxSizer* const chat_szr = new wxStaticBoxSizer(wxVERTICAL, panel, _("Chat")); + chat_szr->Add(nickname_szr, 0, wxEXPAND | wxBOTTOM, 5); chat_szr->Add(m_chat_text, 1, wxEXPAND); chat_szr->Add(chat_msg_szr, 0, wxEXPAND | wxTOP, 5); m_player_lbox = new wxListBox(panel, wxID_ANY, wxDefaultPosition, wxSize(256, -1)); wxStaticBoxSizer* const player_szr = new wxStaticBoxSizer(wxVERTICAL, panel, _("Players")); + player_szr->Add(m_player_lbox, 1, wxEXPAND); // player list if (is_hosting) @@ -327,6 +335,19 @@ void NetPlayDiag::OnConfigPads(wxCommandEvent&) netplay_server->SetWiimoteMapping(wiimotemapping); } +void NetPlayDiag::OnDefocusName(wxFocusEvent&) +{ + std::string name = StripSpaces(WxStrToStr(m_name_text->GetValue())); + std::string* cur = &SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayNickname; + if (*cur != name) + { + *cur = name; + SConfig::GetInstance().SaveSettings(); + netplay_client->ChangeName(name); + Update(); + } +} + bool NetPlayDiag::IsRecording() { return m_record_chkbox->GetValue(); @@ -342,7 +363,7 @@ ConnectDiag::ConnectDiag(wxWindow* parent) // focus and select all m_HostCtrl->SetFocus(); m_HostCtrl->SetSelection(-1, -1); - m_HostCtrl->Bind(wxEVT_COMMAND_TEXT_UPDATED, &ConnectDiag::OnChange, this); + m_HostCtrl->Bind(wxEVT_TEXT, &ConnectDiag::OnChange, this); wxBoxSizer* sizerHost = new wxBoxSizer(wxHORIZONTAL); sizerHost->Add(hostLabel, 0, wxLEFT, 5); sizerHost->Add(m_HostCtrl, 1, wxEXPAND | wxLEFT | wxRIGHT, 5); @@ -425,7 +446,7 @@ bool ConnectDiag::IsHostOk() ConnectDiag::~ConnectDiag() { - SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayHost = WxStrToStr(m_HostCtrl->GetValue()); + SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayHost = StripSpaces(WxStrToStr(m_HostCtrl->GetValue())); SConfig::GetInstance().SaveSettings(); } diff --git a/Source/Core/DolphinWX/Src/NetWindow.h b/Source/Core/DolphinWX/Src/NetWindow.h index 769934d70354..d2b644429cb0 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.h +++ b/Source/Core/DolphinWX/Src/NetWindow.h @@ -68,13 +68,16 @@ class NetPlayDiag : public wxFrame, public NetPlayUI void OnThread(wxCommandEvent& event); void OnAdjustBuffer(wxCommandEvent& event); void OnConfigPads(wxCommandEvent& event); + void OnDefocusName(wxFocusEvent& event); void GetNetSettings(NetSettings &settings); + wxTextCtrl* m_name_text; wxListBox* m_player_lbox; wxTextCtrl* m_chat_text; wxTextCtrl* m_chat_msg_text; wxCheckBox* m_memcard_write; wxCheckBox* m_record_chkbox; + wxStaticText* m_ip_label; std::string m_selected_game; wxStaticText* m_game_label; From a9d96af4583450b8e4d21311a4ced04d29d61346 Mon Sep 17 00:00:00 2001 From: comex Date: Sat, 5 Oct 2013 01:07:06 -0400 Subject: [PATCH 015/202] Add STUN support. --- Source/Core/Core/Src/NetPlayClient.cpp | 1 - Source/Core/Core/Src/NetPlayServer.cpp | 74 ++++++++++++++++++++++++- Source/Core/Core/Src/NetPlayServer.h | 25 +++++++++ Source/Core/DolphinWX/Src/NetWindow.cpp | 70 ++++++++++++++++++++++- Source/Core/DolphinWX/Src/NetWindow.h | 6 +- 5 files changed, 172 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 2e5a68d6bf02..028617ed1dca 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -177,7 +177,6 @@ NetPlayClient::NetPlayClient(const std::string& address, const u16 port, const s m_local_player = &m_players[m_pid]; //PanicAlertT("Connection successful: assigned player id: %d", m_pid); - printf("Connected!\n"); m_IsConnected = true; } } diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 66a8da6fc98f..08b221a57f2c 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -8,6 +8,8 @@ #define MAX_CLIENTS 200 +NetPlayServer* NetPlayServer::s_instance; + NetPlayServer::~NetPlayServer() { if (m_host) @@ -25,6 +27,8 @@ NetPlayServer::NetPlayServer() m_IsConnected = false; m_is_running = false; m_num_players = 0; + m_dialog = NULL; + s_instance = this; memset(m_pad_map, -1, sizeof(m_pad_map)); memset(m_wiimote_map, -1, sizeof(m_wiimote_map)); @@ -42,7 +46,9 @@ NetPlayServer::NetPlayServer() if (m_host == NULL) return; - m_host->intercept = ENetUtil::InterceptCallback; + RetrySTUN(); + + m_host->intercept = InterceptCallback; m_do_loop = true; m_target_buffer_size = 20; @@ -530,7 +536,73 @@ void NetPlayServer::SendToClientsOnThread(const Packet& packet, const PlayerId s } } +// called from ---GUI--- thread u16 NetPlayServer::GetPort() { return m_host->address.port; } + +// called from ---GUI--- thread +std::pair NetPlayServer::GetHost() +{ + auto state = Common::AtomicLoadAcquire(m_stun_state); + return std::make_pair(state, m_address_str); +} + +void NetPlayServer::RetrySTUN() +{ + // no std::to_string on Android + char buf[64]; + sprintf(buf, "%d", (int) GetPort()); + m_address_str = buf; + m_stun_state = STILL_RUNNING; + std::vector servers; + //servers.push_back("dolphin-emu.org"); + servers.push_back("stunserver.org"); + m_stun_client = STUNClient(m_host->socket, std::move(servers)); +} + +// called from ---NETPLAY--- thread +int NetPlayServer::InterceptCallback(ENetHost* host, ENetEvent* event) +{ + return s_instance->Intercept(event); +} + +int NetPlayServer::Intercept(ENetEvent* event) +{ + if (m_stun_client.m_Status == STUNClient::Waiting) + { + bool result = m_stun_client.ReceivedPacket(m_host->receivedData, m_host->receivedDataLength, &m_host->receivedAddress); + m_stun_client.Ping(); + if (m_stun_client.m_Status == STUNClient::Error || + m_stun_client.m_Status == STUNClient::Timeout) + { + Common::AtomicStoreRelease(m_stun_state, STUN_FAILED); + if (m_dialog) + m_dialog->Update(); + } + else if (m_stun_client.m_Status == STUNClient::Ok) + { + const ENetAddress* addr = &m_stun_client.m_MyAddress; + char buf[64]; + if (enet_address_get_host_ip(addr, buf, sizeof(buf)) < 0) + strcpy(buf, "???"); + sprintf(buf + strlen(buf), ":%d", (int) addr->port); + m_address_str = buf; + Common::AtomicStoreRelease(m_stun_state, STUN_OK); + if (m_dialog) + m_dialog->Update(); + } + if (result) + { + event->type = (ENetEventType) 42; + return 1; + } + } + return ENetUtil::InterceptCallback(m_host, event); +} + +void NetPlayServer::SetDialog(NetPlayUI* dialog) +{ + m_dialog = dialog; +} diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 9f05c5e6354f..8041e864a45d 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -13,9 +13,12 @@ #include "NetPlayProto.h" #include "enet/enet.h" #include "FifoQueue.h" +#include "STUNClient.h" #include +class NetPlayUI; + class NetPlayServer { public: @@ -41,6 +44,18 @@ class NetPlayServer bool m_IsConnected; u16 GetPort(); + + void SetDialog(NetPlayUI* dialog); + + enum STUNState + { + STILL_RUNNING, + STUN_OK, + STUN_FAILED + }; + std::pair GetHost(); + void RetrySTUN(); + private: class Client { @@ -62,6 +77,9 @@ class NetPlayServer void UpdatePadMapping(); void UpdateWiimoteMapping(); + static int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event); + int Intercept(ENetEvent* event); + NetSettings m_settings; bool m_is_running; @@ -85,6 +103,13 @@ class NetPlayServer ENetHost* m_host; std::thread m_thread; Common::FifoQueue, false> m_queue; + volatile STUNState m_stun_state; + std::string m_address_str; + STUNClient m_stun_client; + NetPlayUI* m_dialog; + + // this is for the InterceptCallback; replace with a map if you care + static NetPlayServer* s_instance; }; #endif diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 6544eb6ab59a..0236350ff342 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -16,6 +16,8 @@ #include #include +#include + #define NETPLAY_TITLEBAR "Dolphin NetPlay" #define INITIAL_PAD_BUFFER_SIZE 20 @@ -32,6 +34,7 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const : wxFrame(parent, wxID_ANY, wxT(NETPLAY_TITLEBAR), wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_STYLE | wxTAB_TRAVERSAL) , m_selected_game(game) , m_start_btn(NULL) + , m_is_hosting(is_hosting) { npd = this; wxPanel* const panel = new wxPanel(this); @@ -72,6 +75,22 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const wxStaticBoxSizer* const player_szr = new wxStaticBoxSizer(wxVERTICAL, panel, _("Players")); + if (is_hosting) + { + wxBoxSizer* const host_szr = new wxBoxSizer(wxHORIZONTAL); + host_szr->Add(new wxStaticText(panel, wxID_ANY, _("Host:")), 0, wxCENTER); + // The initial label is for sizing... + m_host_label = new wxStaticText(panel, wxID_ANY, "555.555.555.555:5555", wxDefaultPosition, wxDefaultSize, wxST_NO_AUTORESIZE | wxALIGN_LEFT); + // Update() should fix this immediately. + m_host_label->SetLabel(_("")); + host_szr->Add(m_host_label, 1, wxCENTER); + m_host_copy_btn = new wxButton(panel, wxID_ANY, _("Copy")); + m_host_copy_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &NetPlayDiag::OnCopyIP, this); + m_host_copy_btn->Disable(); + host_szr->Add(m_host_copy_btn, 0, wxLEFT | wxCENTER, 5); + player_szr->Add(host_szr, 0, wxEXPAND | wxBOTTOM, 5); + } + player_szr->Add(m_player_lbox, 1, wxEXPAND); // player list if (is_hosting) @@ -265,6 +284,34 @@ void NetPlayDiag::OnQuit(wxCommandEvent&) // update gui void NetPlayDiag::OnThread(wxCommandEvent& event) { + if (m_is_hosting) + { + auto info = netplay_server->GetHost(); + switch (info.first) + { + case NetPlayServer::STILL_RUNNING: + m_host_label->SetForegroundColour(*wxLIGHT_GREY); + m_host_label->SetLabel(wxString::Format(_("(local ip):%s"), StrToWxStr(info.second))); + m_host_copy_btn->SetLabel(_("Copy")); + m_host_copy_btn->Disable(); + break; + case NetPlayServer::STUN_OK: + m_host_label->SetForegroundColour(*wxBLACK); + m_host_label->SetLabel(StrToWxStr(info.second)); + m_host_copy_btn->SetLabel(_("Copy")); + m_host_copy_btn->Enable(); + m_host_copy_btn_is_retry = false; + break; + case NetPlayServer::STUN_FAILED: + m_host_label->SetForegroundColour(*wxBLACK); + m_host_label->SetLabel(wxString::Format(_("(local ip):%s"), StrToWxStr(info.second))); + m_host_copy_btn->SetLabel(_("Retry")); + m_host_copy_btn->Enable(); + m_host_copy_btn_is_retry = true; + break; + } + } + // player list m_playerids.clear(); std::string tmps; @@ -348,6 +395,24 @@ void NetPlayDiag::OnDefocusName(wxFocusEvent&) } } +void NetPlayDiag::OnCopyIP(wxCommandEvent&) +{ + if (m_host_copy_btn_is_retry) + { + // nevermind. + netplay_server->RetrySTUN(); + Update(); + } + else + { + if (wxTheClipboard->Open()) + { + wxTheClipboard->SetData(new wxTextDataObject(m_host_label->GetLabel())); + wxTheClipboard->Close(); + } + } +} + bool NetPlayDiag::IsRecording() { return m_record_chkbox->GetValue(); @@ -584,5 +649,8 @@ void NetPlay::StartHosting(std::string id, wxWindow* parent) netplay_server.reset(); return; } - netplay_client->SetDialog(new NetPlayDiag(parent, id, true)); + auto diag = new NetPlayDiag(parent, id, true); + netplay_client->SetDialog(diag); + netplay_server->SetDialog(diag); + diag->Update(); } diff --git a/Source/Core/DolphinWX/Src/NetWindow.h b/Source/Core/DolphinWX/Src/NetWindow.h index d2b644429cb0..f701aa31112a 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.h +++ b/Source/Core/DolphinWX/Src/NetWindow.h @@ -69,6 +69,7 @@ class NetPlayDiag : public wxFrame, public NetPlayUI void OnAdjustBuffer(wxCommandEvent& event); void OnConfigPads(wxCommandEvent& event); void OnDefocusName(wxFocusEvent& event); + void OnCopyIP(wxCommandEvent& event); void GetNetSettings(NetSettings &settings); wxTextCtrl* m_name_text; @@ -77,11 +78,14 @@ class NetPlayDiag : public wxFrame, public NetPlayUI wxTextCtrl* m_chat_msg_text; wxCheckBox* m_memcard_write; wxCheckBox* m_record_chkbox; - wxStaticText* m_ip_label; + wxStaticText* m_host_label; + wxButton* m_host_copy_btn; + bool m_host_copy_btn_is_retry; std::string m_selected_game; wxStaticText* m_game_label; wxButton* m_start_btn; + bool m_is_hosting; std::vector m_playerids; From 2e6136fb4a1e4d2172a1bf30ab3f4bc9873af652 Mon Sep 17 00:00:00 2001 From: comex Date: Sat, 5 Oct 2013 01:30:44 -0400 Subject: [PATCH 016/202] Put the entries on the player list on two lines. This should really be improved, but for the time being, it was impossible for me to see the ping (on OS X). --- Source/Core/Core/Src/NetPlayClient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 028617ed1dca..2c009026b7f0 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -494,7 +494,7 @@ void NetPlayClient::GetPlayerList(std::string& list, std::vector& pid_list) for ( ; i!=e; ++i) { const Player *player = &(i->second); - ss << player->name << "[" << (int)player->pid << "] : " << player->revision << " | "; + ss << player->name << "[" << (int)player->pid << "] : " << player->revision << "\n | "; for (unsigned int j = 0; j < 4; j++) { if (m_pad_map[j] == player->pid) From 6916f7c3b1ec8358084103b739e408a49c3a548c Mon Sep 17 00:00:00 2001 From: comex Date: Sat, 5 Oct 2013 01:32:53 -0400 Subject: [PATCH 017/202] Bump the Netplay window size. The old size was too small to fit the new stuff. --- Source/Core/DolphinWX/Src/NetWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 0236350ff342..0391006bd0d9 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -140,7 +140,7 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const panel->SetSizerAndFit(main_szr); main_szr->SetSizeHints(this); - SetSize(512, 512-128); + SetSize(650, 512-128); Center(); Show(); From 7d6949b458eb2edef92749174858ab7351cdebbf Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 8 Oct 2013 00:45:57 -0400 Subject: [PATCH 018/202] Replace STUN with Traversal{Client,Server}. The key difference is that the server sends a packet to the client (to be dropped) before the client tries to send to the server. This allows it to work in various situations where pure STUN doesn't. --- Source/Core/Common/CMakeLists.txt | 3 +- Source/Core/Common/Common.vcxproj | 5 +- Source/Core/Common/Common.vcxproj.filters | 5 +- Source/Core/Common/Src/STUNClient.cpp | 271 -------------- Source/Core/Common/Src/STUNClient.h | 39 -- Source/Core/Common/Src/TraversalClient.cpp | 416 +++++++++++++++++++++ Source/Core/Common/Src/TraversalClient.h | 117 ++++++ Source/Core/Common/Src/TraversalProto.h | 86 +++++ Source/Core/Common/Src/TraversalServer.cpp | 412 ++++++++++++++++++++ Source/Core/Core/Src/ConfigManager.cpp | 2 + Source/Core/Core/Src/CoreParameter.h | 1 + Source/Core/Core/Src/NetPlayClient.cpp | 279 +++++++------- Source/Core/Core/Src/NetPlayClient.h | 39 +- Source/Core/Core/Src/NetPlayServer.cpp | 200 +++------- Source/Core/Core/Src/NetPlayServer.h | 37 +- Source/Core/DolphinWX/Src/NetWindow.cpp | 106 ++++-- Source/Core/DolphinWX/Src/NetWindow.h | 4 +- 17 files changed, 1333 insertions(+), 689 deletions(-) delete mode 100644 Source/Core/Common/Src/STUNClient.cpp delete mode 100644 Source/Core/Common/Src/STUNClient.h create mode 100644 Source/Core/Common/Src/TraversalClient.cpp create mode 100644 Source/Core/Common/Src/TraversalClient.h create mode 100644 Source/Core/Common/Src/TraversalProto.h create mode 100644 Source/Core/Common/Src/TraversalServer.cpp diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index dc8ffdc94a68..477b176513ce 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -16,7 +16,7 @@ set(SRCS Src/BreakPoints.cpp Src/SettingsHandler.cpp Src/SDCardUtil.cpp Src/StringUtil.cpp - Src/STUNClient.cpp + Src/TraversalClient.cpp Src/SymbolDB.cpp Src/SysConf.cpp Src/Thread.cpp @@ -53,4 +53,5 @@ endif(WIN32) enable_precompiled_headers(Src/stdafx.h Src/stdafx.cpp SRCS) add_library(common STATIC ${SRCS}) +add_executable(traversal_server Src/TraversalServer.cpp) target_link_libraries(common ${CMAKE_THREAD_LIBS_INIT}) diff --git a/Source/Core/Common/Common.vcxproj b/Source/Core/Common/Common.vcxproj index 7e06603f9cb4..0377ddd4eeb5 100644 --- a/Source/Core/Common/Common.vcxproj +++ b/Source/Core/Common/Common.vcxproj @@ -206,7 +206,7 @@ Create Create - + @@ -259,7 +259,8 @@ - + + diff --git a/Source/Core/Common/Common.vcxproj.filters b/Source/Core/Common/Common.vcxproj.filters index b66d3d774d7c..e1096f36d7c2 100644 --- a/Source/Core/Common/Common.vcxproj.filters +++ b/Source/Core/Common/Common.vcxproj.filters @@ -18,7 +18,7 @@ - + @@ -81,7 +81,8 @@ - + + diff --git a/Source/Core/Common/Src/STUNClient.cpp b/Source/Core/Common/Src/STUNClient.cpp deleted file mode 100644 index 932529723ff2..000000000000 --- a/Source/Core/Common/Src/STUNClient.cpp +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright 2013 Dolphin Emulator Project -// Licensed under GPLv2 -// Refer to the license.txt file included. - -#include "STUNClient.h" -#include "enet/time.h" -#include -#include - -struct STUNHeader -{ - u16 type; - u16 length; - u32 cookie; - u8 transactionID[12]; -}; - -struct STUNAttributeHeader -{ - u16 type; - u16 length; -}; - -struct STUNMappedAddress -{ - u8 zero; - u8 family; - u16 port; -}; - -static void GetRandomishBytes(u8 *buf, size_t size) -{ - // We don't need high quality random numbers (which might not be available), - // just non-repeating numbers! - srand(enet_time_get()); - for (size_t i = 0; i < size; i++) - buf[i] = rand() & 0xff; -} - -static bool SendSTUNMessage(ENetSocket socket, const ENetAddress *address, u8 transactionID[12]) -{ - STUNHeader header; - header.type = Common::swap16(0x0001); // Binding Request - header.length = Common::swap16((u16) 0); - header.cookie = Common::swap32(0x2112A442); - memcpy(header.transactionID, transactionID, 12); - - ENetBuffer buf; - buf.data = &header; - buf.dataLength = sizeof(header); - if (enet_socket_send(socket, address, &buf, 1) != sizeof(header)) - { - ERROR_LOG(NETPLAY, "Failed to send STUN message."); - return false; - } - return true; -} - -static bool ParseSTUNMessage(u8 *data, size_t length, ENetAddress *myAddress) -{ - // This is simple! It just has a lot of error checking. - auto header = (STUNHeader *) data; - if (Common::swap16(header->length) + sizeof(STUNHeader) != length) - { - ERROR_LOG(NETPLAY, "Received invalid STUN packet (bad length)."); - return false; - } - - std::unordered_map attributes; - u8 *ptr = (u8 *) (header + 1), *end = ptr + (length - sizeof(STUNHeader)); - while (ptr < end) - { - auto attribHeader = (STUNAttributeHeader *) ptr; - attribHeader->type = Common::swap16(attribHeader->type); - attribHeader->length = Common::swap16(attribHeader->length); - ptr += sizeof(STUNAttributeHeader); - if (attribHeader->length > end - ptr) - { - ERROR_LOG(NETPLAY, "Received invalid STUN packet (bad attribute length %u / %u).", (int) attribHeader->length, (int) (end - ptr)); - return false; - } - attributes[attribHeader->type] = attribHeader; - ptr += attribHeader->length; - } - - u16 type = Common::swap16(header->type); - if (type == 0x0111) // Binding Error Response - { - // search for a reason - auto it = attributes.find(0x0009); // ERROR-CODE - if (it == attributes.end()) - { - ERROR_LOG(NETPLAY, "Received invalid STUN packet (Binding Error Response with no ERROR-CODE)"); - return false; - } - STUNAttributeHeader *attribHeader = it->second; - if (attribHeader->length < 8) - { - ERROR_LOG(NETPLAY, "Received invalid STUN packet (bad ERROR-CODE length)"); - return false; - } - u8 *errorCodeHeader = (u8 *) (attribHeader + 1); - u32 classAndNumber = Common::swap32(*(u32 *) (errorCodeHeader + 4)); - u32 code = ((classAndNumber >> 8) & 7) * 100 + (classAndNumber & 0xff); - char *desc = (char *) (errorCodeHeader + 8); - ERROR_LOG(NETPLAY, "Received STUN Binding Error %u: \"%*s\"", code, attribHeader->length - 8, desc); - return false; - } - else if (type == 0x0101) // Binding Response - { - auto it = attributes.end(); // since MSVC can't stand decltype - bool isXor; - if ((it = attributes.find(0x0020)) != attributes.end()) - { - // XOR-MAPPED-ADDRESS - isXor = true; - } - else if((it = attributes.find(0x0001)) != attributes.end()) - { - // old-fashioned MAPPED-ADDRESS - isXor = false; - } - else - { - ERROR_LOG(NETPLAY, "Received invalid STUN packet (Binding Response with no address)"); - return false; - } - STUNAttributeHeader *attribHeader = it->second; - if (attribHeader->length < sizeof(STUNMappedAddress)) - { - ERROR_LOG(NETPLAY, "Received invalid STUN packet (bad address header)"); - return false; - } - auto mappedAddress = (STUNMappedAddress *) (attribHeader + 1); - u16 port = Common::swap16(mappedAddress->port); - if (isXor) - port ^= (Common::swap32(header->cookie) >> 16); - size_t addrLength; - if (mappedAddress->family == 0x01) // IPv4 - addrLength = 4; - else if (mappedAddress->family == 0x02) // IPV6 - addrLength = 16; - else - { - ERROR_LOG(NETPLAY, "Received invalid STUN packet (unknown family)"); - return false; - } - if (attribHeader->length != sizeof(STUNMappedAddress) + addrLength) - { - ERROR_LOG(NETPLAY, "Received invalid STUN packet (bad address header)"); - return false; - } - u8 *addrBuf = (u8 *) (mappedAddress + 1); - if (isXor) - { - u8 *pad = (u8 *) &header->cookie; - for (size_t i = 0; i < addrLength; i++) - addrBuf[i] ^= pad[i]; - } - if (mappedAddress->family == 0x01) - { - myAddress->host = *(u32 *) addrBuf; - myAddress->port = port; - return true; - } - else - { - ERROR_LOG(NETPLAY, "Received IPv6 address from STUN, but enet doesn't support IPv6 yet :("); - return false; - } - } - else - { - ERROR_LOG(NETPLAY, "Received unknown STUN packet type 0x%02x", type); - return false; - } -} - -STUNClient::STUNClient() -{ - m_Status = Error; -} - -STUNClient::STUNClient(ENetSocket socket, std::vector servers) -: m_Socket(socket), m_Servers(std::move(servers)) -{ - m_Status = Waiting; - TryNextServer(); -} - -void STUNClient::TryNextServer() -{ - while (1) - { - if (m_Servers.empty()) - { - ERROR_LOG(NETPLAY, "No more STUN servers to try."); - m_Status = Timeout; - return; - } - - const char *server = m_Servers.front().c_str(); - if (enet_address_set_host(&m_CurrentServer, server) < 0) - { - ERROR_LOG(NETPLAY, "DNS lookup of %s failed.", server); - continue; - } - m_CurrentServer.port = 3478; - - GetRandomishBytes(m_TransactionID, sizeof(m_TransactionID)); - m_Tries = 0; - DoTry(); - return; - } -} - -void STUNClient::DoTry() -{ - if (++m_Tries > 5) - { - ERROR_LOG(NETPLAY, "Timed out waiting for STUN server %s.", m_Servers.front().c_str()); - TryNextServer(); - return; - } - - m_TryStartTime = enet_time_get(); - if (!SendSTUNMessage(m_Socket, &m_CurrentServer, m_TransactionID)) - { - TryNextServer(); - } -} - -bool STUNClient::ReceivedPacket(u8 *data, size_t length, const ENetAddress* from) -{ - if (from->host != m_CurrentServer.host || from->port != m_CurrentServer.port) - return false; - if (length < sizeof(STUNHeader)) - return false; - - auto header = (STUNHeader *) data; - if (header->cookie != Common::swap32(0x2112A442)) - return false; - if (memcmp(header->transactionID, m_TransactionID, 12)) - return false; - - // At this point we assume that the packet is for us. - if (m_Status != Waiting) - { - // unnecessary packet - return true; - } - - if (ParseSTUNMessage(data, length, &m_MyAddress)) - m_Status = Ok; - else - m_Status = Error; - return true; -} - -void STUNClient::Ping() -{ - // By the way, does this macro's implementation even make sense? - enet_uint32 now = enet_time_get(); - if (ENET_TIME_DIFFERENCE(m_TryStartTime, now) >= 500) - { - m_TryStartTime = now; - DoTry(); - } -} - - diff --git a/Source/Core/Common/Src/STUNClient.h b/Source/Core/Common/Src/STUNClient.h deleted file mode 100644 index 47b92cb17961..000000000000 --- a/Source/Core/Common/Src/STUNClient.h +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2013 Dolphin Emulator Project -// Licensed under GPLv2 -// Refer to the license.txt file included. - -#pragma once - -#include "Common.h" -#include "enet/enet.h" -#include - -class STUNClient -{ -public: - enum Status - { - Waiting, - Ok, - Error, - Timeout - }; - - STUNClient(); - STUNClient(ENetSocket socket, std::vector servers); - // returns whether it was a STUN packet - bool ReceivedPacket(u8 *data, size_t length, const ENetAddress* from); - void Ping(); - ENetAddress m_MyAddress; - Status m_Status; -private: - ENetSocket m_Socket; - std::vector m_Servers; - ENetAddress m_CurrentServer; - int m_Tries; - enet_uint32 m_TryStartTime; - u8 m_TransactionID[12]; - - void TryNextServer(); - void DoTry(); -}; diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp new file mode 100644 index 000000000000..3b75ce74840d --- /dev/null +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -0,0 +1,416 @@ +// This file is public domain, in case it's useful to anyone. -comex + +#include "TraversalClient.h" +#include "enet/enet.h" +#include "NetPlayClient.h" // for ENetUtil +#include "ConfigManager.h" + +void ENetUtil::BroadcastPacket(ENetHost* host, const Packet& pac) +{ + enet_host_broadcast(host, 0, enet_packet_create((u8*) pac.vec->data(), pac.vec->size(), ENET_PACKET_FLAG_RELIABLE)); +} + +void ENetUtil::SendPacket(ENetPeer* peer, const Packet& pac) +{ + enet_peer_send(peer, 0, enet_packet_create((u8*) pac.vec->data(), pac.vec->size(), ENET_PACKET_FLAG_RELIABLE)); +} + +Packet ENetUtil::MakePacket(ENetPacket* epacket) +{ + Packet pac(PWBuffer(epacket->data, epacket->dataLength)); + enet_packet_destroy(epacket); + return pac; +} + +int ENET_CALLBACK ENetUtil::InterceptCallback(ENetHost* host, ENetEvent* event) +{ + if (host->receivedDataLength == 1) + { + event->type = (ENetEventType) 42; + return 1; + } + return 0; +} + +void ENetUtil::Wakeup(ENetHost* host) +{ + // Send ourselves a spurious message. This is hackier than it should be. + // I reported this as https://github.com/lsalzman/enet/issues/23, so + // hopefully there will be a better way to do it soon. + ENetAddress address; + if (host->address.port != 0) + address.port = host->address.port; + else + enet_socket_get_address(host->socket, &address); + address.host = 0x0100007f; // localhost + + u8 byte = 0; + ENetBuffer buf; + buf.data = &byte; + buf.dataLength = 1; + enet_socket_send(host->socket, &address, &buf, 1); +} + + +static void GetRandomishBytes(u8* buf, size_t size) +{ + // We don't need high quality random numbers (which might not be available), + // just non-repeating numbers! + srand(enet_time_get()); + for (size_t i = 0; i < size; i++) + buf[i] = rand() & 0xff; +} + +ENetHostClient::ENetHostClient(size_t peerCount, bool isTraversalClient) +{ + m_isTraversalClient = isTraversalClient; + ENetAddress addr { ENET_HOST_ANY, ENET_PORT_ANY }; + m_Host = enet_host_create( + &addr, // address + peerCount, // peerCount + 1, // channelLimit + 0, // incomingBandwidth + 0); // outgoingBandwidth + + if (!m_Host) + return; + + m_Host->intercept = ENetUtil::InterceptCallback; + + m_Client = NULL; + m_ShouldEndThread = false; + m_Thread = std::thread(std::mem_fun(&ENetHostClient::ThreadFunc), this); +} + +ENetHostClient::~ENetHostClient() +{ + Reset(); + if (m_Host) + { + RunOnThread([=]() { + m_ShouldEndThread = true; + }); + m_Thread.join(); + enet_host_destroy(m_Host); + } +} + +void ENetHostClient::RunOnThread(std::function func) +{ + m_RunQueue.Push(func); + ENetUtil::Wakeup(m_Host); +} + + +void ENetHostClient::Reset() +{ + // bleh, sync up with the thread + m_ResetEvent.Reset(); + RunOnThread([=]() { + m_ResetEvent.Set(); + }); + m_ResetEvent.Wait(); + m_Client = NULL; +} + +void ENetHostClient::ThreadFunc() +{ + Common::SetCurrentThreadName(m_isTraversalClient ? "TraversalClient thread" : "ENetHostClient thread"); + while (1) + { + while (!m_RunQueue.Empty()) + { + m_RunQueue.Front()(); + m_RunQueue.Pop(); + } + if (m_ShouldEndThread) break; + ENetEvent event; + ENetAddress address; + if (enet_socket_get_address(m_Host->socket, &address) == -1) + { + PanicAlert("enet_socket_get_address failed."); + continue; + } + int count = enet_host_service(m_Host, &event, 500); + if (count < 0) + { + PanicAlert("enet_host_service failed... do something about this."); + continue; + } + + HandleResends(); + + // Even if there was nothing, forward it as a wakeup. + if (m_Client) + m_Client->OnENetEvent(&event); + } +} + +TraversalClient::TraversalClient() +: ENetHostClient(MAX_CLIENTS + 16, true) // leave some spaces free for server full notification +{ + if (!m_Host) + return; + + Reset(); + m_State = InitFailure; + + m_Host->intercept = TraversalClient::InterceptCallback; + m_Host->compressor.destroy = (decltype(m_Host->compressor.destroy)) this; + + ReconnectToServer(); +} + +void TraversalClient::ReconnectToServer() +{ + std::string server = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayCentralServer; + server = "192.168.1.216"; // XXX + if (enet_address_set_host(&m_ServerAddress, server.c_str())) + return; + m_ServerAddress.port = 6262; + + TraversalPacket hello = {0}; + hello.type = TraversalPacketHelloFromClient; + hello.helloFromClient.protoVersion = TraversalProtoVersion; + RunOnThread([=]() { + SendPacket(hello); + }); + m_State = Connecting; +} + +u16 TraversalClient::GetPort() +{ + return m_Host->address.port; +} + +static ENetAddress MakeENetAddress(TraversalInetAddress* address) +{ + ENetAddress eaddr; + if (address->isIPV6) + { + eaddr.port = 0; // no support yet :( + } + else + { + eaddr.host = address->address[0]; + eaddr.port = ntohs(address->port); + } + return eaddr; +} + +void TraversalClient::Connect(const std::string& host) +{ + if (host.size() > sizeof(TraversalHostId)) + { + PanicAlert("host too long"); + return; + } + TraversalPacket packet = {0}; + packet.type = TraversalPacketConnectPlease; + memcpy(packet.connectPlease.hostId.data(), host.c_str(), host.size()); + m_ConnectRequestId = SendPacket(packet); + m_PendingConnect = true; +} + +int ENET_CALLBACK TraversalClient::InterceptCallback(ENetHost* host, ENetEvent* event) +{ + const ENetAddress* addr = &host->receivedAddress; + auto self = (TraversalClient*) host->compressor.destroy; + if (addr->host == self->m_ServerAddress.host && + addr->port == self->m_ServerAddress.port) + { + if (host->receivedDataLength < sizeof(TraversalPacket)) + { + ERROR_LOG(NETPLAY, "Received too-short traversal packet."); + } + else + { + self->HandleServerPacket((TraversalPacket*) host->receivedData); + event->type = (ENetEventType) 42; + return 1; + } + } + return ENetUtil::InterceptCallback(host, event); +} + +void TraversalClient::HandleServerPacket(TraversalPacket* packet) +{ + u8 ok = 1; + switch (packet->type) + { + case TraversalPacketAck: + if (!packet->ack.ok) + { + OnConnectFailure(); + break; + } + for (auto it = m_OutgoingPackets.begin(); it != m_OutgoingPackets.end(); ++it) + { + if (it->packet.requestId == packet->requestId) + { + m_OutgoingPackets.erase(it); + break; + } + } + break; + case TraversalPacketHelloFromServer: + if (m_State != Connecting) + break; + if (!packet->helloFromServer.ok) + { + OnConnectFailure(); + break; + } + m_HostId = packet->helloFromServer.yourHostId; + m_State = Connected; + if (m_Client) + m_Client->OnTraversalStateChanged(); + break; + case TraversalPacketPleaseSendPacket: + { + // security is overrated. + ENetAddress addr = MakeENetAddress(&packet->pleaseSendPacket.address); + if (addr.port != 0) + { + char message[] = "Hello from Dolphin Netplay..."; + ENetBuffer buf; + buf.data = message; + buf.dataLength = sizeof(message) - 1; + enet_socket_send(m_Host->socket, &addr, &buf, 1); + + } + else + { + // invalid IPV6 + ok = 0; + } + break; + } + case TraversalPacketConnectReady: + case TraversalPacketConnectFailed: + { + if (!m_PendingConnect || packet->connectReady.requestId != m_ConnectRequestId) + break; + + m_PendingConnect = false; + + ENetAddress addr; + if (packet->type == TraversalPacketConnectReady) + addr = MakeENetAddress(&packet->connectReady.address); + else + addr.port = 0; + + if (m_Client) + m_Client->OnConnectReady(addr); + + break; + } + default: + WARN_LOG(NETPLAY, "Received unknown packet with type %d", packet->type); + break; + } + if (packet->type != TraversalPacketAck) + { + TraversalPacket ack = {0}; + ack.type = TraversalPacketAck; + ack.requestId = packet->requestId; + ack.ack.ok = ok; + + ENetBuffer buf; + buf.data = &ack; + buf.dataLength = sizeof(ack); + if (enet_socket_send(m_Host->socket, &m_ServerAddress, &buf, 1) == -1) + OnConnectFailure(); + } +} + +void TraversalClient::OnConnectFailure() +{ + m_State = ConnectFailure; + if (m_Client) + m_Client->OnTraversalStateChanged(); +} + +void TraversalClient::ResendPacket(OutgoingPacketInfo* info) +{ + info->sendTime = enet_time_get(); + info->tries++; + ENetBuffer buf; + buf.data = &info->packet; + buf.dataLength = sizeof(info->packet); + if (enet_socket_send(m_Host->socket, &m_ServerAddress, &buf, 1) == -1) + OnConnectFailure(); +} + +void TraversalClient::HandleResends() +{ + enet_uint32 now = enet_time_get(); + for (auto it = m_OutgoingPackets.begin(); it != m_OutgoingPackets.end(); ++it) + { + if (now - it->sendTime >= (u32) (300 * it->tries)) + { + if (it->tries >= 5) + { + OnConnectFailure(); + m_OutgoingPackets.clear(); + break; + } + else + { + ResendPacket(&*it); + } + } + } + HandlePing(); +} + +void TraversalClient::HandlePing() +{ + enet_uint32 now = enet_time_get(); + if (m_State == Connected && now - m_PingTime >= 5000) + { + TraversalPacket ping = {0}; + ping.type = TraversalPacketPing; + ping.ping.hostId = m_HostId; + SendPacket(ping); + m_PingTime = now; + } +} + +TraversalRequestId TraversalClient::SendPacket(const TraversalPacket& packet) +{ + OutgoingPacketInfo info; + info.packet = packet; + GetRandomishBytes((u8*) &info.packet.requestId, sizeof(info.packet.requestId)); + info.tries = 0; + m_OutgoingPackets.push_back(info); + ResendPacket(&m_OutgoingPackets.back()); + return info.packet.requestId; +} + +void TraversalClient::Reset() +{ + ENetHostClient::Reset(); + + for (size_t i = 0; i < m_Host->peerCount; i++) + { + ENetPeer* peer = &m_Host->peers[i]; + if (peer->state != ENET_PEER_STATE_DISCONNECTED) + enet_peer_disconnect_later(peer, 0); + } + m_PendingConnect = false; +} + +std::unique_ptr g_TraversalClient; + +void EnsureTraversalClient() +{ + if (!g_TraversalClient) + { + g_TraversalClient.reset(new TraversalClient); + if (g_TraversalClient->m_State == TraversalClient::InitFailure) + { + g_TraversalClient.reset(); + } + } +} diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h new file mode 100644 index 000000000000..764611f4c906 --- /dev/null +++ b/Source/Core/Common/Src/TraversalClient.h @@ -0,0 +1,117 @@ +// This file is public domain, in case it's useful to anyone. -comex + +#pragma once + +#include "Common.h" +#include "Thread.h" +#include "FifoQueue.h" +#include "TraversalProto.h" +#include "enet/enet.h" +#include +#include + +#define MAX_CLIENTS 200 + +#include "ChunkFile.h" +namespace ENetUtil +{ + void BroadcastPacket(ENetHost* host, const Packet& pac); + void SendPacket(ENetPeer* peer, const Packet& pac); + Packet MakePacket(ENetPacket* epacket); + void Wakeup(ENetHost* host); + int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event); +} + +// Apparently nobody on the C++11 standards committee thought of +// combining two of its most prominent features: lambdas and moves. Nor +// does bind work, despite a StackOverflow answer to the contrary. Derp. +template +class CopyAsMove +{ +public: + CopyAsMove(T&& t) : m_T(std::move(t)) {} + CopyAsMove(const CopyAsMove& other) : m_T((T&&) other.m_T) {} + T& operator*() { return m_T; } +private: + T m_T; +}; + +class TraversalClientClient +{ +public: + virtual void OnENetEvent(ENetEvent*) = 0; + virtual void OnTraversalStateChanged() = 0; + virtual void OnConnectReady(ENetAddress addr) = 0; +}; + +class ENetHostClient +{ +public: + ENetHostClient(size_t peerCount, bool isTraversalClient = false); + ~ENetHostClient(); + void RunOnThread(std::function func); + void CreateThread(); + void Reset(); + + TraversalClientClient* m_Client; + ENetHost* m_Host; +protected: + virtual void HandleResends() {} +private: + void ThreadFunc(); + + Common::FifoQueue, false> m_RunQueue; + std::thread m_Thread; + // *sigh* + Common::Event m_ResetEvent; + bool m_ResetReady; + bool m_ShouldEndThread; + bool m_isTraversalClient; +}; + +class TraversalClient : public ENetHostClient +{ +public: + enum State + { + InitFailure, + Connecting, + ConnectFailure, + Connected + }; + + TraversalClient(); + void Reset(); + void Connect(const std::string& host); + void ReconnectToServer(); + u16 GetPort(); + + // will be called from thread + TraversalHostId m_HostId; + State m_State; +protected: + virtual void HandleResends(); +private: + struct OutgoingPacketInfo + { + TraversalPacket packet; + int tries; + enet_uint32 sendTime; + }; + + void HandleServerPacket(TraversalPacket* packet); + void ResendPacket(OutgoingPacketInfo* info); + TraversalRequestId SendPacket(const TraversalPacket& packet); + void OnConnectFailure(); + static int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event); + void HandlePing(); + + TraversalRequestId m_ConnectRequestId; + bool m_PendingConnect; + std::list m_OutgoingPackets; + ENetAddress m_ServerAddress; + enet_uint32 m_PingTime; +}; + +extern std::unique_ptr g_TraversalClient; +void EnsureTraversalClient(); diff --git a/Source/Core/Common/Src/TraversalProto.h b/Source/Core/Common/Src/TraversalProto.h new file mode 100644 index 000000000000..0f06dd221062 --- /dev/null +++ b/Source/Core/Common/Src/TraversalProto.h @@ -0,0 +1,86 @@ +// This file is public domain, in case it's useful to anyone. -comex + +#pragma once +#include "CommonTypes.h" +#include + +typedef std::array TraversalHostId; +typedef u64 TraversalRequestId; + +enum TraversalPacketType +{ + // [*->*] + TraversalPacketAck = 0, + // [c->s] + TraversalPacketPing = 1, + // [c->s] + TraversalPacketHelloFromClient = 2, + // [s->c] + TraversalPacketHelloFromServer = 3, + // [c->s] When connecting, first the client asks the central server... + TraversalPacketConnectPlease = 4, + // [s->c] ...who asks the game host to send a UDP packet to the + // client... (an ack implies success) + TraversalPacketPleaseSendPacket = 5, + // [s->c] ...which the central server relays back to the client. + TraversalPacketConnectReady = 6, + // [s->c] Alternately, the server might not have heard of this host. + TraversalPacketConnectFailed = 7 +}; + +enum +{ + TraversalProtoVersion = 0 +}; +#pragma pack(push, 1) +struct TraversalInetAddress +{ + u8 isIPV6; + u32 address[4]; + u16 port; +}; +struct TraversalPacket +{ + u8 type; + TraversalRequestId requestId; + union + { + struct + { + u8 ok; + } ack; + struct + { + TraversalHostId hostId; + } ping; + struct + { + u8 protoVersion; + } helloFromClient; + struct + { + u8 ok; + TraversalHostId yourHostId; + TraversalInetAddress yourAddress; // currently unused + } helloFromServer; + struct + { + TraversalHostId hostId; + } connectPlease; + struct + { + TraversalInetAddress address; + } pleaseSendPacket; + struct + { + TraversalRequestId requestId; + TraversalInetAddress address; + } connectReady; + struct + { + TraversalRequestId requestId; + } connectFailed; + }; +}; +#pragma pack(pop) + diff --git a/Source/Core/Common/Src/TraversalServer.cpp b/Source/Core/Common/Src/TraversalServer.cpp new file mode 100644 index 000000000000..b71e03722ad5 --- /dev/null +++ b/Source/Core/Common/Src/TraversalServer.cpp @@ -0,0 +1,412 @@ +// This file is public domain, in case it's useful to anyone. -comex + +// The central server implementation. + +#include "TraversalProto.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG 1 + +static u64 currentTime; + +struct OutgoingPacketInfo +{ + TraversalPacket packet; + TraversalRequestId misc; + struct sockaddr_in6 dest; + int tries; + u64 sendTime; +}; + +template +struct EvictingKey +{ + // 30 seconds + const u64 EvictTimeout = 30 * 1000000; + template + EvictingKey(T&& k) : m_K(std::forward(k)) + { + m_CreateTime = currentTime; + } + bool operator==(const EvictingKey& other) const + { + return currentTime - m_CreateTime > EvictTimeout || + currentTime - other.m_CreateTime > EvictTimeout || + m_K == other.m_K; + } + K m_K; + u32 m_CreateTime; +}; + +template +struct std::hash> +{ + size_t operator()(const EvictingKey& key) const + { + return std::hash()(key.m_K); + } +}; + +template <> +struct std::hash +{ + size_t operator()(const TraversalHostId& id) const + { + auto p = (u32*) id.data(); + return p[0] ^ ((p[1] << 13) | (p[1] >> 19)); + } +}; + +static int sock; +static int urandomFd; +static std::unordered_map< +TraversalRequestId, + OutgoingPacketInfo + > outgoingPackets; + static std::unordered_map< + EvictingKey, + std::pair + > didSendInfo; + static std::unordered_map< + EvictingKey, + TraversalInetAddress + > connectedClients; + +static TraversalInetAddress MakeInetAddress(const struct sockaddr_in6& addr) +{ + if (addr.sin6_family != AF_INET6) + { + fprintf(stderr, "bad sockaddr_in6\n"); + exit(1); + } + u32* words = (u32*) addr.sin6_addr.s6_addr; + TraversalInetAddress result = {0}; + if (words[0] == 0 && words[1] == 0 && words[2] == 0xffff0000) + { + result.isIPV6 = false; + result.address[0] = words[3]; + } + else + { + result.isIPV6 = true; + memcpy(result.address, words, sizeof(result.address)); + } + result.port = addr.sin6_port; + return result; +} + +static struct sockaddr_in6 MakeSinAddr(const TraversalInetAddress& addr) +{ + struct sockaddr_in6 result; + result.sin6_len = sizeof(result); + result.sin6_family = AF_INET6; + result.sin6_port = addr.port; + result.sin6_flowinfo = 0; + if (addr.isIPV6) + { + memcpy(&result.sin6_addr, addr.address, 16); + } + else + { + u32* words = (u32*) result.sin6_addr.s6_addr; + words[0] = 0; + words[1] = 0; + words[2] = 0xffff0000; + words[3] = addr.address[0]; + } + result.sin6_scope_id = 0; + return result; +} + +static void GetRandomBytes(void* output, size_t size) +{ + static u8 bytes[8192]; + static size_t bytesLeft = 0; + if (bytesLeft < size) + { + ssize_t rv = read(urandomFd, bytes, sizeof(bytes)); + if (rv != sizeof(bytes)) + { + perror("read from /dev/urandom"); + exit(1); + } + bytesLeft = sizeof(bytes); + } + memcpy(output, bytes + (bytesLeft -= size), size); +} + +static void GetRandomHostId(TraversalHostId* hostId) +{ + char buf[9]; + u32 num; + GetRandomBytes(&num, sizeof(num)); + sprintf(buf, "%08x", num); + memcpy(hostId->data(), buf, 8); +} + +static const char* SenderName(struct sockaddr_in6* addr) +{ + static char buf[INET6_ADDRSTRLEN + 10]; + inet_ntop(PF_INET6, &addr->sin6_addr, buf, sizeof(buf)); + sprintf(buf + strlen(buf), ":%d", ntohs(addr->sin6_port)); + return buf; +} + +static void TrySend(const void* buffer, size_t size, struct sockaddr_in6* addr) +{ +#if DEBUG + printf("-> %d %llx %s\n", ((TraversalPacket*) buffer)->type, ((TraversalPacket*) buffer)->requestId, SenderName(addr)); +#endif + if ((size_t) sendto(sock, buffer, size, 0, (struct sockaddr*) addr, sizeof(*addr)) != size) + { + perror("sendto"); + } +} + +static TraversalPacket* AllocPacket(const struct sockaddr_in6& dest, TraversalRequestId misc = 0) +{ + TraversalRequestId requestId; + GetRandomBytes(&requestId, sizeof(requestId)); + OutgoingPacketInfo* info = &outgoingPackets[requestId]; + info->dest = dest; + info->misc = misc; + info->tries = 0; + info->sendTime = currentTime; + TraversalPacket* result = &info->packet; + memset(result, 0, sizeof(*result)); + result->requestId = requestId; + return result; +} + +static void SendPacket(OutgoingPacketInfo* info) +{ + info->tries++; + info->sendTime = currentTime; + TrySend(&info->packet, sizeof(info->packet), &info->dest); +} + + +static void ResendPackets() +{ + std::vector> todoFailures; + todoFailures.clear(); + for (auto it = outgoingPackets.begin(); it != outgoingPackets.end();) + { + OutgoingPacketInfo* info = &it->second; + if (currentTime - info->sendTime >= (u64) (300000 * info->tries)) + { + if (info->tries >= 5) + { + if (info->packet.type == TraversalPacketPleaseSendPacket) + { + todoFailures.push_back(std::make_pair(info->packet.pleaseSendPacket.address, info->misc)); + } + it = outgoingPackets.erase(it); + continue; + } + else + { + SendPacket(info); + } + } + ++it; + } + + for (auto it = todoFailures.begin(); it != todoFailures.end(); ++it) + { + TraversalPacket* fail = AllocPacket(MakeSinAddr(it->first)); + fail->type = TraversalPacketConnectFailed; + fail->connectFailed.requestId = it->second; + } +} + +static void HandlePacket(TraversalPacket* packet, struct sockaddr_in6* addr) +{ +#if DEBUG + printf("<- %d %llx %s\n", packet->type, packet->requestId, SenderName(addr)); +#endif + bool packetOk = true; + switch (packet->type) + { + case TraversalPacketAck: + { + auto it = outgoingPackets.find(packet->requestId); + if (it == outgoingPackets.end()) + break; + + OutgoingPacketInfo* info = &it->second; + + if (info->packet.type == TraversalPacketPleaseSendPacket) + { + TraversalPacket* ready = AllocPacket(MakeSinAddr(info->packet.pleaseSendPacket.address)); + if (packet->ack.ok) + { + ready->type = TraversalPacketConnectReady; + ready->connectReady.requestId = info->misc; + ready->connectReady.address = MakeInetAddress(info->dest); + } + else + { + ready->type = TraversalPacketConnectFailed; + ready->connectFailed.requestId = info->misc; + } + } + + outgoingPackets.erase(it); + break; + } + case TraversalPacketPing: + { + packetOk = connectedClients.find(packet->ping.hostId) != connectedClients.end(); + break; + } + case TraversalPacketHelloFromClient: + { + u8 ok = packet->helloFromClient.protoVersion <= TraversalProtoVersion; + TraversalPacket* reply = AllocPacket(*addr); + reply->type = TraversalPacketHelloFromServer; + reply->helloFromServer.ok = ok; + if (ok) + { + TraversalHostId hostId; + TraversalInetAddress* iaddr; + // not that there is any significant change of + // duplication, but... + do + { + GetRandomHostId(&hostId); + iaddr = &connectedClients[hostId]; + } while (iaddr->port); + *iaddr = MakeInetAddress(*addr); + + reply->helloFromServer.yourAddress = *iaddr; + reply->helloFromServer.yourHostId = hostId; + } + break; + } + case TraversalPacketConnectPlease: + { + TraversalHostId& hostId = packet->connectPlease.hostId; + auto it = connectedClients.find(hostId); + if (it == connectedClients.end()) + { + TraversalPacket* reply = AllocPacket(*addr); + reply->type = TraversalPacketConnectFailed; + reply->connectFailed.requestId = packet->requestId; + } + else + { + TraversalPacket* please = AllocPacket(MakeSinAddr(it->second), packet->requestId); + please->type = TraversalPacketPleaseSendPacket; + please->pleaseSendPacket.address = MakeInetAddress(*addr); + } + break; + } + default: + fprintf(stderr, "received unknown packet type %d from %s\n", packet->type, SenderName(addr)); + } + if (packet->type != TraversalPacketAck) + { + TraversalPacket ack = {0}; + ack.type = TraversalPacketAck; + ack.requestId = packet->requestId; + ack.ack.ok = packetOk; + TrySend(&ack, sizeof(ack), addr); + } +} + +int main() +{ + int rv; + + urandomFd = open("/dev/urandom", O_RDONLY); + if (urandomFd < 0) + { + perror("open /dev/urandom"); + return 1; + } + + sock = socket(PF_INET6, SOCK_DGRAM, 0); + if (sock == -1) + { + perror("socket"); + return 1; + } + int no = 0; + rv = setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no)); + if (rv < 0) + { + perror("setsockopt IPV6_V6ONLY"); + return 1; + } + struct in6_addr any = IN6ADDR_ANY_INIT; + struct sockaddr_in6 addr; + addr.sin6_len = sizeof(addr); + addr.sin6_family = AF_INET6; + addr.sin6_port = htons(6262); + addr.sin6_flowinfo = 0; + addr.sin6_addr = any; + addr.sin6_scope_id = 0; + + rv = bind(sock, (struct sockaddr*) &addr, sizeof(addr)); + if (rv < 0) + { + perror("bind"); + return 1; + } + + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 300000; + rv = setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + if (rv < 0) + { + perror("setsockopt SO_RCVTIMEO"); + return 1; + } + + while (1) + { + struct sockaddr_in6 raddr; + socklen_t addrLen = sizeof(raddr); + TraversalPacket packet; + // note: switch to recvmmsg (yes, mmsg) if this becomes + // expensive + rv = recvfrom(sock, &packet, sizeof(packet), 0, (struct sockaddr*) &raddr, &addrLen); + if (gettimeofday(&tv, NULL) < 0) + { + perror("gettimeofday"); + exit(1); + } + currentTime = (u64) tv.tv_sec * 1000000 + tv.tv_usec; + if (rv < 0) + { + if (errno != EAGAIN) + { + perror("recvfrom"); + return 1; + } + } + else if ((size_t) rv < sizeof(packet)) + { + fprintf(stderr, "received short packet from %s\n", SenderName(&raddr)); + } + else + { + HandlePacket(&packet, &raddr); + } + ResendPackets(); + } +} diff --git a/Source/Core/Core/Src/ConfigManager.cpp b/Source/Core/Core/Src/ConfigManager.cpp index 4e69f8cee785..4ba0e2430e03 100644 --- a/Source/Core/Core/Src/ConfigManager.cpp +++ b/Source/Core/Core/Src/ConfigManager.cpp @@ -192,6 +192,7 @@ void SConfig::SaveSettings() ini.Set("Interface", "ThemeName40", m_LocalCoreStartupParameter.theme_name); ini.Set("NetPlay", "LastHost", m_LocalCoreStartupParameter.strNetplayHost); ini.Set("NetPlay", "Nickname", m_LocalCoreStartupParameter.strNetplayNickname); + ini.Set("NetPlay", "CentralServer", m_LocalCoreStartupParameter.strNetplayCentralServer); // Hotkeys for (int i = 0; i < NUM_HOTKEYS; i++) @@ -347,6 +348,7 @@ void SConfig::LoadSettings() ini.Get("Interface", "ThemeName40", &m_LocalCoreStartupParameter.theme_name, "Clean"); ini.Get("NetPlay", "LastHost", &m_LocalCoreStartupParameter.strNetplayHost, "8.8.8.8:1234"); ini.Get("NetPlay", "Nickname", &m_LocalCoreStartupParameter.strNetplayNickname, ""); + ini.Get("NetPlay", "CentralServer", &m_LocalCoreStartupParameter.strNetplayCentralServer, "dolphin-emu.org"); // Hotkeys for (int i = 0; i < NUM_HOTKEYS; i++) diff --git a/Source/Core/Core/Src/CoreParameter.h b/Source/Core/Core/Src/CoreParameter.h index bb43a6d45af2..f9940506e955 100644 --- a/Source/Core/Core/Src/CoreParameter.h +++ b/Source/Core/Core/Src/CoreParameter.h @@ -150,6 +150,7 @@ struct SCoreStartupParameter bool bConfirmStop, bHideCursor, bAutoHideCursor, bUsePanicHandlers, bOnScreenDisplayMessages; std::string theme_name; std::string strNetplayHost, strNetplayNickname; + std::string strNetplayCentralServer; // Hotkeys int iHotkey[NUM_HOTKEYS]; diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 2c009026b7f0..eafbf48a0004 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -27,52 +27,6 @@ NetSettings g_NetPlaySettings; #define RPT_SIZE_HACK (1 << 16) -void ENetUtil::BroadcastPacket(ENetHost* host, const Packet& pac) -{ - enet_host_broadcast(host, 0, enet_packet_create((u8*) pac.vec->data(), pac.vec->size(), ENET_PACKET_FLAG_RELIABLE)); -} - -void ENetUtil::SendPacket(ENetPeer* peer, const Packet& pac) -{ - enet_peer_send(peer, 0, enet_packet_create((u8*) pac.vec->data(), pac.vec->size(), ENET_PACKET_FLAG_RELIABLE)); -} - -Packet ENetUtil::MakePacket(ENetPacket* epacket) -{ - Packet pac(PWBuffer(epacket->data, epacket->dataLength)); - enet_packet_destroy(epacket); - return pac; -} - -void ENetUtil::Wakeup(ENetHost* host) -{ - // Send ourselves a spurious message. This is hackier than it should be. - // I reported this as https://github.com/lsalzman/enet/issues/23, so - // hopefully there will be a better way to do it soon. - ENetAddress address; - if (host->address.port != 0) - address.port = host->address.port; - else - enet_socket_get_address(host->socket, &address); - address.host = 0x0100007f; // localhost - - u8 byte = 0; - ENetBuffer buf; - buf.data = &byte; - buf.dataLength = 1; - enet_socket_send(host->socket, &address, &buf, 1); -} - -int ENET_CALLBACK ENetUtil::InterceptCallback(ENetHost* host, ENetEvent* event) -{ - if (host->receivedDataLength == 1) - { - event->type = (ENetEventType) 42; - return 1; - } - return 0; -} - NetPad::NetPad() { nHi = 0x00808080; @@ -98,110 +52,112 @@ NetPlayClient::~NetPlayClient() if (m_is_running) StopGame(); - if (m_IsConnected) - { - m_do_loop = false; - ENetUtil::Wakeup(m_host); - if (m_dialog) - m_thread.join(); - } - - if (m_host) - enet_host_destroy(m_host); + if (!m_direct_connection && g_TraversalClient) + g_TraversalClient->Reset(); } // called from ---GUI--- thread -NetPlayClient::NetPlayClient(const std::string& address, const u16 port, const std::string& name) +NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& name, std::function stateCallback) { m_is_running = false; m_do_loop = true; m_target_buffer_size = 20; - m_IsConnected = false; + m_state = Failure; m_dialog = NULL; m_host = NULL; - m_ServerError = 0; + m_server_error = 0; + m_state_callback = stateCallback; ClearBuffers(); - ENetAddress addr; - - if (enet_address_set_host(&addr, address.c_str())) - return; - addr.port = port; + Player player; + player.name = name; + player.pid = m_pid; + player.revision = netplay_dolphin_ver; - m_host = enet_host_create( - NULL, // address - 1, // peerCount - 1, // channelLimit - 0, // incomingBandwidth - 0); // outgoingBandwidth + // add self to player list + m_players[m_pid] = player; + m_local_player = &m_players[m_pid]; - m_host->intercept = ENetUtil::InterceptCallback; - - ENetEvent event; - enet_host_connect(m_host, &addr, /*channelCount=*/0, /*data=*/0); - int count = enet_host_service(m_host, &event, 1500); - if (count <= 0) + size_t pos = hostSpec.find(':'); + if (pos != std::string::npos) { - // The connection failed or timed out. - return; + // Direct or local connection. Don't use TraversalClient. + m_direct_connection = true; + m_host_client.reset(new ENetHostClient(1)); + if (!m_host_client->m_Host) + return; + m_host_client->m_Client = this; + + m_host = m_host_client->m_Host; + + std::string host = hostSpec.substr(0, pos); + int port = std::stoi(hostSpec.substr(pos + 1).c_str()); + ENetAddress addr; + if (enet_address_set_host(&addr, host.c_str())) + return; + addr.port = port; + DoDirectConnect(addr); } - - // send connect message - Packet hello; - hello.W(std::string(NETPLAY_VERSION)); - hello.W(std::string(netplay_dolphin_ver)); - hello.W(name); - ENetUtil::BroadcastPacket(m_host, hello); - - count = enet_host_service(m_host, &event, 1000); - if (count <= 0 || - event.type != ENET_EVENT_TYPE_RECEIVE) + else { - // They disconnected or timed out. TODO: better error reporting. - return; + m_direct_connection = false; + EnsureTraversalClient(); + if (!g_TraversalClient) + return; + if (g_TraversalClient->m_State == TraversalClient::InitFailure) + return; + g_TraversalClient->m_Client = this; + m_host_spec = hostSpec; + m_host = g_TraversalClient->m_Host; + m_state = WaitingForTraversalClientConnection; + OnTraversalStateChanged(); } +} - Packet resp = ENetUtil::MakePacket(event.packet); - resp.Do(m_ServerError); - resp.Do(m_pid); - if (!resp.failure && !m_ServerError) - { - Player player; - player.name = name; - player.pid = m_pid; - player.revision = netplay_dolphin_ver; - - // add self to player list - m_players[m_pid] = player; - m_local_player = &m_players[m_pid]; - - //PanicAlertT("Connection successful: assigned player id: %d", m_pid); - m_IsConnected = true; - } +void NetPlayClient::DoDirectConnect(const ENetAddress& addr) +{ + m_state = Connecting; + enet_host_connect(m_host, &addr, /*channelCount=*/0, /*data=*/0); } void NetPlayClient::SetDialog(NetPlayUI* dialog) { - bool hadDialog = m_dialog; m_dialog = dialog; - if (!hadDialog) - { - // don't start receive messages until we have a dialog - m_thread = std::thread(std::mem_fun(&NetPlayClient::ThreadFunc), this); - } + m_have_dialog_event.Set(); } // called from multiple threads void NetPlayClient::SendPacket(Packet& packet) { - m_queue.Push(std::move(packet)); - ENetUtil::Wakeup(m_host); + CopyAsMove tmp(std::move(packet)); + g_TraversalClient->RunOnThread([=]() mutable { + ENetUtil::BroadcastPacket(m_host, *tmp); + }); } // called from ---NETPLAY--- thread void NetPlayClient::OnData(Packet&& packet) { + if (m_state == WaitingForHelloResponse) + { + packet.Do(m_server_error); + packet.Do(m_pid); + if (packet.failure || m_server_error) + return OnDisconnect(); + + m_local_player->pid = m_pid; + + //PanicAlertT("Connection successful: assigned player id: %d", m_pid); + m_state = Connected; + if (m_state_callback) + m_state_callback(this); + m_have_dialog_event.Wait(); + return; + } + else if(m_state != Connected) + return; + MessageId mid; packet.Do(mid); if (packet.failure) @@ -441,43 +397,78 @@ void NetPlayClient::OnData(Packet&& packet) // called from ---NETPLAY--- thread void NetPlayClient::OnDisconnect() { + if (m_state == Connected) + { + NetPlay_Disable(); + m_dialog->AppendChat("< LOST CONNECTION TO SERVER >"); + PanicAlertT("Lost connection to server."); + } m_is_running = false; - NetPlay_Disable(); - m_dialog->AppendChat("< LOST CONNECTION TO SERVER >"); - PanicAlertT("Lost connection to server."); - m_do_loop = false; + m_state = Failure; + if (m_state_callback) + m_state_callback(this); } // called from ---NETPLAY--- thread -void NetPlayClient::ThreadFunc() +void NetPlayClient::OnENetEvent(ENetEvent* event) { - while (m_do_loop) + switch (event->type) { - while (!m_queue.Empty()) + case ENET_EVENT_TYPE_CONNECT: { - Packet& opacket = m_queue.Front(); - ENetUtil::BroadcastPacket(m_host, opacket); - m_queue.Pop(); + if (m_state != Connecting) + break; + // send connect message + Packet hello; + hello.W(std::string(NETPLAY_VERSION)); + hello.W(std::string(netplay_dolphin_ver)); + hello.W(m_local_player->name); + ENetUtil::BroadcastPacket(m_host, hello); + m_state = WaitingForHelloResponse; + if (m_state_callback) + m_state_callback(this); + break; } + case ENET_EVENT_TYPE_DISCONNECT: + OnDisconnect(); + break; + case ENET_EVENT_TYPE_RECEIVE: + OnData(ENetUtil::MakePacket(event->packet)); + break; + default: + break; + } +} - ENetEvent event; - int count = enet_host_service(m_host, &event, 10000); - if (count < 0) - return OnDisconnect(); - if (count > 0) - { - switch (event.type) - { - case ENET_EVENT_TYPE_DISCONNECT: - OnDisconnect(); - break; - case ENET_EVENT_TYPE_RECEIVE: - OnData(ENetUtil::MakePacket(event.packet)); - break; - default: - break; - } - } +// called from ---NETPLAY--- thread +void NetPlayClient::OnTraversalStateChanged() +{ + if (m_state == WaitingForTraversalClientConnection && + g_TraversalClient->m_State == TraversalClient::Connected) + { + m_state = WaitingForTraversalClientConnectReady; + if (m_state_callback) + m_state_callback(this); + g_TraversalClient->Connect(m_host_spec); + } + else if (m_state != Failure && + g_TraversalClient->m_State == TraversalClient::ConnectFailure) + { + OnDisconnect(); + } +} + +// called from ---NETPLAY--- thread +void NetPlayClient::OnConnectReady(ENetAddress addr) +{ + if (m_state == WaitingForTraversalClientConnectReady) + { + if (addr.port != 0) + DoDirectConnect(addr); + else + m_state = Failure; + if (m_state_callback) + m_state_callback(this); } } diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index da2de6240697..ed2bc976ee00 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -21,15 +21,7 @@ #include #include "FifoQueue.h" - -namespace ENetUtil -{ - void BroadcastPacket(ENetHost* host, const Packet& pac); - void SendPacket(ENetPeer* peer, const Packet& pac); - Packet MakePacket(ENetPacket* epacket); - void Wakeup(ENetHost* host); - int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event); -} +#include "TraversalClient.h" class NetPad { @@ -67,19 +59,25 @@ class Player u32 ping; }; -class NetPlayClient +class NetPlayClient : public TraversalClientClient { public: - void ThreadFunc(); - - NetPlayClient(const std::string& address, const u16 port, const std::string& name); + NetPlayClient(const std::string& hostSpec, const std::string& name, std::function stateCallback); ~NetPlayClient(); void GetPlayerList(std::string& list, std::vector& pid_list); void GetPlayers(std::vector& player_list); - bool m_IsConnected; - MessageId m_ServerError; + enum State + { + WaitingForTraversalClientConnection, + WaitingForTraversalClientConnectReady, + Connecting, + WaitingForHelloResponse, + Connected, + Failure + } m_state; + MessageId m_server_error; bool StartGame(const std::string &path); bool StopGame(); @@ -99,6 +97,11 @@ class NetPlayClient void SetDialog(NetPlayUI* dialog); + virtual void OnENetEvent(ENetEvent*) override; + virtual void OnTraversalStateChanged() override; + virtual void OnConnectReady(ENetAddress addr) override; + + std::function m_state_callback; protected: void ClearBuffers(); @@ -109,6 +112,8 @@ class NetPlayClient NetPlayUI* m_dialog; ENetHost* m_host; + std::string m_host_spec; + bool m_direct_connection; std::thread m_thread; std::string m_selected_game; @@ -133,10 +138,12 @@ class NetPlayClient void OnData(Packet&& packet); void OnDisconnect(); void SendPacket(Packet& packet); + void DoDirectConnect(const ENetAddress& addr); PlayerId m_pid; std::map m_players; - Common::FifoQueue m_queue; + std::unique_ptr m_host_client; + Common::Event m_have_dialog_event; }; void NetPlay_Enable(NetPlayClient* const np); diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 08b221a57f2c..422ad76a08c4 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -3,116 +3,71 @@ // Refer to the license.txt file included. #include "NetPlayServer.h" -// for ENetUtils -#include "NetPlayClient.h" - -#define MAX_CLIENTS 200 - -NetPlayServer* NetPlayServer::s_instance; +#include "NetPlayClient.h" // for NetPlayUI NetPlayServer::~NetPlayServer() { - if (m_host) - { - m_do_loop = false; - ENetUtil::Wakeup(m_host); - m_thread.join(); - enet_host_destroy(m_host); - } + // leave the host open for future use + g_TraversalClient->Reset(); } // called from ---GUI--- thread NetPlayServer::NetPlayServer() { - m_IsConnected = false; m_is_running = false; m_num_players = 0; m_dialog = NULL; - s_instance = this; memset(m_pad_map, -1, sizeof(m_pad_map)); memset(m_wiimote_map, -1, sizeof(m_wiimote_map)); - - ENetAddress address; - address.host = ENET_HOST_ANY; - address.port = ENET_PORT_ANY; - // leave some spaces free for server full notification - m_host = enet_host_create( - &address, // address - MAX_CLIENTS + 16, // peerCount - 1, // channelLimit - 0, // incomingBandwidth - 0); // outgoingBandwidth - - if (m_host == NULL) - return; - - RetrySTUN(); - - m_host->intercept = InterceptCallback; - - m_do_loop = true; m_target_buffer_size = 20; - m_thread = std::thread(std::mem_fun(&NetPlayServer::ThreadFunc), this); - m_IsConnected = true; + + g_TraversalClient->m_Client = this; + m_host = g_TraversalClient->m_Host; } // called from ---NETPLAY--- thread -void NetPlayServer::ThreadFunc() +void NetPlayServer::UpdatePings() { - while (m_do_loop) - { - // update pings every so many seconds - if ((m_ping_timer.GetTimeElapsed() > (10 * 1000)) || m_update_pings) - { - //PanicAlertT("Sending pings"); + m_ping_key = Common::Timer::GetTimeMs(); - m_ping_key = Common::Timer::GetTimeMs(); + Packet ping; + ping.W((MessageId)NP_MSG_PING); + ping.W(m_ping_key); - Packet ping; - ping.W((MessageId)NP_MSG_PING); - ping.W(m_ping_key); + m_ping_timer.Start(); + SendToClientsOnThread(ping); - m_ping_timer.Start(); - SendToClientsOnThread(ping); - - m_update_pings = false; - } - - // handle pending messages - while (!m_queue.Empty()) - { - auto& out = m_queue.Front(); - SendToClientsOnThread(out.first, out.second); - m_queue.Pop(); - } - - ENetEvent event; - int count = enet_host_service(m_host, &event, 10000); - if (count < 0) - { - PanicAlert("Wait failed... do something about this."); - continue; - } + m_update_pings = false; +} - if (count == 0) - continue; +// called from ---NETPLAY--- thread +void NetPlayServer::OnENetEvent(ENetEvent* event) +{ + // update pings every so many seconds + if (m_ping_timer.GetTimeElapsed() > (10 * 1000)) + UpdatePings(); - PlayerId pid = event.peer - m_host->peers; - switch (event.type) - { - case ENET_EVENT_TYPE_DISCONNECT: - OnDisconnect(pid); - break; - case ENET_EVENT_TYPE_RECEIVE: - OnData(pid, ENetUtil::MakePacket(event.packet)); - break; - default: - // notably, ignore connects until we get a hello message - break; - } + PlayerId pid = event->peer - m_host->peers; + switch (event->type) + { + case ENET_EVENT_TYPE_DISCONNECT: + OnDisconnect(pid); + break; + case ENET_EVENT_TYPE_RECEIVE: + OnData(pid, ENetUtil::MakePacket(event->packet)); + break; + default: + // notably, ignore connects until we get a hello message + break; } } +void NetPlayServer::OnTraversalStateChanged() +{ + if (m_dialog) + m_dialog->Update(); +} + // called from ---NETPLAY--- thread MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) { @@ -135,8 +90,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) if (m_num_players >= MAX_CLIENTS) return CON_ERR_SERVER_FULL; - // cause pings to be updated - m_update_pings = true; + UpdatePings(); // try to automatically assign new user a pad for (unsigned int m = 0; m < 4; ++m) @@ -515,8 +469,10 @@ bool NetPlayServer::StartGame(const std::string &path) // called from multiple threads void NetPlayServer::SendToClients(Packet& packet, const PlayerId skip_pid) { - m_queue.Push(std::make_pair(std::move(packet), skip_pid)); - ENetUtil::Wakeup(m_host); + CopyAsMove tmp(std::move(packet)); + g_TraversalClient->RunOnThread([=]() mutable { + SendToClientsOnThread(*tmp, skip_pid); + }); } @@ -530,78 +486,12 @@ void NetPlayServer::SendToClientsOnThread(const Packet& packet, const PlayerId s m_players[pid].connected) { if (!epacket) - epacket = enet_packet_create((u8*) packet.vec->data(), packet.vec->size(), ENET_PACKET_FLAG_RELIABLE); + epacket = enet_packet_create(packet.vec->data(), packet.vec->size(), ENET_PACKET_FLAG_RELIABLE); enet_peer_send(&m_host->peers[pid], 0, epacket); } } } -// called from ---GUI--- thread -u16 NetPlayServer::GetPort() -{ - return m_host->address.port; -} - -// called from ---GUI--- thread -std::pair NetPlayServer::GetHost() -{ - auto state = Common::AtomicLoadAcquire(m_stun_state); - return std::make_pair(state, m_address_str); -} - -void NetPlayServer::RetrySTUN() -{ - // no std::to_string on Android - char buf[64]; - sprintf(buf, "%d", (int) GetPort()); - m_address_str = buf; - m_stun_state = STILL_RUNNING; - std::vector servers; - //servers.push_back("dolphin-emu.org"); - servers.push_back("stunserver.org"); - m_stun_client = STUNClient(m_host->socket, std::move(servers)); -} - -// called from ---NETPLAY--- thread -int NetPlayServer::InterceptCallback(ENetHost* host, ENetEvent* event) -{ - return s_instance->Intercept(event); -} - -int NetPlayServer::Intercept(ENetEvent* event) -{ - if (m_stun_client.m_Status == STUNClient::Waiting) - { - bool result = m_stun_client.ReceivedPacket(m_host->receivedData, m_host->receivedDataLength, &m_host->receivedAddress); - m_stun_client.Ping(); - if (m_stun_client.m_Status == STUNClient::Error || - m_stun_client.m_Status == STUNClient::Timeout) - { - Common::AtomicStoreRelease(m_stun_state, STUN_FAILED); - if (m_dialog) - m_dialog->Update(); - } - else if (m_stun_client.m_Status == STUNClient::Ok) - { - const ENetAddress* addr = &m_stun_client.m_MyAddress; - char buf[64]; - if (enet_address_get_host_ip(addr, buf, sizeof(buf)) < 0) - strcpy(buf, "???"); - sprintf(buf + strlen(buf), ":%d", (int) addr->port); - m_address_str = buf; - Common::AtomicStoreRelease(m_stun_state, STUN_OK); - if (m_dialog) - m_dialog->Update(); - } - if (result) - { - event->type = (ENetEventType) 42; - return 1; - } - } - return ENetUtil::InterceptCallback(m_host, event); -} - void NetPlayServer::SetDialog(NetPlayUI* dialog) { m_dialog = dialog; diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 8041e864a45d..5e72e54fc42c 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -10,20 +10,18 @@ #include "Thread.h" #include "Timer.h" -#include "NetPlayProto.h" #include "enet/enet.h" +#include "NetPlayProto.h" #include "FifoQueue.h" -#include "STUNClient.h" +#include "TraversalClient.h" #include class NetPlayUI; -class NetPlayServer +class NetPlayServer : public TraversalClientClient { public: - void ThreadFunc(); - NetPlayServer(); ~NetPlayServer(); @@ -41,21 +39,11 @@ class NetPlayServer void AdjustPadBufferSize(unsigned int size); - bool m_IsConnected; - - u16 GetPort(); - void SetDialog(NetPlayUI* dialog); - enum STUNState - { - STILL_RUNNING, - STUN_OK, - STUN_FAILED - }; - std::pair GetHost(); - void RetrySTUN(); - + virtual void OnENetEvent(ENetEvent*) override; + virtual void OnTraversalStateChanged() override; + virtual void OnConnectReady(ENetAddress addr) override {} private: class Client { @@ -76,14 +64,11 @@ class NetPlayServer void OnData(PlayerId pid, Packet&& packet); void UpdatePadMapping(); void UpdateWiimoteMapping(); - - static int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event); - int Intercept(ENetEvent* event); + void UpdatePings(); NetSettings m_settings; bool m_is_running; - bool m_do_loop; Common::Timer m_ping_timer; u32 m_ping_key; bool m_update_pings; @@ -101,15 +86,7 @@ class NetPlayServer std::string m_selected_game; ENetHost* m_host; - std::thread m_thread; - Common::FifoQueue, false> m_queue; - volatile STUNState m_stun_state; - std::string m_address_str; - STUNClient m_stun_client; NetPlayUI* m_dialog; - - // this is for the InterceptCallback; replace with a map if you care - static NetPlayServer* s_instance; }; #endif diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 0391006bd0d9..59df57946641 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -25,6 +25,10 @@ BEGIN_EVENT_TABLE(NetPlayDiag, wxFrame) EVT_COMMAND(wxID_ANY, wxEVT_THREAD, NetPlayDiag::OnThread) END_EVENT_TABLE() +BEGIN_EVENT_TABLE(ConnectDiag, wxDialog) + EVT_COMMAND(wxID_ANY, wxEVT_THREAD, ConnectDiag::OnThread) +END_EVENT_TABLE() + static std::unique_ptr netplay_server; static std::unique_ptr netplay_client; extern CFrame* main_frame; @@ -286,29 +290,31 @@ void NetPlayDiag::OnThread(wxCommandEvent& event) { if (m_is_hosting) { - auto info = netplay_server->GetHost(); - switch (info.first) + switch (g_TraversalClient->m_State) { - case NetPlayServer::STILL_RUNNING: + case TraversalClient::Connecting: m_host_label->SetForegroundColour(*wxLIGHT_GREY); - m_host_label->SetLabel(wxString::Format(_("(local ip):%s"), StrToWxStr(info.second))); + m_host_label->SetLabel("..."); m_host_copy_btn->SetLabel(_("Copy")); m_host_copy_btn->Disable(); break; - case NetPlayServer::STUN_OK: + case TraversalClient::Connected: m_host_label->SetForegroundColour(*wxBLACK); - m_host_label->SetLabel(StrToWxStr(info.second)); + m_host_label->SetLabel(wxString(g_TraversalClient->m_HostId.data(), g_TraversalClient->m_HostId.size())); m_host_copy_btn->SetLabel(_("Copy")); m_host_copy_btn->Enable(); m_host_copy_btn_is_retry = false; break; - case NetPlayServer::STUN_FAILED: + case TraversalClient::ConnectFailure: m_host_label->SetForegroundColour(*wxBLACK); - m_host_label->SetLabel(wxString::Format(_("(local ip):%s"), StrToWxStr(info.second))); + m_host_label->SetLabel(_("failure xxx")); m_host_copy_btn->SetLabel(_("Retry")); m_host_copy_btn->Enable(); m_host_copy_btn_is_retry = true; break; + case TraversalClient::InitFailure: + // can't happen + break; } } @@ -400,7 +406,7 @@ void NetPlayDiag::OnCopyIP(wxCommandEvent&) if (m_host_copy_btn_is_retry) { // nevermind. - netplay_server->RetrySTUN(); + //netplay_server->RetrySTUN(); Update(); } else @@ -442,24 +448,41 @@ ConnectDiag::ConnectDiag(wxWindow* parent) SetMaxSize(wxSize(10000, GetBestSize().GetHeight())); } - bool ConnectDiag::Validate() { if (netplay_client) { // shouldn't be possible, just in case + printf("already a NPC???\n"); return false; } std::string hostSpec = GetHost(); - size_t pos = hostSpec.find(':'); - std::string host = hostSpec.substr(0, pos); - int port = std::stoi(hostSpec.substr(pos + 1).c_str()); std::string nickname = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayNickname; - netplay_client.reset(new NetPlayClient(host, (u16)port, nickname)); - if (!netplay_client->m_IsConnected) + netplay_client.reset(new NetPlayClient(GetHost(), nickname, [=](NetPlayClient* npc) { + auto state = npc->m_state; + if (state == NetPlayClient::Connected || state == NetPlayClient::Failure) + { + wxCommandEvent evt(wxEVT_THREAD, 1); + GetEventHandler()->AddPendingEvent(evt); + } + })); + // disable the GUI + m_HostCtrl->Disable(); + m_ConnectBtn->Disable(); + return false; +} + +void ConnectDiag::OnThread(wxCommandEvent& event) +{ + if (netplay_client->m_state == NetPlayClient::Connected) + { + netplay_client->SetDialog(new NetPlayDiag(GetParent(), "", false)); + EndModal(0); + } + else { wxString err; - switch (netplay_client->m_ServerError) + switch (netplay_client->m_server_error) { case 0: // there was no server error. @@ -483,11 +506,10 @@ bool ConnectDiag::Validate() auto complain = new wxMessageDialog(this, err); complain->ShowWindowModal(); // We leak the message dialog because of a wx bug. - return false; + // bring the UI back + m_HostCtrl->Enable(); + m_ConnectBtn->Enable(); } - netplay_client->SetDialog(new NetPlayDiag(GetParent(), "", false)); - EndModal(0); - return true; } void ConnectDiag::OnChange(wxCommandEvent& event) @@ -504,13 +526,23 @@ bool ConnectDiag::IsHostOk() { std::string host = GetHost(); size_t pos = host.find(':'); - return pos != std::string::npos && - pos + 1 < host.size() && - host.find_first_not_of("0123456789", pos + 1) == std::string::npos; + if (pos != std::string::npos) + { + // ip:port + return pos + 1 < host.size() && + host.find_first_not_of("0123456789", pos + 1) == std::string::npos; + } + else + { + // traversal host id + return host.size() == sizeof(TraversalHostId); + } } ConnectDiag::~ConnectDiag() { + if (GetReturnCode() != 0) + netplay_client.reset(); SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayHost = StripSpaces(WxStrToStr(m_HostCtrl->GetValue())); SConfig::GetInstance().SaveSettings(); } @@ -631,19 +663,37 @@ void NetPlay::StartHosting(std::string id, wxWindow* parent) return; } + EnsureTraversalClient(); + if (!g_TraversalClient) + { + wxMessageBox(_("Failed to init traversal client. This shouldn't happen..."), _("Error"), wxOK, parent); + return; + } + netplay_server.reset(new NetPlayServer()); netplay_server->ChangeGame(id); netplay_server->AdjustPadBufferSize(INITIAL_PAD_BUFFER_SIZE); - if (!netplay_server->m_IsConnected) + std::string nickname = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayNickname; + Common::Event ev; + char buf[64]; + sprintf(buf, "127.0.0.1:%d", g_TraversalClient->GetPort()); + netplay_client.reset(new NetPlayClient(buf, nickname, [&](NetPlayClient* npc) { + if (npc->m_state == NetPlayClient::Connected || + npc->m_state == NetPlayClient::Failure) + ev.Set(); + })); + if (netplay_client->m_state == NetPlayClient::Failure) { - wxMessageBox(_("Failed to host. This shouldn't happen..."), _("Error"), wxOK, parent); + PanicAlert("Failed to init netplay client. This shouldn't happen..."); + netplay_client.reset(); netplay_server.reset(); return; } - std::string nickname = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayNickname; - netplay_client.reset(new NetPlayClient("127.0.0.1", netplay_server->GetPort(), nickname)); - if (!netplay_client->m_IsConnected) + ev.Wait(); + + if (netplay_client->m_state != NetPlayClient::Connected) { + printf("state=%d\n", netplay_client->m_state); wxMessageBox(_("Failed to connect to localhost. This shouldn't happen..."), _("Error"), wxOK, parent); netplay_client.reset(); netplay_server.reset(); diff --git a/Source/Core/DolphinWX/Src/NetWindow.h b/Source/Core/DolphinWX/Src/NetWindow.h index f701aa31112a..aa585af27300 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.h +++ b/Source/Core/DolphinWX/Src/NetWindow.h @@ -99,10 +99,12 @@ class ConnectDiag : public wxDialog ~ConnectDiag(); std::string GetHost(); bool Validate(); - void OnThread(wxCommandEvent& event); private: + DECLARE_EVENT_TABLE() + void OnChange(wxCommandEvent& event); + void OnThread(wxCommandEvent& event); bool IsHostOk(); wxTextCtrl* m_HostCtrl; wxButton* m_ConnectBtn; From 277c49710287505d602b2a9890462d2a28df14a1 Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 8 Oct 2013 00:50:01 -0400 Subject: [PATCH 019/202] Fix retry button. --- Source/Core/Common/Src/TraversalClient.cpp | 2 ++ Source/Core/DolphinWX/Src/NetWindow.cpp | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index 3b75ce74840d..bb3cc4279eb0 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -176,6 +176,8 @@ void TraversalClient::ReconnectToServer() SendPacket(hello); }); m_State = Connecting; + if (m_Client) + m_Client->OnTraversalStateChanged(); } u16 TraversalClient::GetPort() diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 59df57946641..cf53f9f36258 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -405,8 +405,7 @@ void NetPlayDiag::OnCopyIP(wxCommandEvent&) { if (m_host_copy_btn_is_retry) { - // nevermind. - //netplay_server->RetrySTUN(); + g_TraversalClient->ReconnectToServer(); Update(); } else From 9116a71b39de29e8e5ea61e02651f67bba2a99d5 Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 8 Oct 2013 00:54:22 -0400 Subject: [PATCH 020/202] Fix evicting (and change test server) --- Source/Core/Common/Src/TraversalClient.cpp | 3 +- Source/Core/Common/Src/TraversalServer.cpp | 125 +++++++++++++-------- Source/Core/Core/Src/NetPlayClient.cpp | 1 + 3 files changed, 81 insertions(+), 48 deletions(-) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index bb3cc4279eb0..3444db326525 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -164,7 +164,7 @@ TraversalClient::TraversalClient() void TraversalClient::ReconnectToServer() { std::string server = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayCentralServer; - server = "192.168.1.216"; // XXX + server = "vps.qoid.us"; // XXX if (enet_address_set_host(&m_ServerAddress, server.c_str())) return; m_ServerAddress.port = 6262; @@ -291,6 +291,7 @@ void TraversalClient::HandleServerPacket(TraversalPacket* packet) case TraversalPacketConnectReady: case TraversalPacketConnectFailed: { + if (!m_PendingConnect || packet->connectReady.requestId != m_ConnectRequestId) break; diff --git a/Source/Core/Common/Src/TraversalServer.cpp b/Source/Core/Common/Src/TraversalServer.cpp index b71e03722ad5..e567f0c524c9 100644 --- a/Source/Core/Common/Src/TraversalServer.cpp +++ b/Source/Core/Common/Src/TraversalServer.cpp @@ -30,59 +30,83 @@ struct OutgoingPacketInfo u64 sendTime; }; -template -struct EvictingKey +template +struct EvictEntry { - // 30 seconds - const u64 EvictTimeout = 30 * 1000000; - template - EvictingKey(T&& k) : m_K(std::forward(k)) - { - m_CreateTime = currentTime; - } - bool operator==(const EvictingKey& other) const - { - return currentTime - m_CreateTime > EvictTimeout || - currentTime - other.m_CreateTime > EvictTimeout || - m_K == other.m_K; - } - K m_K; - u32 m_CreateTime; + u64 updateTime; + T value; }; -template -struct std::hash> +template +struct EvictFindResult { - size_t operator()(const EvictingKey& key) const - { - return std::hash()(key.m_K); - } + bool found; + V* value; }; -template <> -struct std::hash +template +EvictFindResult EvictFind(typename std::unordered_map>& map, const K& key, bool refresh = false) { - size_t operator()(const TraversalHostId& id) const + retry: + const u64 expiryTime = 30 * 1000000; // 30s + EvictFindResult result; + if (map.bucket_count()) { - auto p = (u32*) id.data(); - return p[0] ^ ((p[1] << 13) | (p[1] >> 19)); + auto bucket = map.bucket(key); + auto it = map.begin(bucket); + for (; it != map.end(bucket); ++it) + { + if (currentTime - it->second.updateTime > expiryTime) + { + map.erase(it->first); + goto retry; + } + if (it->first == key) + { + if (refresh) + it->second.updateTime = currentTime; + result.found = true; + result.value = &it->second.value; + return result; + } + } } -}; + result.found = false; + return result; +} + +template +V* EvictSet(typename std::unordered_map>& map, const K& key) +{ + // can't use a local_iterator to emplace... + auto& result = map[key]; + result.updateTime = currentTime; + return &result.value; +} + +namespace std +{ + template <> + struct hash + { + size_t operator()(const TraversalHostId& id) const + { + auto p = (u32*) id.data(); + return p[0] ^ ((p[1] << 13) | (p[1] >> 19)); + } + }; +} static int sock; static int urandomFd; static std::unordered_map< -TraversalRequestId, + TraversalRequestId, OutgoingPacketInfo - > outgoingPackets; - static std::unordered_map< - EvictingKey, - std::pair - > didSendInfo; - static std::unordered_map< - EvictingKey, - TraversalInetAddress - > connectedClients; +> outgoingPackets; +static std::unordered_map< + TraversalHostId, + EvictEntry +> connectedClients; static TraversalInetAddress MakeInetAddress(const struct sockaddr_in6& addr) { @@ -269,7 +293,8 @@ static void HandlePacket(TraversalPacket* packet, struct sockaddr_in6* addr) } case TraversalPacketPing: { - packetOk = connectedClients.find(packet->ping.hostId) != connectedClients.end(); + auto r = EvictFind(connectedClients, packet->ping.hostId, true); + packetOk = r.found; break; } case TraversalPacketHelloFromClient: @@ -284,11 +309,17 @@ static void HandlePacket(TraversalPacket* packet, struct sockaddr_in6* addr) TraversalInetAddress* iaddr; // not that there is any significant change of // duplication, but... - do + GetRandomHostId(&hostId); + while (1) { - GetRandomHostId(&hostId); - iaddr = &connectedClients[hostId]; - } while (iaddr->port); + auto r = EvictFind(connectedClients, hostId); + if (!r.found) + { + iaddr = EvictSet(connectedClients, hostId); + break; + } + } + *iaddr = MakeInetAddress(*addr); reply->helloFromServer.yourAddress = *iaddr; @@ -299,8 +330,8 @@ static void HandlePacket(TraversalPacket* packet, struct sockaddr_in6* addr) case TraversalPacketConnectPlease: { TraversalHostId& hostId = packet->connectPlease.hostId; - auto it = connectedClients.find(hostId); - if (it == connectedClients.end()) + auto r = EvictFind(connectedClients, hostId); + if (!r.found) { TraversalPacket* reply = AllocPacket(*addr); reply->type = TraversalPacketConnectFailed; @@ -308,7 +339,7 @@ static void HandlePacket(TraversalPacket* packet, struct sockaddr_in6* addr) } else { - TraversalPacket* please = AllocPacket(MakeSinAddr(it->second), packet->requestId); + TraversalPacket* please = AllocPacket(MakeSinAddr(*r.value), packet->requestId); please->type = TraversalPacketPleaseSendPacket; please->pleaseSendPacket.address = MakeInetAddress(*addr); } diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index eafbf48a0004..bf99f6c48036 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -117,6 +117,7 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam void NetPlayClient::DoDirectConnect(const ENetAddress& addr) { + printf("DDC to %x:%d\n", addr.host, addr.port); m_state = Connecting; enet_host_connect(m_host, &addr, /*channelCount=*/0, /*data=*/0); } From 3b64446093c441a9b5ffe4798fb832e9e4437d68 Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 8 Oct 2013 02:11:25 -0400 Subject: [PATCH 021/202] Fix Linux build because Unix is stupid. --- Source/Core/Common/Src/TraversalServer.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Core/Common/Src/TraversalServer.cpp b/Source/Core/Common/Src/TraversalServer.cpp index e567f0c524c9..e88c2d504bd9 100644 --- a/Source/Core/Common/Src/TraversalServer.cpp +++ b/Source/Core/Common/Src/TraversalServer.cpp @@ -134,7 +134,9 @@ static TraversalInetAddress MakeInetAddress(const struct sockaddr_in6& addr) static struct sockaddr_in6 MakeSinAddr(const TraversalInetAddress& addr) { struct sockaddr_in6 result; +#ifdef SIN6_LEN result.sin6_len = sizeof(result); +#endif result.sin6_family = AF_INET6; result.sin6_port = addr.port; result.sin6_flowinfo = 0; @@ -384,7 +386,9 @@ int main() } struct in6_addr any = IN6ADDR_ANY_INIT; struct sockaddr_in6 addr; +#ifdef SIN6_LEN addr.sin6_len = sizeof(addr); +#endif addr.sin6_family = AF_INET6; addr.sin6_port = htons(6262); addr.sin6_flowinfo = 0; From 171ecab47a76a5a6497f9d1c286e56e2db62a0a3 Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 8 Oct 2013 02:52:42 -0400 Subject: [PATCH 022/202] Fix Common depending on Core, breaking Windows build. --- Source/Core/Common/Src/TraversalClient.cpp | 16 +++++++--------- Source/Core/Common/Src/TraversalClient.h | 5 +++-- Source/Core/Core/Src/NetPlayClient.cpp | 3 ++- Source/Core/DolphinWX/Src/NetWindow.cpp | 3 ++- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index 3444db326525..9835892e7132 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -2,8 +2,6 @@ #include "TraversalClient.h" #include "enet/enet.h" -#include "NetPlayClient.h" // for ENetUtil -#include "ConfigManager.h" void ENetUtil::BroadcastPacket(ENetHost* host, const Packet& pac) { @@ -146,8 +144,9 @@ void ENetHostClient::ThreadFunc() } } -TraversalClient::TraversalClient() -: ENetHostClient(MAX_CLIENTS + 16, true) // leave some spaces free for server full notification +TraversalClient::TraversalClient(const std::string& server) +: ENetHostClient(MAX_CLIENTS + 16, true), // leave some spaces free for server full notification +m_Server(server) { if (!m_Host) return; @@ -163,9 +162,8 @@ TraversalClient::TraversalClient() void TraversalClient::ReconnectToServer() { - std::string server = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayCentralServer; - server = "vps.qoid.us"; // XXX - if (enet_address_set_host(&m_ServerAddress, server.c_str())) + m_Server = "vps.qoid.us"; // XXX + if (enet_address_set_host(&m_ServerAddress, m_Server.c_str())) return; m_ServerAddress.port = 6262; @@ -406,11 +404,11 @@ void TraversalClient::Reset() std::unique_ptr g_TraversalClient; -void EnsureTraversalClient() +void EnsureTraversalClient(const std::string& server) { if (!g_TraversalClient) { - g_TraversalClient.reset(new TraversalClient); + g_TraversalClient.reset(new TraversalClient(server)); if (g_TraversalClient->m_State == TraversalClient::InitFailure) { g_TraversalClient.reset(); diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index 764611f4c906..57f7c9a7e7bd 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -80,7 +80,7 @@ class TraversalClient : public ENetHostClient Connected }; - TraversalClient(); + TraversalClient(const std::string& server); void Reset(); void Connect(const std::string& host); void ReconnectToServer(); @@ -111,7 +111,8 @@ class TraversalClient : public ENetHostClient std::list m_OutgoingPackets; ENetAddress m_ServerAddress; enet_uint32 m_PingTime; + std::string m_Server; }; extern std::unique_ptr g_TraversalClient; -void EnsureTraversalClient(); +void EnsureTraversalClient(const std::string& server); diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index bf99f6c48036..fe180deb5a44 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -102,7 +102,8 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam else { m_direct_connection = false; - EnsureTraversalClient(); + std::string server = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayCentralServer; + EnsureTraversalClient(server); if (!g_TraversalClient) return; if (g_TraversalClient->m_State == TraversalClient::InitFailure) diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index cf53f9f36258..7dcddcb7b861 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -662,7 +662,8 @@ void NetPlay::StartHosting(std::string id, wxWindow* parent) return; } - EnsureTraversalClient(); + std::string server = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayCentralServer; + EnsureTraversalClient(server); if (!g_TraversalClient) { wxMessageBox(_("Failed to init traversal client. This shouldn't happen..."), _("Error"), wxOK, parent); From bcdd488d69812c337431e240a53e976912f8de55 Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 8 Oct 2013 02:56:33 -0400 Subject: [PATCH 023/202] ...and don't use C++11 initializers. --- Source/Core/Common/Src/TraversalClient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index 9835892e7132..d9a6d9e9c594 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -62,7 +62,7 @@ static void GetRandomishBytes(u8* buf, size_t size) ENetHostClient::ENetHostClient(size_t peerCount, bool isTraversalClient) { m_isTraversalClient = isTraversalClient; - ENetAddress addr { ENET_HOST_ANY, ENET_PORT_ANY }; + ENetAddress addr = { ENET_HOST_ANY, ENET_PORT_ANY }; m_Host = enet_host_create( &addr, // address peerCount, // peerCount From f66cc471d7fc4ff8d0fe228d4a835569246a501b Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 15 Oct 2013 00:06:21 -0400 Subject: [PATCH 024/202] Compile time thread safety checking for clang. For use in situations where related code is frequently being used from multiple threads. Implemented in the NetPlay code in place of the existing comments like "// called from ---NETPLAY--- thread" - if you're going to mark up every function anyway, may as well do it in a way the compiler can verify. It might be a dumb idea to be using threads for netplay in the first place, but since the CPU thread blocks on NetPlay and in theory the GUI thread's performance could be unpredictable, it might be justified. --- CMakeLists.txt | 4 +- Source/Core/AudioCommon/Src/AudioCommon.cpp | 2 +- Source/Core/Common/Src/Common.h | 67 +++++++++++++++++++ Source/Core/Common/Src/StdConditionVariable.h | 2 +- Source/Core/Common/Src/StdMutex.h | 40 +++++++++-- Source/Core/Common/Src/Thread.h | 2 +- Source/Core/Common/Src/TraversalClient.cpp | 2 + Source/Core/Common/Src/TraversalClient.h | 42 ++++++------ Source/Core/Core/Src/HW/CPU.cpp | 1 + Source/Core/Core/Src/HW/DSPLLE/DSPLLE.cpp | 1 + .../Core/Src/HW/WiimoteReal/WiimoteReal.cpp | 1 + Source/Core/Core/Src/NetPlayClient.cpp | 62 ++++++----------- Source/Core/Core/Src/NetPlayClient.h | 41 ++++++------ Source/Core/Core/Src/NetPlayServer.cpp | 17 +---- Source/Core/Core/Src/NetPlayServer.h | 36 +++++----- Source/Core/InputCommon/Src/UDPWiimote.cpp | 1 + Source/Core/VideoCommon/Src/Fifo.cpp | 1 + .../Plugin_VideoSoftware/Src/SWmain.cpp | 1 + 18 files changed, 197 insertions(+), 126 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 76493eaec62a..2a9e836abbc4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -181,8 +181,8 @@ if(APPLE) set(ENV{PATH} /usr/bin:/bin:/usr/sbin:/sbin) # Some of our code contains Objective C constructs. - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -x objective-c -stdlib=libc++") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -x objective-c++ -stdlib=libc++") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -x objective-c -stdlib=libc++ -Werror=thread-safety") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -x objective-c++ -stdlib=libc++ -Werror=thread-safety") # Avoid mistaking an object file for a source file on the link command line. set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -x none") diff --git a/Source/Core/AudioCommon/Src/AudioCommon.cpp b/Source/Core/AudioCommon/Src/AudioCommon.cpp index 8b343aa493c2..008121aa31ca 100644 --- a/Source/Core/AudioCommon/Src/AudioCommon.cpp +++ b/Source/Core/AudioCommon/Src/AudioCommon.cpp @@ -119,7 +119,7 @@ namespace AudioCommon return SConfig::GetInstance().m_EnableJIT; } - void PauseAndLock(bool doLock, bool unpauseOnUnlock) + void PauseAndLock(bool doLock, bool unpauseOnUnlock) IGNORE_THREAD_SAFETY { if (soundStream) { diff --git a/Source/Core/Common/Src/Common.h b/Source/Core/Common/Src/Common.h index c4bf6a605a81..463660853343 100644 --- a/Source/Core/Common/Src/Common.h +++ b/Source/Core/Common/Src/Common.h @@ -175,4 +175,71 @@ enum EMUSTATE_CHANGE #include "CommonFuncs.h" +#ifdef __clang__ +#define THREAD_SAFETY_ANNOTATIONS +#endif + +#ifdef THREAD_SAFETY_ANNOTATIONS +/* + Optional annotations to denote thread roles. + Example: + // entry point known to be on CPU thread + void foo() ASSUME_ON_CPU; + // must be called by someone ON(CPU) or ASSUME_ON(CPU) + void foo() ON(CPU) { ... + // guarding variables + int foo ACCESS_ON(CPU); + int *ptr DEREF_ON(CPU); + // writes from CPU, reads from GPU + int baz ACCESS_ON(CPU2GPU); +*/ +struct ThreadHat +{ +} __attribute__((lockable)); + +template +struct ThreadHatLock +{ + __attribute__((exclusive_lock_function(THH::hat()))) + ThreadHatLock() {} + __attribute__((unlock_function(THH::hat()))) + ~ThreadHatLock() {} +} __attribute__((scoped_lockable)); +#define _TS_MACRO(a...) a +#else +#define _TS_MACRO(...) +#endif + +#define DEFINE_THREAD_HAT(name) \ + _TS_MACRO( \ + extern ThreadHat name##ThreadHat; \ + struct name##ThreadHatLock \ + { \ + __attribute__((exclusive_lock_function(name##ThreadHat))) \ + name##ThreadHatLock() {} \ + __attribute__((unlock_function(name##ThreadHat))) \ + ~name##ThreadHatLock() {} \ + } __attribute__((scoped_lockable)) \ + ) + +DEFINE_THREAD_HAT(CPU); +DEFINE_THREAD_HAT(GPU); +DEFINE_THREAD_HAT(GUI); + +#define GUARDED_BY(name) \ + _TS_MACRO(__attribute__((guarded_by(name)))) +#define PT_GUARDED_BY(name) \ + _TS_MACRO(__attribute__((pt_guarded_by(name)))) +#define ACCESS_ON(name) \ + _TS_MACRO(__attribute__((guarded_by(name##ThreadHat)))) +#define DEREF_ON(name) \ + _TS_MACRO(__attribute__((pt_guarded_by(name##ThreadHat)))) +#define ON(name) \ + _TS_MACRO(__attribute__((exclusive_locks_required(name##ThreadHat)))) +#define IGNORE_THREAD_SAFETY \ + _TS_MACRO(__attribute__((no_thread_safety_analysis))) +#define ASSUME_ON(name) \ + _TS_MACRO(__attribute__((exclusive_trylock_function(12345, name##ThreadHat)))) +#define DO_ASSUME_ON(name) \ + _TS_MACRO(name##ThreadHatLock __##name##__thlock) #endif // _COMMON_H_ diff --git a/Source/Core/Common/Src/StdConditionVariable.h b/Source/Core/Common/Src/StdConditionVariable.h index 048a7e897579..b1a8bbf8d8ed 100644 --- a/Source/Core/Common/Src/StdConditionVariable.h +++ b/Source/Core/Common/Src/StdConditionVariable.h @@ -14,7 +14,7 @@ // GCC 4.4 provides #include -#elif __has_include() && !ANDROID +#elif __has_include() && !ANDROID && !defined(THREAD_SAFETY_ANNOTATIONS) // clang and libc++ provide on OSX. However, the version // of libc++ bundled with OSX 10.7 and 10.8 is buggy: it uses _ as a variable. diff --git a/Source/Core/Common/Src/StdMutex.h b/Source/Core/Common/Src/StdMutex.h index ce46a2f59183..61b45f78b5e4 100644 --- a/Source/Core/Common/Src/StdMutex.h +++ b/Source/Core/Common/Src/StdMutex.h @@ -12,7 +12,7 @@ #if GCC_VERSION >= GCC_VER(4,4,0) && __GXX_EXPERIMENTAL_CXX0X__ // GCC 4.4 provides #include -#elif __has_include() && !ANDROID +#elif __has_include() && !ANDROID && !defined(THREAD_SAFETY_ANNOTATIONS) // Clang + libc++ #include #else @@ -40,6 +40,18 @@ #define USE_SRWLOCKS #endif +#ifdef __clang__ +#include +#define defer_lock_t _defer_lock_t +#define try_to_lock_t _try_to_lock_t +#define adopt_lock_t _adopt_lock_t +#define defer_lock _defer_lock +#define try_to_lock _try_to_lock +#define adopt_lock _adopt_lock +#define mutex _mutex +#define recursive_mutex _recursive_mutex +#endif + namespace std { @@ -78,6 +90,7 @@ class recursive_mutex #endif } + _TS_MACRO(__attribute__((exclusive_lock_function(*this)))) void lock() { #ifdef _WIN32 @@ -87,6 +100,7 @@ class recursive_mutex #endif } + _TS_MACRO(__attribute__((unlock_function(*this)))) void unlock() { #ifdef _WIN32 @@ -96,6 +110,7 @@ class recursive_mutex #endif } + _TS_MACRO(__attribute__((exclusive_trylock_function(true, *this)))) bool try_lock() { #ifdef _WIN32 @@ -112,7 +127,7 @@ class recursive_mutex private: native_type m_handle; -}; +} _TS_MACRO(__attribute__((lockable))); #if !defined(_WIN32) || defined(USE_SRWLOCKS) @@ -147,6 +162,7 @@ class mutex #endif } + _TS_MACRO(__attribute__((exclusive_lock_function(*this)))) void lock() { #ifdef _WIN32 @@ -156,6 +172,7 @@ class mutex #endif } + _TS_MACRO(__attribute__((unlock_function(*this)))) void unlock() { #ifdef _WIN32 @@ -165,6 +182,7 @@ class mutex #endif } + _TS_MACRO(__attribute__((exclusive_trylock_function(true, *this)))) bool try_lock() { #ifdef _WIN32 @@ -183,7 +201,7 @@ class mutex private: native_type m_handle; -}; +} _TS_MACRO(__attribute__((lockable))); #else typedef recursive_mutex mutex; // just use CriticalSections @@ -200,17 +218,20 @@ class lock_guard public: typedef Mutex mutex_type; + _TS_MACRO(__attribute__((exclusive_lock_function(m)))) explicit lock_guard(mutex_type& m) : pm(m) { m.lock(); } + _TS_MACRO(__attribute__((exclusive_lock_function(m)))) lock_guard(mutex_type& m, adopt_lock_t) : pm(m) { } + _TS_MACRO(__attribute__((unlock_function))) ~lock_guard() { pm.unlock(); @@ -221,7 +242,7 @@ class lock_guard private: mutex_type& pm; -}; +} _TS_MACRO(__attribute__((scoped_lockable))); template class unique_lock @@ -233,6 +254,7 @@ class unique_lock : pm(NULL), owns(false) {} + _TS_MACRO(__attribute__((exclusive_lock_function(m)))) /*explicit*/ unique_lock(mutex_type& m) : pm(&m), owns(true) { @@ -243,10 +265,14 @@ class unique_lock : pm(&m), owns(false) {} + /* There is no "tryunlock_function", so this must be annotated this way. + * This generally fails to be expressive enough for lock inversion. */ + _TS_MACRO(__attribute__((exclusive_lock_function(m)))) unique_lock(mutex_type& m, try_to_lock_t) : pm(&m), owns(m.try_lock()) {} + _TS_MACRO(__attribute__((exclusive_lock_function(m)))) unique_lock(mutex_type& m, adopt_lock_t) : pm(&m), owns(true) {} @@ -257,6 +283,7 @@ class unique_lock //template //unique_lock(mutex_type& m, const chrono::duration& rel_time); + _TS_MACRO(__attribute__((unlock_function))) ~unique_lock() { if (owns_lock()) @@ -294,12 +321,14 @@ class unique_lock swap(other); } + _TS_MACRO(__attribute__((exclusive_lock_function))) void lock() { mutex()->lock(); owns = true; } + _TS_MACRO(__attribute__((exclusive_lock_function))) bool try_lock() { owns = mutex()->try_lock(); @@ -311,6 +340,7 @@ class unique_lock //template //bool try_lock_until(const chrono::time_point& abs_time); + _TS_MACRO(__attribute__((unlock_function))) void unlock() { mutex()->unlock(); @@ -351,7 +381,7 @@ class unique_lock private: mutex_type* pm; bool owns; -}; +} _TS_MACRO(__attribute__((scoped_lockable))); template void swap(unique_lock& x, unique_lock& y) diff --git a/Source/Core/Common/Src/Thread.h b/Source/Core/Common/Src/Thread.h index b0328f7a1fa3..ebbf7207cabf 100644 --- a/Source/Core/Common/Src/Thread.h +++ b/Source/Core/Common/Src/Thread.h @@ -137,7 +137,7 @@ class Barrier const size_t m_count; volatile size_t m_waiting; }; - + void SleepCurrentThread(int ms); void SwitchCurrentThread(); // On Linux, this is equal to sleep 1ms diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index d9a6d9e9c594..c61473d9199d 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -86,6 +86,7 @@ ENetHostClient::~ENetHostClient() if (m_Host) { RunOnThread([=]() { + DO_ASSUME_ON(NET); m_ShouldEndThread = true; }); m_Thread.join(); @@ -171,6 +172,7 @@ void TraversalClient::ReconnectToServer() hello.type = TraversalPacketHelloFromClient; hello.helloFromClient.protoVersion = TraversalProtoVersion; RunOnThread([=]() { + DO_ASSUME_ON(NET); SendPacket(hello); }); m_State = Connecting; diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index 57f7c9a7e7bd..022dca742c1a 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -10,16 +10,18 @@ #include #include +DEFINE_THREAD_HAT(NET); + #define MAX_CLIENTS 200 #include "ChunkFile.h" namespace ENetUtil { - void BroadcastPacket(ENetHost* host, const Packet& pac); - void SendPacket(ENetPeer* peer, const Packet& pac); + void BroadcastPacket(ENetHost* host, const Packet& pac) ON(NET); + void SendPacket(ENetPeer* peer, const Packet& pac) ON(NET); Packet MakePacket(ENetPacket* epacket); void Wakeup(ENetHost* host); - int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event); + int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) ASSUME_ON(NET); } // Apparently nobody on the C++11 standards committee thought of @@ -39,9 +41,9 @@ class CopyAsMove class TraversalClientClient { public: - virtual void OnENetEvent(ENetEvent*) = 0; - virtual void OnTraversalStateChanged() = 0; - virtual void OnConnectReady(ENetAddress addr) = 0; + virtual void OnENetEvent(ENetEvent*) ASSUME_ON(NET) = 0; + virtual void OnTraversalStateChanged() ASSUME_ON(NET) = 0; + virtual void OnConnectReady(ENetAddress addr) ASSUME_ON(NET) = 0; }; class ENetHostClient @@ -56,16 +58,15 @@ class ENetHostClient TraversalClientClient* m_Client; ENetHost* m_Host; protected: - virtual void HandleResends() {} + virtual void HandleResends() ON(NET) {} private: - void ThreadFunc(); + void ThreadFunc() ASSUME_ON(NET); Common::FifoQueue, false> m_RunQueue; std::thread m_Thread; // *sigh* Common::Event m_ResetEvent; - bool m_ResetReady; - bool m_ShouldEndThread; + bool m_ShouldEndThread ACCESS_ON(NET); bool m_isTraversalClient; }; @@ -81,16 +82,15 @@ class TraversalClient : public ENetHostClient }; TraversalClient(const std::string& server); - void Reset(); - void Connect(const std::string& host); + void Reset() ON(NET); + void Connect(const std::string& host) ON(NET); void ReconnectToServer(); u16 GetPort(); - // will be called from thread TraversalHostId m_HostId; State m_State; protected: - virtual void HandleResends(); + virtual void HandleResends() ON(NET); private: struct OutgoingPacketInfo { @@ -99,16 +99,16 @@ class TraversalClient : public ENetHostClient enet_uint32 sendTime; }; - void HandleServerPacket(TraversalPacket* packet); - void ResendPacket(OutgoingPacketInfo* info); - TraversalRequestId SendPacket(const TraversalPacket& packet); - void OnConnectFailure(); - static int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event); - void HandlePing(); + void HandleServerPacket(TraversalPacket* packet) ON(NET); + void ResendPacket(OutgoingPacketInfo* info) ON(NET); + TraversalRequestId SendPacket(const TraversalPacket& packet) ON(NET); + void OnConnectFailure() ON(NET); + static int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) ASSUME_ON(NET); + void HandlePing() ON(NET); TraversalRequestId m_ConnectRequestId; bool m_PendingConnect; - std::list m_OutgoingPackets; + std::list m_OutgoingPackets ACCESS_ON(NET); ENetAddress m_ServerAddress; enet_uint32 m_PingTime; std::string m_Server; diff --git a/Source/Core/Core/Src/HW/CPU.cpp b/Source/Core/Core/Src/HW/CPU.cpp index f013c9ee7d74..9769021549b8 100644 --- a/Source/Core/Core/Src/HW/CPU.cpp +++ b/Source/Core/Core/Src/HW/CPU.cpp @@ -128,6 +128,7 @@ void CCPU::Break() EnableStepping(true); } +IGNORE_THREAD_SAFETY bool CCPU::PauseAndLock(bool doLock, bool unpauseOnUnlock) { bool wasUnpaused = !IsStepping(); diff --git a/Source/Core/Core/Src/HW/DSPLLE/DSPLLE.cpp b/Source/Core/Core/Src/HW/DSPLLE/DSPLLE.cpp index d16af5f6014c..edcd1190dc3e 100644 --- a/Source/Core/Core/Src/HW/DSPLLE/DSPLLE.cpp +++ b/Source/Core/Core/Src/HW/DSPLLE/DSPLLE.cpp @@ -347,6 +347,7 @@ void DSPLLE::DSP_ClearAudioBuffer(bool mute) soundStream->Clear(mute); } +IGNORE_THREAD_SAFETY void DSPLLE::PauseAndLock(bool doLock, bool unpauseOnUnlock) { if (doLock || unpauseOnUnlock) diff --git a/Source/Core/Core/Src/HW/WiimoteReal/WiimoteReal.cpp b/Source/Core/Core/Src/HW/WiimoteReal/WiimoteReal.cpp index ee6de5f19d9c..91a787c555b6 100644 --- a/Source/Core/Core/Src/HW/WiimoteReal/WiimoteReal.cpp +++ b/Source/Core/Core/Src/HW/WiimoteReal/WiimoteReal.cpp @@ -763,6 +763,7 @@ void HandleFoundWiimotes(const std::vector& wiimotes) } // This is called from the GUI thread +IGNORE_THREAD_SAFETY void Refresh() { g_wiimote_scanner.StopScanning(); diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index fe180deb5a44..864bbffae64f 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -45,7 +45,6 @@ NetPad::NetPad(const SPADStatus* const pad_status) nLo |= (u32)((u8)pad_status->substickX << 24); } -// called from ---GUI--- thread NetPlayClient::~NetPlayClient() { // not perfect @@ -56,11 +55,9 @@ NetPlayClient::~NetPlayClient() g_TraversalClient->Reset(); } -// called from ---GUI--- thread NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& name, std::function stateCallback) { m_is_running = false; - m_do_loop = true; m_target_buffer_size = 20; m_state = Failure; m_dialog = NULL; @@ -129,16 +126,15 @@ void NetPlayClient::SetDialog(NetPlayUI* dialog) m_have_dialog_event.Set(); } -// called from multiple threads void NetPlayClient::SendPacket(Packet& packet) { CopyAsMove tmp(std::move(packet)); g_TraversalClient->RunOnThread([=]() mutable { + DO_ASSUME_ON(NET); ENetUtil::BroadcastPacket(m_host, *tmp); }); } -// called from ---NETPLAY--- thread void NetPlayClient::OnData(Packet&& packet) { if (m_state == WaitingForHelloResponse) @@ -396,7 +392,6 @@ void NetPlayClient::OnData(Packet&& packet) } } -// called from ---NETPLAY--- thread void NetPlayClient::OnDisconnect() { if (m_state == Connected) @@ -411,7 +406,6 @@ void NetPlayClient::OnDisconnect() m_state_callback(this); } -// called from ---NETPLAY--- thread void NetPlayClient::OnENetEvent(ENetEvent* event) { switch (event->type) @@ -442,7 +436,6 @@ void NetPlayClient::OnENetEvent(ENetEvent* event) } } -// called from ---NETPLAY--- thread void NetPlayClient::OnTraversalStateChanged() { if (m_state == WaitingForTraversalClientConnection && @@ -460,7 +453,6 @@ void NetPlayClient::OnTraversalStateChanged() } } -// called from ---NETPLAY--- thread void NetPlayClient::OnConnectReady(ENetAddress addr) { if (m_state == WaitingForTraversalClientConnectReady) @@ -474,7 +466,6 @@ void NetPlayClient::OnConnectReady(ENetAddress addr) } } -// called from ---GUI--- thread void NetPlayClient::GetPlayerList(std::string& list, std::vector& pid_list) { std::lock_guard lk(m_crit); @@ -509,7 +500,6 @@ void NetPlayClient::GetPlayerList(std::string& list, std::vector& pid_list) list = ss.str(); } -// called from ---GUI--- thread void NetPlayClient::GetPlayers(std::vector &player_list) { std::lock_guard lk(m_crit); @@ -524,7 +514,6 @@ void NetPlayClient::GetPlayers(std::vector &player_list) } -// called from ---GUI--- thread void NetPlayClient::SendChatMessage(const std::string& msg) { Packet packet; @@ -534,7 +523,6 @@ void NetPlayClient::SendChatMessage(const std::string& msg) SendPacket(packet); } -// called from ---GUI--- thread void NetPlayClient::ChangeName(const std::string& name) { { @@ -548,7 +536,6 @@ void NetPlayClient::ChangeName(const std::string& name) SendPacket(packet); } -// called from ---CPU--- thread void NetPlayClient::SendPadState(const PadMapping in_game_pad, const NetPad& np) { // send to server @@ -561,7 +548,6 @@ void NetPlayClient::SendPadState(const PadMapping in_game_pad, const NetPad& np) SendPacket(packet); } -// called from ---CPU--- thread void NetPlayClient::SendWiimoteState(const PadMapping in_game_pad, const NetWiimote& nw) { // send to server @@ -573,7 +559,6 @@ void NetPlayClient::SendWiimoteState(const PadMapping in_game_pad, const NetWiim SendPacket(packet); } -// called from ---GUI--- thread bool NetPlayClient::StartGame(const std::string &path) { if (m_is_running) @@ -644,13 +629,11 @@ bool NetPlayClient::StartGame(const std::string &path) return true; } -// called from ---GUI--- thread bool NetPlayClient::ChangeGame(const std::string&) { return true; } -// called from ---NETPLAY--- thread void NetPlayClient::UpdateDevices() { for (PadMapping i = 0; i < 4; i++) @@ -660,7 +643,6 @@ void NetPlayClient::UpdateDevices() } } -// called from ---NETPLAY--- thread void NetPlayClient::ClearBuffers() { // clear pad buffers, Clear method isn't thread safe @@ -674,7 +656,6 @@ void NetPlayClient::ClearBuffers() } } -// called from ---CPU--- thread bool NetPlayClient::GetNetPads(const u8 pad_nb, const SPADStatus* const pad_status, NetPad* const netvalues) { // The interface for this is extremely silly. @@ -758,7 +739,6 @@ bool NetPlayClient::GetNetPads(const u8 pad_nb, const SPADStatus* const pad_stat } -// called from ---CPU--- thread bool NetPlayClient::WiimoteUpdate(int _number, u8* data, const u8 size) { NetWiimote nw; @@ -853,7 +833,6 @@ bool NetPlayClient::WiimoteUpdate(int _number, u8* data, const u8 size) return true; } -// called from ---GUI--- thread and ---NETPLAY--- thread (client side) bool NetPlayClient::StopGame() { std::lock_guard lk(m_crit); @@ -879,24 +858,27 @@ void NetPlayClient::Stop() { if (m_is_running == false) return; - bool isPadMapped = false; - for (unsigned int i = 0; i < 4; ++i) - { - if (m_pad_map[i] == m_local_player->pid) - isPadMapped = true; - } - for (unsigned int i = 0; i < 4; ++i) - { - if (m_wiimote_map[i] == m_local_player->pid) - isPadMapped = true; - } - // tell the server to stop if we have a pad mapped in game. - if (isPadMapped) - { - Packet packet; - packet.W((MessageId)NP_MSG_STOP_GAME); - ENetUtil::BroadcastPacket(m_host, packet); - } + g_TraversalClient->RunOnThread([=]() mutable { + bool isPadMapped = false; + DO_ASSUME_ON(NET); + for (unsigned int i = 0; i < 4; ++i) + { + if (m_pad_map[i] == m_local_player->pid) + isPadMapped = true; + } + for (unsigned int i = 0; i < 4; ++i) + { + if (m_wiimote_map[i] == m_local_player->pid) + isPadMapped = true; + } + // tell the server to stop if we have a pad mapped in game. + if (isPadMapped) + { + Packet packet; + packet.W((MessageId)NP_MSG_STOP_GAME); + ENetUtil::BroadcastPacket(m_host, packet); + } + }); } u8 NetPlayClient::InGamePadToLocalPad(u8 ingame_pad) diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index ed2bc976ee00..5ed4a2dac1c5 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -65,8 +65,8 @@ class NetPlayClient : public TraversalClientClient NetPlayClient(const std::string& hostSpec, const std::string& name, std::function stateCallback); ~NetPlayClient(); - void GetPlayerList(std::string& list, std::vector& pid_list); - void GetPlayers(std::vector& player_list); + void GetPlayerList(std::string& list, std::vector& pid_list) ASSUME_ON(GUI); + void GetPlayers(std::vector& player_list) ASSUME_ON(GUI); enum State { @@ -79,16 +79,16 @@ class NetPlayClient : public TraversalClientClient } m_state; MessageId m_server_error; - bool StartGame(const std::string &path); - bool StopGame(); - void Stop(); - bool ChangeGame(const std::string& game); - void SendChatMessage(const std::string& msg); - void ChangeName(const std::string& name); + bool StartGame(const std::string &path) ASSUME_ON(GUI); + bool StopGame() /* multiple threads */; + void Stop() ASSUME_ON(GUI); + bool ChangeGame(const std::string& game) ASSUME_ON(GUI); + void SendChatMessage(const std::string& msg) ASSUME_ON(GUI); + void ChangeName(const std::string& name) ASSUME_ON(GUI); // Send and receive pads values - bool WiimoteUpdate(int _number, u8* data, const u8 size); - bool GetNetPads(const u8 pad_nb, const SPADStatus* const, NetPad* const netvalues); + bool WiimoteUpdate(int _number, u8* data, const u8 size) ASSUME_ON(CPU); + bool GetNetPads(const u8 pad_nb, const SPADStatus* const, NetPad* const netvalues) ASSUME_ON(CPU); u8 LocalPadToInGamePad(u8 localPad); u8 InGamePadToLocalPad(u8 localPad); @@ -97,13 +97,13 @@ class NetPlayClient : public TraversalClientClient void SetDialog(NetPlayUI* dialog); - virtual void OnENetEvent(ENetEvent*) override; - virtual void OnTraversalStateChanged() override; - virtual void OnConnectReady(ENetAddress addr) override; + virtual void OnENetEvent(ENetEvent*) override ON(NET); + virtual void OnTraversalStateChanged() override ON(NET); + virtual void OnConnectReady(ENetAddress addr) override ON(NET); std::function m_state_callback; protected: - void ClearBuffers(); + void ClearBuffers() ON(NET); std::recursive_mutex m_crit; @@ -116,9 +116,8 @@ class NetPlayClient : public TraversalClientClient bool m_direct_connection; std::thread m_thread; - std::string m_selected_game; + std::string m_selected_game GUARDED_BY(m_crit); volatile bool m_is_running; - volatile bool m_do_loop; unsigned int m_target_buffer_size; @@ -132,11 +131,11 @@ class NetPlayClient : public TraversalClientClient bool m_is_recording; private: - void UpdateDevices(); - void SendPadState(const PadMapping in_game_pad, const NetPad& np); - void SendWiimoteState(const PadMapping in_game_pad, const NetWiimote& nw); - void OnData(Packet&& packet); - void OnDisconnect(); + void UpdateDevices() ON(NET); + void SendPadState(const PadMapping in_game_pad, const NetPad& np) ASSUME_ON(CPU); + void SendWiimoteState(const PadMapping in_game_pad, const NetWiimote& nw) ASSUME_ON(CPU); + void OnData(Packet&& packet) ON(NET); + void OnDisconnect() ON(NET); void SendPacket(Packet& packet); void DoDirectConnect(const ENetAddress& addr); diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 422ad76a08c4..f9f28a571e13 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -25,7 +25,6 @@ NetPlayServer::NetPlayServer() m_host = g_TraversalClient->m_Host; } -// called from ---NETPLAY--- thread void NetPlayServer::UpdatePings() { m_ping_key = Common::Timer::GetTimeMs(); @@ -40,7 +39,6 @@ void NetPlayServer::UpdatePings() m_update_pings = false; } -// called from ---NETPLAY--- thread void NetPlayServer::OnENetEvent(ENetEvent* event) { // update pings every so many seconds @@ -68,7 +66,6 @@ void NetPlayServer::OnTraversalStateChanged() m_dialog->Update(); } -// called from ---NETPLAY--- thread MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) { Client& player = m_players[pid]; @@ -161,7 +158,6 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) return 0; } -// called from ---NETPLAY--- thread void NetPlayServer::OnDisconnect(PlayerId pid) { Client& player = m_players[pid]; @@ -199,7 +195,6 @@ void NetPlayServer::OnDisconnect(PlayerId pid) UpdateWiimoteMapping(); } -// called from ---GUI--- thread void NetPlayServer::GetPadMapping(PadMapping map[4]) { for (int i = 0; i < 4; i++) @@ -212,7 +207,6 @@ void NetPlayServer::GetWiimoteMapping(PadMapping map[4]) map[i] = m_wiimote_map[i]; } -// called from ---GUI--- thread void NetPlayServer::SetPadMapping(const PadMapping map[4]) { for (int i = 0; i < 4; i++) @@ -220,7 +214,6 @@ void NetPlayServer::SetPadMapping(const PadMapping map[4]) UpdatePadMapping(); } -// called from ---GUI--- thread void NetPlayServer::SetWiimoteMapping(const PadMapping map[4]) { for (int i = 0; i < 4; i++) @@ -228,7 +221,6 @@ void NetPlayServer::SetWiimoteMapping(const PadMapping map[4]) UpdateWiimoteMapping(); } -// called from ---GUI--- thread and ---NETPLAY--- thread void NetPlayServer::UpdatePadMapping() { Packet opacket; @@ -237,7 +229,6 @@ void NetPlayServer::UpdatePadMapping() SendToClients(opacket); } -// called from ---GUI--- thread and ---NETPLAY--- thread void NetPlayServer::UpdateWiimoteMapping() { Packet opacket; @@ -246,7 +237,6 @@ void NetPlayServer::UpdateWiimoteMapping() SendToClients(opacket); } -// called from ---GUI--- thread and ---NETPLAY--- thread void NetPlayServer::AdjustPadBufferSize(unsigned int size) { m_target_buffer_size = size; @@ -258,7 +248,6 @@ void NetPlayServer::AdjustPadBufferSize(unsigned int size) SendToClients(opacket); } -// called from ---NETPLAY--- thread void NetPlayServer::OnData(PlayerId pid, Packet&& packet) { ENetPeer* peer = &m_host->peers[pid]; @@ -418,7 +407,6 @@ void NetPlayServer::OnData(PlayerId pid, Packet&& packet) } } -// called from ---GUI--- thread bool NetPlayServer::ChangeGame(const std::string &game) { std::lock_guard lk(m_crit); @@ -434,13 +422,11 @@ bool NetPlayServer::ChangeGame(const std::string &game) return true; } -// called from ---GUI--- thread void NetPlayServer::SetNetSettings(const NetSettings &settings) { m_settings = settings; } -// called from ---GUI--- thread bool NetPlayServer::StartGame(const std::string &path) { m_current_game = Common::Timer::GetTimeMs(); @@ -466,17 +452,16 @@ bool NetPlayServer::StartGame(const std::string &path) return true; } -// called from multiple threads void NetPlayServer::SendToClients(Packet& packet, const PlayerId skip_pid) { CopyAsMove tmp(std::move(packet)); g_TraversalClient->RunOnThread([=]() mutable { + DO_ASSUME_ON(NET); SendToClientsOnThread(*tmp, skip_pid); }); } -// called from ---NETPLAY--- thread void NetPlayServer::SendToClientsOnThread(const Packet& packet, const PlayerId skip_pid) { ENetPacket* epacket = NULL; diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 5e72e54fc42c..a70e3df0bcdc 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -25,24 +25,24 @@ class NetPlayServer : public TraversalClientClient NetPlayServer(); ~NetPlayServer(); - bool ChangeGame(const std::string& game); + bool ChangeGame(const std::string& game) ASSUME_ON(GUI); - void SetNetSettings(const NetSettings &settings); + void SetNetSettings(const NetSettings &settings) ASSUME_ON(GUI); - bool StartGame(const std::string &path); + bool StartGame(const std::string &path) ASSUME_ON(GUI); - void GetPadMapping(PadMapping map[]); - void SetPadMapping(const PadMapping map[]); + void GetPadMapping(PadMapping map[]) ASSUME_ON(GUI); + void SetPadMapping(const PadMapping map[]) ASSUME_ON(GUI); - void GetWiimoteMapping(PadMapping map[]); - void SetWiimoteMapping(const PadMapping map[]); + void GetWiimoteMapping(PadMapping map[]) ASSUME_ON(GUI); + void SetWiimoteMapping(const PadMapping map[]) ASSUME_ON(GUI); - void AdjustPadBufferSize(unsigned int size); + void AdjustPadBufferSize(unsigned int size) /* multiple threads */; void SetDialog(NetPlayUI* dialog); - virtual void OnENetEvent(ENetEvent*) override; - virtual void OnTraversalStateChanged() override; + virtual void OnENetEvent(ENetEvent*) override ON(NET); + virtual void OnTraversalStateChanged() override ON(NET); virtual void OnConnectReady(ENetAddress addr) override {} private: class Client @@ -58,13 +58,13 @@ class NetPlayServer : public TraversalClientClient }; void SendToClients(Packet& packet, const PlayerId skip_pid = -1); - void SendToClientsOnThread(const Packet& packet, const PlayerId skip_pid = -1); - MessageId OnConnect(PlayerId pid, Packet& hello); - void OnDisconnect(PlayerId pid); - void OnData(PlayerId pid, Packet&& packet); - void UpdatePadMapping(); - void UpdateWiimoteMapping(); - void UpdatePings(); + void SendToClientsOnThread(const Packet& packet, const PlayerId skip_pid = -1) ON(NET); + MessageId OnConnect(PlayerId pid, Packet& hello) ON(NET); + void OnDisconnect(PlayerId pid) ON(NET); + void OnData(PlayerId pid, Packet&& packet) ON(NET); + void UpdatePadMapping() /* multiple threads */; + void UpdateWiimoteMapping() /* multiple threads */; + void UpdatePings() ON(NET); NetSettings m_settings; @@ -83,7 +83,7 @@ class NetPlayServer : public TraversalClientClient // only protects m_selected_game std::recursive_mutex m_crit; - std::string m_selected_game; + std::string m_selected_game GUARDED_BY(m_crit); ENetHost* m_host; NetPlayUI* m_dialog; diff --git a/Source/Core/InputCommon/Src/UDPWiimote.cpp b/Source/Core/InputCommon/Src/UDPWiimote.cpp index e0cf787e0f9b..895235f403f2 100644 --- a/Source/Core/InputCommon/Src/UDPWiimote.cpp +++ b/Source/Core/InputCommon/Src/UDPWiimote.cpp @@ -146,6 +146,7 @@ UDPWiimote::UDPWiimote(const char *_port, const char * name, int _index) : return; } +IGNORE_THREAD_SAFETY void UDPWiimote::mainThread() { std::unique_lock lk(d->termLock); diff --git a/Source/Core/VideoCommon/Src/Fifo.cpp b/Source/Core/VideoCommon/Src/Fifo.cpp index b7e00e128aca..a5c12a6eb4dc 100644 --- a/Source/Core/VideoCommon/Src/Fifo.cpp +++ b/Source/Core/VideoCommon/Src/Fifo.cpp @@ -36,6 +36,7 @@ void Fifo_DoState(PointerWrap &p) p.Do(g_bSkipCurrentFrame); } +IGNORE_THREAD_SAFETY void Fifo_PauseAndLock(bool doLock, bool unpauseOnUnlock) { if (doLock) diff --git a/Source/Plugins/Plugin_VideoSoftware/Src/SWmain.cpp b/Source/Plugins/Plugin_VideoSoftware/Src/SWmain.cpp index 7f6792fc1cee..e15193faba92 100644 --- a/Source/Plugins/Plugin_VideoSoftware/Src/SWmain.cpp +++ b/Source/Plugins/Plugin_VideoSoftware/Src/SWmain.cpp @@ -117,6 +117,7 @@ void VideoSoftware::CheckInvalidState() // there is no state to invalidate } +IGNORE_THREAD_SAFETY void VideoSoftware::PauseAndLock(bool doLock, bool unpauseOnUnlock) { if (doLock) From a1916acbbde822d40734a1b1ed817f8bbdc8620f Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 15 Oct 2013 00:55:30 -0400 Subject: [PATCH 025/202] Fix some stupid things. --- Source/Core/Common/Src/ChunkFile.h | 5 ++ Source/Core/Core/Src/NetPlayClient.cpp | 74 +++++++++++++------------ Source/Core/Core/Src/NetPlayClient.h | 7 ++- Source/Core/Core/Src/NetPlayServer.cpp | 6 +- Source/Core/DolphinWX/Src/NetWindow.cpp | 1 + 5 files changed, 51 insertions(+), 42 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index 2a664b468fcc..b44f15329684 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -345,9 +345,14 @@ class PointerWrap { case MODE_READ: if (size > vec->size() - readOff) + { failure = true; + return; + } else + { memcpy(data, vec->data() + readOff, size); + } break; case MODE_WRITE: diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 864bbffae64f..3ed0a49d382e 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -64,18 +64,10 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam m_host = NULL; m_server_error = 0; m_state_callback = stateCallback; + m_local_name = name; ClearBuffers(); - Player player; - player.name = name; - player.pid = m_pid; - player.revision = netplay_dolphin_ver; - - // add self to player list - m_players[m_pid] = player; - m_local_player = &m_players[m_pid]; - size_t pos = hostSpec.find(':'); if (pos != std::string::npos) { @@ -115,7 +107,6 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam void NetPlayClient::DoDirectConnect(const ENetAddress& addr) { - printf("DDC to %x:%d\n", addr.host, addr.port); m_state = Connecting; enet_host_connect(m_host, &addr, /*channelCount=*/0, /*data=*/0); } @@ -139,12 +130,18 @@ void NetPlayClient::OnData(Packet&& packet) { if (m_state == WaitingForHelloResponse) { + std::lock_guard lk(m_crit); + packet.Do(m_server_error); packet.Do(m_pid); if (packet.failure || m_server_error) return OnDisconnect(); - m_local_player->pid = m_pid; + Player player; + player.name = m_local_name; + player.pid = m_pid; + player.revision = netplay_dolphin_ver; + m_players[m_pid] = player; //PanicAlertT("Connection successful: assigned player id: %d", m_pid); m_state = Connected; @@ -206,14 +203,16 @@ void NetPlayClient::OnData(Packet&& packet) if (packet.failure) return OnDisconnect(); - // don't need lock to read in this thread - const Player& player = m_players[pid]; + { + std::lock_guard lk(m_crit); + const Player& player = m_players[pid]; - // add to gui - std::ostringstream ss; - ss << player.name << '[' << (char)(pid+'0') << "]: " << msg; + // add to gui + std::ostringstream ss; + ss << player.name << '[' << (char)(pid+'0') << "]: " << msg; - m_dialog->AppendChat(ss.str()); + m_dialog->AppendChat(ss.str()); + } } break; @@ -303,10 +302,7 @@ void NetPlayClient::OnData(Packet&& packet) case NP_MSG_CHANGE_GAME : { - { - std::lock_guard lk(m_crit); - packet.Do(m_selected_game); - } + packet.Do(m_selected_game); // update gui m_dialog->OnMsgChangeGame(m_selected_game); @@ -394,11 +390,14 @@ void NetPlayClient::OnData(Packet&& packet) void NetPlayClient::OnDisconnect() { + std::lock_guard lk(m_crit); if (m_state == Connected) { NetPlay_Disable(); m_dialog->AppendChat("< LOST CONNECTION TO SERVER >"); PanicAlertT("Lost connection to server."); + m_players.clear(); + m_pid = -1; } m_is_running = false; m_state = Failure; @@ -418,7 +417,7 @@ void NetPlayClient::OnENetEvent(ENetEvent* event) Packet hello; hello.W(std::string(NETPLAY_VERSION)); hello.W(std::string(netplay_dolphin_ver)); - hello.W(m_local_player->name); + hello.W(m_local_name); ENetUtil::BroadcastPacket(m_host, hello); m_state = WaitingForHelloResponse; if (m_state_callback) @@ -525,15 +524,18 @@ void NetPlayClient::SendChatMessage(const std::string& msg) void NetPlayClient::ChangeName(const std::string& name) { - { + g_TraversalClient->RunOnThread([=]() { + DO_ASSUME_ON(NET); std::lock_guard lk(m_crit); - m_local_player->name = name; - } - Packet packet; - packet.W((MessageId)NP_MSG_CHANGE_NAME); - packet.W(name); - - SendPacket(packet); + m_local_name = name; + if (m_pid != (PlayerId) -1) + m_players[m_pid].name = name; + Packet packet; + packet.W((MessageId)NP_MSG_CHANGE_NAME); + packet.W(name); + + SendPacket(packet); + }); } void NetPlayClient::SendPadState(const PadMapping in_game_pad, const NetPad& np) @@ -863,12 +865,12 @@ void NetPlayClient::Stop() DO_ASSUME_ON(NET); for (unsigned int i = 0; i < 4; ++i) { - if (m_pad_map[i] == m_local_player->pid) + if (m_pad_map[i] == m_pid) isPadMapped = true; } for (unsigned int i = 0; i < 4; ++i) { - if (m_wiimote_map[i] == m_local_player->pid) + if (m_wiimote_map[i] == m_pid) isPadMapped = true; } // tell the server to stop if we have a pad mapped in game. @@ -884,7 +886,7 @@ void NetPlayClient::Stop() u8 NetPlayClient::InGamePadToLocalPad(u8 ingame_pad) { // not our pad - if (m_pad_map[ingame_pad] != m_local_player->pid) + if (m_pad_map[ingame_pad] != m_pid) return 4; int local_pad = 0; @@ -892,7 +894,7 @@ u8 NetPlayClient::InGamePadToLocalPad(u8 ingame_pad) for (; pad < ingame_pad; pad++) { - if (m_pad_map[pad] == m_local_player->pid) + if (m_pad_map[pad] == m_pid) local_pad++; } @@ -908,7 +910,7 @@ u8 NetPlayClient::LocalPadToInGamePad(u8 local_pad) int ingame_pad = 0; for (; ingame_pad < 4; ingame_pad++) { - if (m_pad_map[ingame_pad] == m_local_player->pid) + if (m_pad_map[ingame_pad] == m_pid) local_pad_count++; if (local_pad_count == local_pad) @@ -927,7 +929,7 @@ u8 NetPlayClient::LocalWiimoteToInGameWiimote(u8 local_pad) int ingame_pad = 0; for (; ingame_pad < 4; ingame_pad++) { - if (m_wiimote_map[ingame_pad] == m_local_player->pid) + if (m_wiimote_map[ingame_pad] == m_pid) local_pad_count++; if (local_pad_count == local_pad) diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 5ed4a2dac1c5..46aaf43ae5bf 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -116,12 +116,13 @@ class NetPlayClient : public TraversalClientClient bool m_direct_connection; std::thread m_thread; - std::string m_selected_game GUARDED_BY(m_crit); + std::string m_selected_game ACCESS_ON(NET); volatile bool m_is_running; unsigned int m_target_buffer_size; - Player* m_local_player; + Player* m_local_player GUARDED_BY(m_crit); + std::string m_local_name ACCESS_ON(NET); u32 m_current_game; @@ -140,7 +141,7 @@ class NetPlayClient : public TraversalClientClient void DoDirectConnect(const ENetAddress& addr); PlayerId m_pid; - std::map m_players; + std::map m_players GUARDED_BY(m_crit); std::unique_ptr m_host_client; Common::Event m_have_dialog_event; }; diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index f9f28a571e13..c11acc1dc343 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -138,14 +138,14 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) } // send players - for (auto it = m_players.begin(); it != m_players.end(); ++it) + for (size_t opid = 0; opid < m_players.size(); opid++) { - Client& oplayer = *it; + Client& oplayer = m_players[opid]; if (oplayer.connected) { Packet opacket; opacket.W((MessageId)NP_MSG_PLAYER_JOIN); - opacket.W(pid); + opacket.W((PlayerId)opid); opacket.W(oplayer.name); opacket.W(oplayer.revision); ENetUtil::SendPacket(peer, opacket); diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 7dcddcb7b861..284f5d012ded 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -540,6 +540,7 @@ bool ConnectDiag::IsHostOk() ConnectDiag::~ConnectDiag() { + netplay_client->m_state_callback = nullptr; if (GetReturnCode() != 0) netplay_client.reset(); SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayHost = StripSpaces(WxStrToStr(m_HostCtrl->GetValue())); From 43df1012054c9a1a8470e7d87c1b119884dee708 Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 15 Oct 2013 21:14:21 -0400 Subject: [PATCH 026/202] Improve error reporting. --- Source/Core/Common/Src/TraversalClient.cpp | 35 +++++----- Source/Core/Common/Src/TraversalClient.h | 25 ++++++-- Source/Core/Common/Src/TraversalProto.h | 9 +++ Source/Core/Common/Src/TraversalServer.cpp | 3 + Source/Core/Core/Src/NetPlayClient.cpp | 62 ++++++++++-------- Source/Core/Core/Src/NetPlayClient.h | 13 +++- Source/Core/Core/Src/NetPlayServer.h | 1 + Source/Core/DolphinWX/Src/NetWindow.cpp | 75 +++++++++++++++------- 8 files changed, 149 insertions(+), 74 deletions(-) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index c61473d9199d..8bbee0d84fcb 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -168,16 +168,17 @@ void TraversalClient::ReconnectToServer() return; m_ServerAddress.port = 6262; + m_State = Connecting; + TraversalPacket hello = {0}; hello.type = TraversalPacketHelloFromClient; hello.helloFromClient.protoVersion = TraversalProtoVersion; RunOnThread([=]() { DO_ASSUME_ON(NET); SendPacket(hello); + if (m_Client) + m_Client->OnTraversalStateChanged(); }); - m_State = Connecting; - if (m_Client) - m_Client->OnTraversalStateChanged(); } u16 TraversalClient::GetPort() @@ -200,7 +201,7 @@ static ENetAddress MakeENetAddress(TraversalInetAddress* address) return eaddr; } -void TraversalClient::Connect(const std::string& host) +void TraversalClient::ConnectToClient(const std::string& host) { if (host.size() > sizeof(TraversalHostId)) { @@ -243,7 +244,7 @@ void TraversalClient::HandleServerPacket(TraversalPacket* packet) case TraversalPacketAck: if (!packet->ack.ok) { - OnConnectFailure(); + OnFailure(ServerForgotAboutUs); break; } for (auto it = m_OutgoingPackets.begin(); it != m_OutgoingPackets.end(); ++it) @@ -260,7 +261,7 @@ void TraversalClient::HandleServerPacket(TraversalPacket* packet) break; if (!packet->helloFromServer.ok) { - OnConnectFailure(); + OnFailure(VersionTooOld); break; } m_HostId = packet->helloFromServer.yourHostId; @@ -297,14 +298,13 @@ void TraversalClient::HandleServerPacket(TraversalPacket* packet) m_PendingConnect = false; - ENetAddress addr; + if (!m_Client) + break; + if (packet->type == TraversalPacketConnectReady) - addr = MakeENetAddress(&packet->connectReady.address); + m_Client->OnConnectReady(MakeENetAddress(&packet->connectReady.address)); else - addr.port = 0; - - if (m_Client) - m_Client->OnConnectReady(addr); + m_Client->OnConnectFailed(packet->connectFailed.reason); break; } @@ -323,13 +323,14 @@ void TraversalClient::HandleServerPacket(TraversalPacket* packet) buf.data = &ack; buf.dataLength = sizeof(ack); if (enet_socket_send(m_Host->socket, &m_ServerAddress, &buf, 1) == -1) - OnConnectFailure(); + OnFailure(SocketSendError); } } -void TraversalClient::OnConnectFailure() +void TraversalClient::OnFailure(int reason) { - m_State = ConnectFailure; + m_State = Failure; + m_FailureReason = reason; if (m_Client) m_Client->OnTraversalStateChanged(); } @@ -342,7 +343,7 @@ void TraversalClient::ResendPacket(OutgoingPacketInfo* info) buf.data = &info->packet; buf.dataLength = sizeof(info->packet); if (enet_socket_send(m_Host->socket, &m_ServerAddress, &buf, 1) == -1) - OnConnectFailure(); + OnFailure(SocketSendError); } void TraversalClient::HandleResends() @@ -354,7 +355,7 @@ void TraversalClient::HandleResends() { if (it->tries >= 5) { - OnConnectFailure(); + OnFailure(ResendTimeout); m_OutgoingPackets.clear(); break; } diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index 022dca742c1a..f4313e0521bb 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -41,9 +41,10 @@ class CopyAsMove class TraversalClientClient { public: - virtual void OnENetEvent(ENetEvent*) ASSUME_ON(NET) = 0; - virtual void OnTraversalStateChanged() ASSUME_ON(NET) = 0; - virtual void OnConnectReady(ENetAddress addr) ASSUME_ON(NET) = 0; + virtual void OnENetEvent(ENetEvent*) ON(NET) = 0; + virtual void OnTraversalStateChanged() ON(NET) = 0; + virtual void OnConnectReady(ENetAddress addr) ON(NET) = 0; + virtual void OnConnectFailed(u8 reason) ON(NET) = 0; }; class ENetHostClient @@ -77,18 +78,28 @@ class TraversalClient : public ENetHostClient { InitFailure, Connecting, - ConnectFailure, - Connected + Connected, + Failure + }; + + enum FailureReason + { + VersionTooOld = 0x300, + ServerForgotAboutUs, + SocketSendError, + ResendTimeout, + ConnectFailedError = 0x400, }; TraversalClient(const std::string& server); void Reset() ON(NET); - void Connect(const std::string& host) ON(NET); + void ConnectToClient(const std::string& host) ON(NET); void ReconnectToServer(); u16 GetPort(); TraversalHostId m_HostId; State m_State; + int m_FailureReason; protected: virtual void HandleResends() ON(NET); private: @@ -102,7 +113,7 @@ class TraversalClient : public ENetHostClient void HandleServerPacket(TraversalPacket* packet) ON(NET); void ResendPacket(OutgoingPacketInfo* info) ON(NET); TraversalRequestId SendPacket(const TraversalPacket& packet) ON(NET); - void OnConnectFailure() ON(NET); + void OnFailure(int reason) ON(NET); static int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) ASSUME_ON(NET); void HandlePing() ON(NET); diff --git a/Source/Core/Common/Src/TraversalProto.h b/Source/Core/Common/Src/TraversalProto.h index 0f06dd221062..6f314d99ca00 100644 --- a/Source/Core/Common/Src/TraversalProto.h +++ b/Source/Core/Common/Src/TraversalProto.h @@ -32,6 +32,14 @@ enum { TraversalProtoVersion = 0 }; + +enum TraversalConnectFailedReason +{ + TraversalConnectFailedClientDidntRespond = 0, + TraversalConnectFailedClientFailure, + TraversalConnectFailedNoSuchClient +}; + #pragma pack(push, 1) struct TraversalInetAddress { @@ -79,6 +87,7 @@ struct TraversalPacket struct { TraversalRequestId requestId; + u8 reason; } connectFailed; }; }; diff --git a/Source/Core/Common/Src/TraversalServer.cpp b/Source/Core/Common/Src/TraversalServer.cpp index e88c2d504bd9..46f2329a1a84 100644 --- a/Source/Core/Common/Src/TraversalServer.cpp +++ b/Source/Core/Common/Src/TraversalServer.cpp @@ -255,6 +255,7 @@ static void ResendPackets() TraversalPacket* fail = AllocPacket(MakeSinAddr(it->first)); fail->type = TraversalPacketConnectFailed; fail->connectFailed.requestId = it->second; + fail->connectFailed.reason = TraversalConnectFailedClientDidntRespond; } } @@ -287,6 +288,7 @@ static void HandlePacket(TraversalPacket* packet, struct sockaddr_in6* addr) { ready->type = TraversalPacketConnectFailed; ready->connectFailed.requestId = info->misc; + ready->connectFailed.reason = TraversalConnectFailedClientFailure; } } @@ -338,6 +340,7 @@ static void HandlePacket(TraversalPacket* packet, struct sockaddr_in6* addr) TraversalPacket* reply = AllocPacket(*addr); reply->type = TraversalPacketConnectFailed; reply->connectFailed.requestId = packet->requestId; + reply->connectFailed.reason = TraversalConnectFailedNoSuchClient; } else { diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 3ed0a49d382e..e564131a2ca2 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -62,7 +62,6 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam m_state = Failure; m_dialog = NULL; m_host = NULL; - m_server_error = 0; m_state_callback = stateCallback; m_local_name = name; @@ -132,10 +131,15 @@ void NetPlayClient::OnData(Packet&& packet) { std::lock_guard lk(m_crit); - packet.Do(m_server_error); + MessageId server_error; + packet.Do(server_error); packet.Do(m_pid); - if (packet.failure || m_server_error) - return OnDisconnect(); + + if (packet.failure) + return OnDisconnect(InvalidPacket); + + if (server_error) + return OnDisconnect(ServerError + server_error); Player player; player.name = m_local_name; @@ -156,7 +160,7 @@ void NetPlayClient::OnData(Packet&& packet) MessageId mid; packet.Do(mid); if (packet.failure) - return OnDisconnect(); + return OnDisconnect(InvalidPacket); switch (mid) { @@ -167,7 +171,7 @@ void NetPlayClient::OnData(Packet&& packet) packet.Do(player.name); packet.Do(player.revision); if (packet.failure) - return OnDisconnect(); + return OnDisconnect(InvalidPacket); { std::lock_guard lk(m_crit); @@ -183,7 +187,7 @@ void NetPlayClient::OnData(Packet&& packet) PlayerId pid; packet.Do(pid); if (packet.failure) - return OnDisconnect(); + return OnDisconnect(InvalidPacket); { std::lock_guard lk(m_crit); @@ -201,7 +205,7 @@ void NetPlayClient::OnData(Packet&& packet) packet.Do(pid); packet.Do(msg); if (packet.failure) - return OnDisconnect(); + return OnDisconnect(InvalidPacket); { std::lock_guard lk(m_crit); @@ -223,7 +227,7 @@ void NetPlayClient::OnData(Packet&& packet) packet.Do(pid); packet.Do(name); if (packet.failure) - return OnDisconnect(); + return OnDisconnect(InvalidPacket); { std::lock_guard lk(m_crit); @@ -240,7 +244,7 @@ void NetPlayClient::OnData(Packet&& packet) packet.DoArray(m_pad_map, 4); if (packet.failure) - return OnDisconnect(); + return OnDisconnect(InvalidPacket); UpdateDevices(); @@ -253,7 +257,7 @@ void NetPlayClient::OnData(Packet&& packet) packet.DoArray(m_wiimote_map, 4); if (packet.failure) - return OnDisconnect(); + return OnDisconnect(InvalidPacket); m_dialog->Update(); } @@ -267,7 +271,7 @@ void NetPlayClient::OnData(Packet&& packet) packet.Do(np.nHi); packet.Do(np.nLo); if (packet.failure || map < 0 || map >= 4) - return OnDisconnect(); + return OnDisconnect(InvalidPacket); // add to pad buffer m_pad_buffer[map].Push(np); @@ -281,7 +285,7 @@ void NetPlayClient::OnData(Packet&& packet) packet.Do(map); packet.Do(nw); if (packet.failure || map < 0 || map >= 4) - return OnDisconnect(); + return OnDisconnect(InvalidPacket); // add to wiimote buffer m_wiimote_buffer[(unsigned)map].Push(std::move(nw)); @@ -294,7 +298,7 @@ void NetPlayClient::OnData(Packet&& packet) u32 size = 0; packet.Do(size); if (packet.failure) - return OnDisconnect(); + return OnDisconnect(InvalidPacket); m_target_buffer_size = size; } @@ -324,7 +328,7 @@ void NetPlayClient::OnData(Packet&& packet) packet.Do(tmp); g_NetPlaySettings.m_EXIDevice[1] = (TEXIDevices) tmp; if (packet.failure) - return OnDisconnect(); + return OnDisconnect(InvalidPacket); } m_dialog->OnMsgStartGame(); @@ -351,7 +355,7 @@ void NetPlayClient::OnData(Packet&& packet) u32 ping_key = 0; packet.Do(ping_key); if (packet.failure) - return OnDisconnect(); + return OnDisconnect(InvalidPacket); Packet pong; pong.W((MessageId)NP_MSG_PONG); @@ -369,7 +373,7 @@ void NetPlayClient::OnData(Packet&& packet) packet.Do(pid); packet.Do(ping); if (packet.failure) - return OnDisconnect(); + return OnDisconnect(InvalidPacket); { std::lock_guard lk(m_crit); @@ -388,7 +392,7 @@ void NetPlayClient::OnData(Packet&& packet) } } -void NetPlayClient::OnDisconnect() +void NetPlayClient::OnDisconnect(int reason) { std::lock_guard lk(m_crit); if (m_state == Connected) @@ -401,6 +405,7 @@ void NetPlayClient::OnDisconnect() } m_is_running = false; m_state = Failure; + m_failure_reason = reason; if (m_state_callback) m_state_callback(this); } @@ -425,7 +430,7 @@ void NetPlayClient::OnENetEvent(ENetEvent* event) break; } case ENET_EVENT_TYPE_DISCONNECT: - OnDisconnect(); + OnDisconnect(ReceivedENetDisconnect); break; case ENET_EVENT_TYPE_RECEIVE: OnData(ENetUtil::MakePacket(event->packet)); @@ -443,12 +448,12 @@ void NetPlayClient::OnTraversalStateChanged() m_state = WaitingForTraversalClientConnectReady; if (m_state_callback) m_state_callback(this); - g_TraversalClient->Connect(m_host_spec); + g_TraversalClient->ConnectToClient(m_host_spec); } else if (m_state != Failure && - g_TraversalClient->m_State == TraversalClient::ConnectFailure) + g_TraversalClient->m_State == TraversalClient::Failure) { - OnDisconnect(); + OnDisconnect(g_TraversalClient->m_FailureReason); } } @@ -456,15 +461,20 @@ void NetPlayClient::OnConnectReady(ENetAddress addr) { if (m_state == WaitingForTraversalClientConnectReady) { - if (addr.port != 0) - DoDirectConnect(addr); - else - m_state = Failure; + DoDirectConnect(addr); // sets m_state if (m_state_callback) m_state_callback(this); } } +void NetPlayClient::OnConnectFailed(u8 reason) +{ + m_state = Failure; + m_failure_reason = ServerError + reason; + if (m_state_callback) + m_state_callback(this); +} + void NetPlayClient::GetPlayerList(std::string& list, std::vector& pid_list) { std::lock_guard lk(m_crit); diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 46aaf43ae5bf..0498203f224c 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -68,6 +68,14 @@ class NetPlayClient : public TraversalClientClient void GetPlayerList(std::string& list, std::vector& pid_list) ASSUME_ON(GUI); void GetPlayers(std::vector& player_list) ASSUME_ON(GUI); + enum FailureReason + { + ServerFull = 0x100, + InvalidPacket, + ReceivedENetDisconnect, + ServerError = 0x200, + }; + enum State { WaitingForTraversalClientConnection, @@ -77,7 +85,7 @@ class NetPlayClient : public TraversalClientClient Connected, Failure } m_state; - MessageId m_server_error; + int m_failure_reason; bool StartGame(const std::string &path) ASSUME_ON(GUI); bool StopGame() /* multiple threads */; @@ -100,6 +108,7 @@ class NetPlayClient : public TraversalClientClient virtual void OnENetEvent(ENetEvent*) override ON(NET); virtual void OnTraversalStateChanged() override ON(NET); virtual void OnConnectReady(ENetAddress addr) override ON(NET); + virtual void OnConnectFailed(u8 reason) override ON(NET); std::function m_state_callback; protected: @@ -136,7 +145,7 @@ class NetPlayClient : public TraversalClientClient void SendPadState(const PadMapping in_game_pad, const NetPad& np) ASSUME_ON(CPU); void SendWiimoteState(const PadMapping in_game_pad, const NetWiimote& nw) ASSUME_ON(CPU); void OnData(Packet&& packet) ON(NET); - void OnDisconnect() ON(NET); + void OnDisconnect(int reason) ON(NET); void SendPacket(Packet& packet); void DoDirectConnect(const ENetAddress& addr); diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index a70e3df0bcdc..5332bde87fc3 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -44,6 +44,7 @@ class NetPlayServer : public TraversalClientClient virtual void OnENetEvent(ENetEvent*) override ON(NET); virtual void OnTraversalStateChanged() override ON(NET); virtual void OnConnectReady(ENetAddress addr) override {} + virtual void OnConnectFailed(u8 reason) override ON(NET) {} private: class Client { diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 284f5d012ded..1281cf5f3a3c 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -21,6 +21,56 @@ #define NETPLAY_TITLEBAR "Dolphin NetPlay" #define INITIAL_PAD_BUFFER_SIZE 20 +static wxString FailureReasonStringForHost(int reason) +{ + switch (reason) + { + case TraversalClient::VersionTooOld: + return _("(Error: Dolphin too old)"); + case TraversalClient::ServerForgotAboutUs: + return _("(Error: Disconnected)"); + case TraversalClient::SocketSendError: + return _("(Error: Socket)"); + case TraversalClient::ResendTimeout: + return _("(Error: Timeout)"); + default: + return _("(Error: Unknown)"); + } +} + +static wxString FailureReasonStringForConnect(int reason) +{ + switch (reason) + { + case TraversalClient::VersionTooOld: + return _("Dolphin too old for traversal server"); + case TraversalClient::ServerForgotAboutUs: + return _("Disconnected from traversal server"); + case TraversalClient::SocketSendError: + return _("Socket error sending to traversal server"); + case TraversalClient::ResendTimeout: + return _("Timeout connecting to traversal server"); + case TraversalClient::ConnectFailedError + TraversalConnectFailedClientDidntRespond: + return _("Traversal server timed out connecting to the host"); + case TraversalClient::ConnectFailedError + TraversalConnectFailedClientFailure: + return _("Server rejected traversal attempt"); + case TraversalClient::ConnectFailedError + TraversalConnectFailedNoSuchClient: + return _("Invalid host"); + case NetPlayClient::ServerError + CON_ERR_SERVER_FULL: + return _("Server full"); + case NetPlayClient::ServerError + CON_ERR_GAME_RUNNING: + return _("Game already running"); + case NetPlayClient::ServerError + CON_ERR_VERSION_MISMATCH: + return _("Dolphin version mismatch"); + case NetPlayClient::InvalidPacket: + return _("Bad packet from server"); + case NetPlayClient::ReceivedENetDisconnect: + return _("Disconnected"); + default: + return _("Unknown error"); + } +} + BEGIN_EVENT_TABLE(NetPlayDiag, wxFrame) EVT_COMMAND(wxID_ANY, wxEVT_THREAD, NetPlayDiag::OnThread) END_EVENT_TABLE() @@ -305,9 +355,9 @@ void NetPlayDiag::OnThread(wxCommandEvent& event) m_host_copy_btn->Enable(); m_host_copy_btn_is_retry = false; break; - case TraversalClient::ConnectFailure: + case TraversalClient::Failure: m_host_label->SetForegroundColour(*wxBLACK); - m_host_label->SetLabel(_("failure xxx")); + m_host_label->SetLabel(FailureReasonStringForHost(g_TraversalClient->m_FailureReason)); m_host_copy_btn->SetLabel(_("Retry")); m_host_copy_btn->Enable(); m_host_copy_btn_is_retry = true; @@ -480,26 +530,7 @@ void ConnectDiag::OnThread(wxCommandEvent& event) } else { - wxString err; - switch (netplay_client->m_server_error) - { - case 0: - // there was no server error. - err = _("Failed to connect."); - break; - case CON_ERR_SERVER_FULL : - err = _("The server is full."); - break; - case CON_ERR_VERSION_MISMATCH : - err = _("The server and client's NetPlay versions are incompatible."); - break; - case CON_ERR_GAME_RUNNING : - err = _("The server responded: the game is currently running."); - break; - default : - err = _("The server sent an unknown error message."); - break; - } + wxString err = FailureReasonStringForConnect(netplay_client->m_failure_reason); netplay_client.reset(); // connection failure auto complain = new wxMessageDialog(this, err); From 10c6bf1db057af4d60a84dce2e87b11ba7daff10 Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 15 Oct 2013 21:17:30 -0400 Subject: [PATCH 027/202] Automatically reconnect when a new NetPlay{Client,Server} is created with a disconnected TraversalClient in the background. The backgrounding is, for the record, intentional, to decrease latency for repeated connection attempts, and for host ID consistency. --- Source/Core/Core/Src/NetPlayClient.cpp | 8 +++++--- Source/Core/Core/Src/NetPlayServer.cpp | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index e564131a2ca2..4c0be15884a6 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -92,10 +92,12 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam m_direct_connection = false; std::string server = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayCentralServer; EnsureTraversalClient(server); - if (!g_TraversalClient) - return; - if (g_TraversalClient->m_State == TraversalClient::InitFailure) + if (!g_TraversalClient || + g_TraversalClient->m_State == TraversalClient::InitFailure) return; + // If we were disconnected in the background, reconnect. + if (g_TraversalClient->m_State == TraversalClient::Failure) + g_TraversalClient->ReconnectToServer(); g_TraversalClient->m_Client = this; m_host_spec = hostSpec; m_host = g_TraversalClient->m_Host; diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index c11acc1dc343..cd4b933113db 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -23,6 +23,9 @@ NetPlayServer::NetPlayServer() g_TraversalClient->m_Client = this; m_host = g_TraversalClient->m_Host; + + if (g_TraversalClient->m_State == TraversalClient::Failure) + g_TraversalClient->ReconnectToServer(); } void NetPlayServer::UpdatePings() From 1c1cb33def0001f0d57ed673e5331c44a059b237 Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 15 Oct 2013 21:25:29 -0400 Subject: [PATCH 028/202] UI fix --- Source/Core/DolphinWX/Src/NetWindow.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 1281cf5f3a3c..9cecc74cd3dd 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -132,7 +132,7 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const if (is_hosting) { wxBoxSizer* const host_szr = new wxBoxSizer(wxHORIZONTAL); - host_szr->Add(new wxStaticText(panel, wxID_ANY, _("Host:")), 0, wxCENTER); + host_szr->Add(new wxStaticText(panel, wxID_ANY, _("ID:")), 0, wxCENTER); // The initial label is for sizing... m_host_label = new wxStaticText(panel, wxID_ANY, "555.555.555.555:5555", wxDefaultPosition, wxDefaultSize, wxST_NO_AUTORESIZE | wxALIGN_LEFT); // Update() should fix this immediately. @@ -478,14 +478,14 @@ ConnectDiag::ConnectDiag(wxWindow* parent) { wxBoxSizer* sizerTop = new wxBoxSizer(wxVERTICAL); std::string host = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayHost; - wxStaticText* hostLabel = new wxStaticText(this, wxID_ANY, _("Host :")); + wxStaticText* hostLabel = new wxStaticText(this, wxID_ANY, _("Host or ID:")); m_HostCtrl = new wxTextCtrl(this, wxID_ANY, StrToWxStr(host)); // focus and select all m_HostCtrl->SetFocus(); m_HostCtrl->SetSelection(-1, -1); m_HostCtrl->Bind(wxEVT_TEXT, &ConnectDiag::OnChange, this); wxBoxSizer* sizerHost = new wxBoxSizer(wxHORIZONTAL); - sizerHost->Add(hostLabel, 0, wxLEFT, 5); + sizerHost->Add(hostLabel, 0, wxLEFT | wxCENTER, 5); sizerHost->Add(m_HostCtrl, 1, wxEXPAND | wxLEFT | wxRIGHT, 5); wxStdDialogButtonSizer* sizerButtons = CreateStdDialogButtonSizer(wxOK | wxCANCEL); m_ConnectBtn = sizerButtons->GetAffirmativeButton(); @@ -571,9 +571,12 @@ bool ConnectDiag::IsHostOk() ConnectDiag::~ConnectDiag() { - netplay_client->m_state_callback = nullptr; - if (GetReturnCode() != 0) - netplay_client.reset(); + if (netplay_client) + { + netplay_client->m_state_callback = nullptr; + if (GetReturnCode() != 0) + netplay_client.reset(); + } SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayHost = StripSpaces(WxStrToStr(m_HostCtrl->GetValue())); SConfig::GetInstance().SaveSettings(); } From 2467f85140003ed3e39b3de7f59fb029d3b42291 Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 15 Oct 2013 22:22:36 -0400 Subject: [PATCH 029/202] Fix the thread safety stuff (these changes fell into the reflog-void somehow) --- CMakeLists.txt | 5 +++-- Source/Core/Common/Src/Common.h | 4 +--- Source/Core/Common/Src/TraversalClient.cpp | 6 +++-- Source/Core/Common/Src/TraversalClient.h | 6 ++--- Source/Core/Core/Src/NetPlayClient.cpp | 6 ++--- Source/Core/Core/Src/NetPlayClient.h | 26 +++++++++++----------- Source/Core/Core/Src/NetPlayServer.cpp | 2 +- Source/Core/Core/Src/NetPlayServer.h | 14 ++++++------ 8 files changed, 35 insertions(+), 34 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a9e836abbc4..30e6c4d5dcba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -181,8 +181,9 @@ if(APPLE) set(ENV{PATH} /usr/bin:/bin:/usr/sbin:/sbin) # Some of our code contains Objective C constructs. - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -x objective-c -stdlib=libc++ -Werror=thread-safety") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -x objective-c++ -stdlib=libc++ -Werror=thread-safety") + set(CLANG_FLAGS "-stdlib=libc++ -Werror=thread-safety -Werror=thread-safety-beta") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -x objective-c ${CLANG_FLAGS}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -x objective-c++ ${CLANG_FLAGS}") # Avoid mistaking an object file for a source file on the link command line. set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -x none") diff --git a/Source/Core/Common/Src/Common.h b/Source/Core/Common/Src/Common.h index 463660853343..6553f680c183 100644 --- a/Source/Core/Common/Src/Common.h +++ b/Source/Core/Common/Src/Common.h @@ -184,7 +184,7 @@ enum EMUSTATE_CHANGE Optional annotations to denote thread roles. Example: // entry point known to be on CPU thread - void foo() ASSUME_ON_CPU; + { ... ASSUME_ON(CPU); ... } // must be called by someone ON(CPU) or ASSUME_ON(CPU) void foo() ON(CPU) { ... // guarding variables @@ -239,7 +239,5 @@ DEFINE_THREAD_HAT(GUI); #define IGNORE_THREAD_SAFETY \ _TS_MACRO(__attribute__((no_thread_safety_analysis))) #define ASSUME_ON(name) \ - _TS_MACRO(__attribute__((exclusive_trylock_function(12345, name##ThreadHat)))) -#define DO_ASSUME_ON(name) \ _TS_MACRO(name##ThreadHatLock __##name##__thlock) #endif // _COMMON_H_ diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index 8bbee0d84fcb..6c2061ac6de9 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -86,7 +86,7 @@ ENetHostClient::~ENetHostClient() if (m_Host) { RunOnThread([=]() { - DO_ASSUME_ON(NET); + ASSUME_ON(NET); m_ShouldEndThread = true; }); m_Thread.join(); @@ -114,6 +114,7 @@ void ENetHostClient::Reset() void ENetHostClient::ThreadFunc() { + ASSUME_ON(NET); Common::SetCurrentThreadName(m_isTraversalClient ? "TraversalClient thread" : "ENetHostClient thread"); while (1) { @@ -174,7 +175,7 @@ void TraversalClient::ReconnectToServer() hello.type = TraversalPacketHelloFromClient; hello.helloFromClient.protoVersion = TraversalProtoVersion; RunOnThread([=]() { - DO_ASSUME_ON(NET); + ASSUME_ON(NET); SendPacket(hello); if (m_Client) m_Client->OnTraversalStateChanged(); @@ -217,6 +218,7 @@ void TraversalClient::ConnectToClient(const std::string& host) int ENET_CALLBACK TraversalClient::InterceptCallback(ENetHost* host, ENetEvent* event) { + ASSUME_ON(NET); const ENetAddress* addr = &host->receivedAddress; auto self = (TraversalClient*) host->compressor.destroy; if (addr->host == self->m_ServerAddress.host && diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index f4313e0521bb..a73c2ad3f656 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -21,7 +21,7 @@ namespace ENetUtil void SendPacket(ENetPeer* peer, const Packet& pac) ON(NET); Packet MakePacket(ENetPacket* epacket); void Wakeup(ENetHost* host); - int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) ASSUME_ON(NET); + int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) /* ON(NET) */; } // Apparently nobody on the C++11 standards committee thought of @@ -61,7 +61,7 @@ class ENetHostClient protected: virtual void HandleResends() ON(NET) {} private: - void ThreadFunc() ASSUME_ON(NET); + void ThreadFunc() /* ON(NET) */; Common::FifoQueue, false> m_RunQueue; std::thread m_Thread; @@ -114,7 +114,7 @@ class TraversalClient : public ENetHostClient void ResendPacket(OutgoingPacketInfo* info) ON(NET); TraversalRequestId SendPacket(const TraversalPacket& packet) ON(NET); void OnFailure(int reason) ON(NET); - static int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) ASSUME_ON(NET); + static int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) /* ON(NET) */; void HandlePing() ON(NET); TraversalRequestId m_ConnectRequestId; diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 4c0be15884a6..ed5f0de52d78 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -122,7 +122,7 @@ void NetPlayClient::SendPacket(Packet& packet) { CopyAsMove tmp(std::move(packet)); g_TraversalClient->RunOnThread([=]() mutable { - DO_ASSUME_ON(NET); + ASSUME_ON(NET); ENetUtil::BroadcastPacket(m_host, *tmp); }); } @@ -537,7 +537,7 @@ void NetPlayClient::SendChatMessage(const std::string& msg) void NetPlayClient::ChangeName(const std::string& name) { g_TraversalClient->RunOnThread([=]() { - DO_ASSUME_ON(NET); + ASSUME_ON(NET); std::lock_guard lk(m_crit); m_local_name = name; if (m_pid != (PlayerId) -1) @@ -874,7 +874,7 @@ void NetPlayClient::Stop() return; g_TraversalClient->RunOnThread([=]() mutable { bool isPadMapped = false; - DO_ASSUME_ON(NET); + ASSUME_ON(NET); for (unsigned int i = 0; i < 4; ++i) { if (m_pad_map[i] == m_pid) diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 0498203f224c..0223e950173b 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -65,8 +65,8 @@ class NetPlayClient : public TraversalClientClient NetPlayClient(const std::string& hostSpec, const std::string& name, std::function stateCallback); ~NetPlayClient(); - void GetPlayerList(std::string& list, std::vector& pid_list) ASSUME_ON(GUI); - void GetPlayers(std::vector& player_list) ASSUME_ON(GUI); + void GetPlayerList(std::string& list, std::vector& pid_list) /* ON(GUI) */; + void GetPlayers(std::vector& player_list) /* ON(GUI) */; enum FailureReason { @@ -87,16 +87,16 @@ class NetPlayClient : public TraversalClientClient } m_state; int m_failure_reason; - bool StartGame(const std::string &path) ASSUME_ON(GUI); + bool StartGame(const std::string &path) /* ON(GUI) */; bool StopGame() /* multiple threads */; - void Stop() ASSUME_ON(GUI); - bool ChangeGame(const std::string& game) ASSUME_ON(GUI); - void SendChatMessage(const std::string& msg) ASSUME_ON(GUI); - void ChangeName(const std::string& name) ASSUME_ON(GUI); + void Stop() /* ON(GUI) */; + bool ChangeGame(const std::string& game) /* ON(GUI) */; + void SendChatMessage(const std::string& msg) /* ON(GUI) */; + void ChangeName(const std::string& name) /* ON(GUI) */; // Send and receive pads values - bool WiimoteUpdate(int _number, u8* data, const u8 size) ASSUME_ON(CPU); - bool GetNetPads(const u8 pad_nb, const SPADStatus* const, NetPad* const netvalues) ASSUME_ON(CPU); + bool WiimoteUpdate(int _number, u8* data, const u8 size) /* ON(CPU) */; + bool GetNetPads(const u8 pad_nb, const SPADStatus* const, NetPad* const netvalues) /* ON(CPU) */; u8 LocalPadToInGamePad(u8 localPad); u8 InGamePadToLocalPad(u8 localPad); @@ -112,7 +112,7 @@ class NetPlayClient : public TraversalClientClient std::function m_state_callback; protected: - void ClearBuffers() ON(NET); + void ClearBuffers() /* on multiple */; std::recursive_mutex m_crit; @@ -141,9 +141,9 @@ class NetPlayClient : public TraversalClientClient bool m_is_recording; private: - void UpdateDevices() ON(NET); - void SendPadState(const PadMapping in_game_pad, const NetPad& np) ASSUME_ON(CPU); - void SendWiimoteState(const PadMapping in_game_pad, const NetWiimote& nw) ASSUME_ON(CPU); + void UpdateDevices() /* on multiple, this sucks */; + void SendPadState(const PadMapping in_game_pad, const NetPad& np) /* ON(CPU) */; + void SendWiimoteState(const PadMapping in_game_pad, const NetWiimote& nw) /* ON(CPU) */; void OnData(Packet&& packet) ON(NET); void OnDisconnect(int reason) ON(NET); void SendPacket(Packet& packet); diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index cd4b933113db..8963bc6cf539 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -459,7 +459,7 @@ void NetPlayServer::SendToClients(Packet& packet, const PlayerId skip_pid) { CopyAsMove tmp(std::move(packet)); g_TraversalClient->RunOnThread([=]() mutable { - DO_ASSUME_ON(NET); + ASSUME_ON(NET); SendToClientsOnThread(*tmp, skip_pid); }); } diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 5332bde87fc3..0d10e60b7466 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -25,17 +25,17 @@ class NetPlayServer : public TraversalClientClient NetPlayServer(); ~NetPlayServer(); - bool ChangeGame(const std::string& game) ASSUME_ON(GUI); + bool ChangeGame(const std::string& game) /* ON(GUI) */; - void SetNetSettings(const NetSettings &settings) ASSUME_ON(GUI); + void SetNetSettings(const NetSettings &settings) /* ON(GUI) */; - bool StartGame(const std::string &path) ASSUME_ON(GUI); + bool StartGame(const std::string &path) /* ON(GUI) */; - void GetPadMapping(PadMapping map[]) ASSUME_ON(GUI); - void SetPadMapping(const PadMapping map[]) ASSUME_ON(GUI); + void GetPadMapping(PadMapping map[]) /* ON(GUI) */; + void SetPadMapping(const PadMapping map[]) /* ON(GUI) */; - void GetWiimoteMapping(PadMapping map[]) ASSUME_ON(GUI); - void SetWiimoteMapping(const PadMapping map[]) ASSUME_ON(GUI); + void GetWiimoteMapping(PadMapping map[]) /* ON(GUI) */; + void SetWiimoteMapping(const PadMapping map[]) /* ON(GUI) */; void AdjustPadBufferSize(unsigned int size) /* multiple threads */; From f06f17066aead90a9bba1d1e9c2a60efccb112a3 Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 15 Oct 2013 23:18:09 -0400 Subject: [PATCH 030/202] Add a GUI option to get the actual host:port (for your choice of interface). --- CMakeLists.txt | 1 + Source/Core/Core/Src/NetPlayServer.cpp | 113 ++++++++++++++++++++++++ Source/Core/Core/Src/NetPlayServer.h | 10 +++ Source/Core/DolphinWX/CMakeLists.txt | 4 + Source/Core/DolphinWX/Src/NetWindow.cpp | 62 +++++++++++-- Source/Core/DolphinWX/Src/NetWindow.h | 3 + 6 files changed, 187 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 30e6c4d5dcba..442dae94873a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -242,6 +242,7 @@ if(APPLE) find_library(IOB_LIBRARY IOBluetooth) find_library(IOK_LIBRARY IOKit) find_library(QUICKTIME_LIBRARY QuickTime) + find_library(SYSTEMCONFIGURATION_LIBRARY SystemConfiguration) find_library(WEBKIT_LIBRARY WebKit) endif() diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 8963bc6cf539..2aee1b8d4995 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -5,8 +5,24 @@ #include "NetPlayServer.h" #include "NetPlayClient.h" // for NetPlayUI +#if defined(__APPLE__) +#include +#include +#elif !defined(__WIN32__) +#include +#include +#include +#include +#endif + NetPlayServer::~NetPlayServer() { +#ifdef __APPLE__ + if (m_dynamic_store) + CFRelease(m_dynamic_store); + if (m_prefs) + CFRelease(m_prefs); +#endif // leave the host open for future use g_TraversalClient->Reset(); } @@ -20,6 +36,10 @@ NetPlayServer::NetPlayServer() memset(m_pad_map, -1, sizeof(m_pad_map)); memset(m_wiimote_map, -1, sizeof(m_wiimote_map)); m_target_buffer_size = 20; +#ifdef __APPLE__ + m_dynamic_store = SCDynamicStoreCreate(NULL, CFSTR("NetPlayServer"), NULL, NULL); + m_prefs = SCPreferencesCreate(NULL, CFSTR("NetPlayServer"), NULL); +#endif g_TraversalClient->m_Client = this; m_host = g_TraversalClient->m_Host; @@ -69,6 +89,99 @@ void NetPlayServer::OnTraversalStateChanged() m_dialog->Update(); } +#if defined(__APPLE__) +std::string CFStrToStr(CFStringRef cfstr) +{ + if (!cfstr) + return ""; + if (const char* ptr = CFStringGetCStringPtr(cfstr, kCFStringEncodingUTF8)) + return ptr; + char buf[512]; + if (CFStringGetCString(cfstr, buf, sizeof(buf), kCFStringEncodingUTF8)) + return buf; + return ""; +} +#endif + +std::vector> NetPlayServer::GetInterfaceListInternal() +{ + std::vector> result; +#if defined(_WIN32) + +#elif defined(__APPLE__) + // we do this to get the friendly names rather than the BSD ones. ew. + if (m_dynamic_store && m_prefs) + { + CFArrayRef ary = SCNetworkServiceCopyAll((SCPreferencesRef) m_prefs); + for (CFIndex i = 0; i < CFArrayGetCount(ary); i++) + { + SCNetworkServiceRef ifr = (SCNetworkServiceRef) CFArrayGetValueAtIndex(ary, i); + std::string name = CFStrToStr(SCNetworkServiceGetName(ifr)); + CFStringRef key = SCDynamicStoreKeyCreateNetworkServiceEntity(NULL, kSCDynamicStoreDomainState, SCNetworkServiceGetServiceID(ifr), kSCEntNetIPv4); + CFDictionaryRef props = (CFDictionaryRef) SCDynamicStoreCopyValue((SCDynamicStoreRef) m_dynamic_store, key); + CFRelease(key); + if (!props) + continue; + CFArrayRef ipary = (CFArrayRef) CFDictionaryGetValue(props, kSCPropNetIPv4Addresses); + if (ipary) + { + for (CFIndex j = 0; j < CFArrayGetCount(ipary); j++) + result.push_back(std::make_pair(name, CFStrToStr((CFStringRef) CFArrayGetValueAtIndex(ipary, j)))); + CFRelease(ipary); + } + } + CFRelease(ary); + } +#else + struct ifaddrs* ifp; + char buf[512]; + if (getifaddrs(&ifp) != -1) + { + for (struct ifaddrs* curifp = ifp; curifp; curifp = curifp->ifa_next) + { + struct sockaddr* sa = curifp->ifa_addr; + if (sa->sa_family != AF_INET) + continue; + struct sockaddr_in* sai = (struct sockaddr_in*) sa; + if (ntohl(((struct sockaddr_in*) sa)->sin_addr.s_addr) == 0x7f000001) + continue; + const char *ip = inet_ntop(sa->sa_family, &sai->sin_addr, buf, sizeof(buf)); + if (ip == NULL) + continue; + result.push_back(std::make_pair(curifp->ifa_name, ip)); + } + freeifaddrs(ifp); + } +#endif + if (result.empty()) + result.push_back(std::make_pair("?", "127.0.0.1:")); + return result; +} + +std::unordered_set NetPlayServer::GetInterfaceSet() +{ + std::unordered_set result; + auto lst = GetInterfaceListInternal(); + for (auto it = lst.begin(); it != lst.end(); ++it) + result.insert(it->first); + return result; +} + +std::string NetPlayServer::GetInterfaceHost(std::string interface) +{ + char buf[16]; + sprintf(buf, ":%d", g_TraversalClient->GetPort()); + auto lst = GetInterfaceListInternal(); + for (auto it = lst.begin(); it != lst.end(); ++it) + { + if (it->first == interface) + { + return it->second + buf; + } + } + return "?"; +} + MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) { Client& player = m_players[pid]; diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 0d10e60b7466..63ebc0211319 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -16,6 +16,7 @@ #include "TraversalClient.h" #include +#include class NetPlayUI; @@ -45,6 +46,9 @@ class NetPlayServer : public TraversalClientClient virtual void OnTraversalStateChanged() override ON(NET); virtual void OnConnectReady(ENetAddress addr) override {} virtual void OnConnectFailed(u8 reason) override ON(NET) {} + + std::unordered_set GetInterfaceSet(); + std::string GetInterfaceHost(std::string interface); private: class Client { @@ -66,6 +70,7 @@ class NetPlayServer : public TraversalClientClient void UpdatePadMapping() /* multiple threads */; void UpdateWiimoteMapping() /* multiple threads */; void UpdatePings() ON(NET); + std::vector> GetInterfaceListInternal(); NetSettings m_settings; @@ -88,6 +93,11 @@ class NetPlayServer : public TraversalClientClient ENetHost* m_host; NetPlayUI* m_dialog; + +#if defined(__APPLE__) + const void* m_dynamic_store; + const void* m_prefs; +#endif }; #endif diff --git a/Source/Core/DolphinWX/CMakeLists.txt b/Source/Core/DolphinWX/CMakeLists.txt index 6bec0bc19c2c..1aa7ea0aefc2 100644 --- a/Source/Core/DolphinWX/CMakeLists.txt +++ b/Source/Core/DolphinWX/CMakeLists.txt @@ -47,6 +47,10 @@ if(LIBAV_FOUND) set(LIBS ${LIBS} ${LIBAV_LIBRARIES}) endif() +if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + set(LIBS ${LIBS} ${SYSTEMCONFIGURATION_LIBRARY}) +endif() + if(wxWidgets_FOUND) set(SRCS Src/ARCodeAddEdit.cpp diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 9cecc74cd3dd..6cab8fbe21b1 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -132,12 +132,15 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const if (is_hosting) { wxBoxSizer* const host_szr = new wxBoxSizer(wxHORIZONTAL); - host_szr->Add(new wxStaticText(panel, wxID_ANY, _("ID:")), 0, wxCENTER); + m_host_type_choice = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxSize(60, -1)); + m_host_type_choice->Bind(wxEVT_COMMAND_CHOICE_SELECTED, &NetPlayDiag::OnChoice, this); + m_host_type_choice->Append(_("ID:")); + host_szr->Add(m_host_type_choice); // The initial label is for sizing... - m_host_label = new wxStaticText(panel, wxID_ANY, "555.555.555.555:5555", wxDefaultPosition, wxDefaultSize, wxST_NO_AUTORESIZE | wxALIGN_LEFT); + m_host_label = new wxStaticText(panel, wxID_ANY, "555.555.555.555:55555", wxDefaultPosition, wxDefaultSize, wxST_NO_AUTORESIZE | wxALIGN_LEFT); // Update() should fix this immediately. m_host_label->SetLabel(_("")); - host_szr->Add(m_host_label, 1, wxCENTER); + host_szr->Add(m_host_label, 1, wxLEFT | wxCENTER, 5); m_host_copy_btn = new wxButton(panel, wxID_ANY, _("Copy")); m_host_copy_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &NetPlayDiag::OnCopyIP, this); m_host_copy_btn->Disable(); @@ -335,11 +338,19 @@ void NetPlayDiag::OnQuit(wxCommandEvent&) Destroy(); } -// update gui -void NetPlayDiag::OnThread(wxCommandEvent& event) +void NetPlayDiag::UpdateHostLabel() { - if (m_is_hosting) + wxString label = _(" (internal IP)"); + auto DeLabel = [=](wxString str) { + return WxStrToStr(str.Left(str.Len() - label.Len())); + }; + auto EnLabel = [=](std::string str) { + return StrToWxStr(str) + label; + }; + int sel = m_host_type_choice->GetSelection(); + if (sel == 0) { + // the traversal ID switch (g_TraversalClient->m_State) { case TraversalClient::Connecting: @@ -367,6 +378,45 @@ void NetPlayDiag::OnThread(wxCommandEvent& event) break; } } + else if (sel != wxNOT_FOUND) // wxNOT_FOUND shouldn't generally happen + { + m_host_label->SetForegroundColour(*wxBLACK); + m_host_label->SetLabel(netplay_server->GetInterfaceHost(DeLabel(m_host_type_choice->GetString(sel)))); + m_host_copy_btn->SetLabel(_("Copy")); + m_host_copy_btn->Enable(); + m_host_copy_btn_is_retry = false; + } + + auto set = netplay_server->GetInterfaceSet(); + for (auto it = set.begin(); it != set.end(); ++it) + { + wxString kind = EnLabel(*it); + if (m_host_type_choice->FindString(kind) == wxNOT_FOUND) + m_host_type_choice->Append(kind); + } + for (unsigned i = 1, count = m_host_type_choice->GetCount(); i != count; i++) + { + if (set.find(DeLabel(m_host_type_choice->GetString(i))) == set.end()) + { + m_host_type_choice->Delete(i); + i--; + count--; + } + } +} + +void NetPlayDiag::OnChoice(wxCommandEvent& event) +{ + UpdateHostLabel(); +} + +// update gui +void NetPlayDiag::OnThread(wxCommandEvent& event) +{ + if (m_is_hosting) + { + UpdateHostLabel(); + } // player list m_playerids.clear(); diff --git a/Source/Core/DolphinWX/Src/NetWindow.h b/Source/Core/DolphinWX/Src/NetWindow.h index aa585af27300..738b778d39d9 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.h +++ b/Source/Core/DolphinWX/Src/NetWindow.h @@ -65,6 +65,8 @@ class NetPlayDiag : public wxFrame, public NetPlayUI void OnChat(wxCommandEvent& event); void OnQuit(wxCommandEvent& event); + void UpdateHostLabel(); + void OnChoice(wxCommandEvent& event); void OnThread(wxCommandEvent& event); void OnAdjustBuffer(wxCommandEvent& event); void OnConfigPads(wxCommandEvent& event); @@ -78,6 +80,7 @@ class NetPlayDiag : public wxFrame, public NetPlayUI wxTextCtrl* m_chat_msg_text; wxCheckBox* m_memcard_write; wxCheckBox* m_record_chkbox; + wxChoice* m_host_type_choice; wxStaticText* m_host_label; wxButton* m_host_copy_btn; bool m_host_copy_btn_is_retry; From d49c2e64606046a5b6f8e743519951de9c61a02d Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 15 Oct 2013 23:23:07 -0400 Subject: [PATCH 031/202] Fix initialization of ping time. --- Source/Core/Core/Src/NetPlayClient.cpp | 9 ++++++++- Source/Core/Core/Src/NetPlayServer.cpp | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index ed5f0de52d78..45f15eae148d 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -147,6 +147,7 @@ void NetPlayClient::OnData(Packet&& packet) player.name = m_local_name; player.pid = m_pid; player.revision = netplay_dolphin_ver; + player.ping = -1u; m_players[m_pid] = player; //PanicAlertT("Connection successful: assigned player id: %d", m_pid); @@ -172,6 +173,7 @@ void NetPlayClient::OnData(Packet&& packet) packet.Do(player.pid); packet.Do(player.name); packet.Do(player.revision); + player.ping = -1u; if (packet.failure) return OnDisconnect(InvalidPacket); @@ -504,7 +506,12 @@ void NetPlayClient::GetPlayerList(std::string& list, std::vector& pid_list) else ss << '-'; } - ss << " | " << player->ping << "ms\n"; + ss << " | "; + if (player->ping != -1u) + ss << player->ping; + else + ss << "?"; + ss << "ms\n"; pid_list.push_back(player->pid); } diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 2aee1b8d4995..3368f6822553 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -191,6 +191,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) hello.Do(npver); hello.Do(player.revision); hello.Do(player.name); + player.ping = -1u; // dolphin netplay version if (hello.failure || npver != NETPLAY_VERSION) return CON_ERR_VERSION_MISMATCH; From 755db3f12110b54e3b15730fe439a33cc6832558 Mon Sep 17 00:00:00 2001 From: comex Date: Wed, 16 Oct 2013 00:03:19 -0400 Subject: [PATCH 032/202] Fix compilation issues. Among other things, detect the version of clang in cmake for the thread safety warnings. --- CMakeLists.txt | 22 ++++++++++++++++++---- Source/Core/Common/Src/Common.h | 7 +++---- Source/Core/Common/Src/StdMutex.h | 13 ++++++------- Source/Core/Common/Src/Thread.h | 1 + Source/Core/Common/Src/TraversalServer.cpp | 2 ++ Source/Core/Core/Src/NetPlayClient.cpp | 2 +- Source/Core/Core/Src/NetPlayServer.cpp | 2 +- 7 files changed, 32 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 442dae94873a..ab236e0e392d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -181,9 +181,8 @@ if(APPLE) set(ENV{PATH} /usr/bin:/bin:/usr/sbin:/sbin) # Some of our code contains Objective C constructs. - set(CLANG_FLAGS "-stdlib=libc++ -Werror=thread-safety -Werror=thread-safety-beta") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -x objective-c ${CLANG_FLAGS}") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -x objective-c++ ${CLANG_FLAGS}") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -x objective-c -stdlib=libc++") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -x objective-c++ -stdlib=libc++") # Avoid mistaking an object file for a source file on the link command line. set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -x none") @@ -246,11 +245,26 @@ if(APPLE) find_library(WEBKIT_LIBRARY WebKit) endif() + if(WIN32) add_definitions(-D_SECURE_SCL=0) add_definitions(-D_CRT_SECURE_NO_WARNINGS) add_definitions(-D_CRT_SECURE_NO_DEPRECATE) -endif(WIN32) +else() + execute_process(COMMAND ${CMAKE_CXX_COMPILER} --version + OUTPUT_VARIABLE CXX_COMPILER_VERSION) + string(REGEX MATCH "based on LLVM [0-9.]*" + CXX_COMPILER_LLVM_VERSION + ${CXX_COMPILER_VERSION}) + if(NOT ("${CXX_COMPILER_LLVM_VERSION}" STREQUAL "")) + string(SUBSTRING "${CXX_COMPILER_LLVM_VERSION}" + 14 -1 CXX_COMPILER_LLVM_VERSION) + message(version is ${CXX_COMPILER_LLVM_VERSION}) + if(NOT ("${CXX_COMPILER_LLVM_VERSION}" VERSION_LESS "3.3")) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=thread-safety -Werror=thread-safety-beta -DTHREAD_SAFETY_ANNOTATIONS") + endif() + endif() +endif() if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Release" CACHE STRING diff --git a/Source/Core/Common/Src/Common.h b/Source/Core/Common/Src/Common.h index 6553f680c183..d7e067ae1996 100644 --- a/Source/Core/Common/Src/Common.h +++ b/Source/Core/Common/Src/Common.h @@ -175,10 +175,6 @@ enum EMUSTATE_CHANGE #include "CommonFuncs.h" -#ifdef __clang__ -#define THREAD_SAFETY_ANNOTATIONS -#endif - #ifdef THREAD_SAFETY_ANNOTATIONS /* Optional annotations to denote thread roles. @@ -192,6 +188,9 @@ enum EMUSTATE_CHANGE int *ptr DEREF_ON(CPU); // writes from CPU, reads from GPU int baz ACCESS_ON(CPU2GPU); + + Currently only supported in clang >= LLVM 3.3, i.e. not the buildbot at the + moment. */ struct ThreadHat { diff --git a/Source/Core/Common/Src/StdMutex.h b/Source/Core/Common/Src/StdMutex.h index 61b45f78b5e4..7dd5e6eac915 100644 --- a/Source/Core/Common/Src/StdMutex.h +++ b/Source/Core/Common/Src/StdMutex.h @@ -1,4 +1,3 @@ - #ifndef MUTEX_H_ #define MUTEX_H_ @@ -90,7 +89,7 @@ class recursive_mutex #endif } - _TS_MACRO(__attribute__((exclusive_lock_function(*this)))) + _TS_MACRO(__attribute__((exclusive_lock_function))) void lock() { #ifdef _WIN32 @@ -100,7 +99,7 @@ class recursive_mutex #endif } - _TS_MACRO(__attribute__((unlock_function(*this)))) + _TS_MACRO(__attribute__((unlock_function))) void unlock() { #ifdef _WIN32 @@ -110,7 +109,7 @@ class recursive_mutex #endif } - _TS_MACRO(__attribute__((exclusive_trylock_function(true, *this)))) + _TS_MACRO(__attribute__((exclusive_trylock_function(true)))) bool try_lock() { #ifdef _WIN32 @@ -162,7 +161,7 @@ class mutex #endif } - _TS_MACRO(__attribute__((exclusive_lock_function(*this)))) + _TS_MACRO(__attribute__((exclusive_lock_function))) void lock() { #ifdef _WIN32 @@ -172,7 +171,7 @@ class mutex #endif } - _TS_MACRO(__attribute__((unlock_function(*this)))) + _TS_MACRO(__attribute__((unlock_function))) void unlock() { #ifdef _WIN32 @@ -182,7 +181,7 @@ class mutex #endif } - _TS_MACRO(__attribute__((exclusive_trylock_function(true, *this)))) + _TS_MACRO(__attribute__((exclusive_trylock_function(true)))) bool try_lock() { #ifdef _WIN32 diff --git a/Source/Core/Common/Src/Thread.h b/Source/Core/Common/Src/Thread.h index ebbf7207cabf..6cde03685722 100644 --- a/Source/Core/Common/Src/Thread.h +++ b/Source/Core/Common/Src/Thread.h @@ -5,6 +5,7 @@ #ifndef _THREAD_H_ #define _THREAD_H_ +#include "Common.h" #include "StdConditionVariable.h" #include "StdMutex.h" #include "StdThread.h" diff --git a/Source/Core/Common/Src/TraversalServer.cpp b/Source/Core/Common/Src/TraversalServer.cpp index 46f2329a1a84..f3bca723e4e8 100644 --- a/Source/Core/Common/Src/TraversalServer.cpp +++ b/Source/Core/Common/Src/TraversalServer.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 45f15eae148d..fc0456ecc866 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -80,7 +80,7 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam m_host = m_host_client->m_Host; std::string host = hostSpec.substr(0, pos); - int port = std::stoi(hostSpec.substr(pos + 1).c_str()); + int port = atoi(hostSpec.substr(pos + 1).c_str()); ENetAddress addr; if (enet_address_set_host(&addr, host.c_str())) return; diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 3368f6822553..c8b556d67036 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -8,7 +8,7 @@ #if defined(__APPLE__) #include #include -#elif !defined(__WIN32__) +#elif !defined(_WIN32) #include #include #include From 65f56036d6e8191117afd732557aebf77f269cdf Mon Sep 17 00:00:00 2001 From: comex Date: Wed, 16 Oct 2013 00:47:06 -0400 Subject: [PATCH 033/202] Improve update-enet.sh to support local patches. And update enet. --- Externals/enet/git-revision | 2 +- Externals/enet/update-enet.sh | 12 +++++++++++- Externals/enet/win32.h | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Externals/enet/git-revision b/Externals/enet/git-revision index 5ed4da933377..28a33b319e22 100644 --- a/Externals/enet/git-revision +++ b/Externals/enet/git-revision @@ -1 +1 @@ -2c5dd69b176f1a3089bc10affc0c65d7fcbbab07 +48571bb05fcdb420da2b0b38cdaf2488bd031d20 diff --git a/Externals/enet/update-enet.sh b/Externals/enet/update-enet.sh index 05aad8519fe9..b4231aa9c727 100755 --- a/Externals/enet/update-enet.sh +++ b/Externals/enet/update-enet.sh @@ -1,9 +1,19 @@ #!/bin/sh set -xe rm -rf _enet -git rm -rf --ignore-unmatch *.[ch] git clone https://github.com/lsalzman/enet.git _enet +if [ -a git-revision ]; then + cd _enet + git checkout $(cat ../git-revision) + cp ../*.h include/ + cp ../*.c . + git stash + git checkout MASTER + git stash pop + cd .. +fi cd _enet; git rev-parse HEAD > ../git-revision; cd .. +git rm -rf --ignore-unmatch *.[ch] mv _enet/*.c _enet/include/enet/*.h _enet/LICENSE . git add *.[ch] LICENSE git-revision rm -rf _enet diff --git a/Externals/enet/win32.h b/Externals/enet/win32.h index 40fb803237db..197dfc047099 100644 --- a/Externals/enet/win32.h +++ b/Externals/enet/win32.h @@ -11,6 +11,7 @@ #pragma warning (disable: 4267) // size_t to int conversion #pragma warning (disable: 4244) // 64bit to 32bit int #pragma warning (disable: 4018) // signed/unsigned mismatch +#pragma warning (disable: 4146) // unary minus operator applied to unsigned type #endif #endif From 28427fce92b990ac692cc0cd5091dd0a25c38c1d Mon Sep 17 00:00:00 2001 From: comex Date: Wed, 16 Oct 2013 20:12:35 -0400 Subject: [PATCH 034/202] Add an option to force a particular listen port. And clean up the TraversalClient lifecycle somewhat. --- Source/Core/Common/Src/TraversalClient.cpp | 45 ++++++++++++++-------- Source/Core/Common/Src/TraversalClient.h | 9 +++-- Source/Core/Core/Src/ConfigManager.cpp | 14 ++++--- Source/Core/Core/Src/CoreParameter.h | 5 ++- Source/Core/Core/Src/NetPlayClient.cpp | 12 +++--- Source/Core/Core/Src/NetPlayServer.cpp | 9 ++++- Source/Core/DolphinWX/Src/ConfigMain.cpp | 18 +++++++++ Source/Core/DolphinWX/Src/ConfigMain.h | 4 ++ Source/Core/DolphinWX/Src/NetWindow.cpp | 31 +++++++++------ 9 files changed, 101 insertions(+), 46 deletions(-) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index 6c2061ac6de9..ec6b194ef7fd 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -59,10 +59,10 @@ static void GetRandomishBytes(u8* buf, size_t size) buf[i] = rand() & 0xff; } -ENetHostClient::ENetHostClient(size_t peerCount, bool isTraversalClient) +ENetHostClient::ENetHostClient(size_t peerCount, u16 port, bool isTraversalClient) { m_isTraversalClient = isTraversalClient; - ENetAddress addr = { ENET_HOST_ANY, ENET_PORT_ANY }; + ENetAddress addr = { ENET_HOST_ANY, port }; m_Host = enet_host_create( &addr, // address peerCount, // peerCount @@ -82,9 +82,9 @@ ENetHostClient::ENetHostClient(size_t peerCount, bool isTraversalClient) ENetHostClient::~ENetHostClient() { - Reset(); if (m_Host) { + Reset(); RunOnThread([=]() { ASSUME_ON(NET); m_ShouldEndThread = true; @@ -106,6 +106,12 @@ void ENetHostClient::Reset() // bleh, sync up with the thread m_ResetEvent.Reset(); RunOnThread([=]() { + for (size_t i = 0; i < m_Host->peerCount; i++) + { + ENetPeer* peer = &m_Host->peers[i]; + if (peer->state != ENET_PEER_STATE_DISCONNECTED) + enet_peer_disconnect_later(peer, 0); + } m_ResetEvent.Set(); }); m_ResetEvent.Wait(); @@ -146,15 +152,16 @@ void ENetHostClient::ThreadFunc() } } -TraversalClient::TraversalClient(const std::string& server) -: ENetHostClient(MAX_CLIENTS + 16, true), // leave some spaces free for server full notification +TraversalClient::TraversalClient(const std::string& server, u16 port) +: ENetHostClient(MAX_CLIENTS + 16, port, true), // leave some spaces free for server full notification m_Server(server) { + m_State = InitFailure; + if (!m_Host) return; Reset(); - m_State = InitFailure; m_Host->intercept = TraversalClient::InterceptCallback; m_Host->compressor.destroy = (decltype(m_Host->compressor.destroy)) this; @@ -398,25 +405,33 @@ void TraversalClient::Reset() { ENetHostClient::Reset(); - for (size_t i = 0; i < m_Host->peerCount; i++) - { - ENetPeer* peer = &m_Host->peers[i]; - if (peer->state != ENET_PEER_STATE_DISCONNECTED) - enet_peer_disconnect_later(peer, 0); - } m_PendingConnect = false; } std::unique_ptr g_TraversalClient; +static std::string g_OldServer; +static u16 g_OldPort; -void EnsureTraversalClient(const std::string& server) +void EnsureTraversalClient(const std::string& server, u16 port) { - if (!g_TraversalClient) + if (!g_TraversalClient || server != g_OldServer || port != g_OldPort) { - g_TraversalClient.reset(new TraversalClient(server)); + g_OldServer = server; + g_OldPort = port; + g_TraversalClient.reset(new TraversalClient(g_OldServer, g_OldPort)); if (g_TraversalClient->m_State == TraversalClient::InitFailure) { g_TraversalClient.reset(); } } } + +void ReleaseTraversalClient() +{ + if (!g_TraversalClient) + return; + if (g_OldPort != 0) + g_TraversalClient.reset(); + else + g_TraversalClient->Reset(); +} diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index a73c2ad3f656..a209adbbc459 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -50,7 +50,7 @@ class TraversalClientClient class ENetHostClient { public: - ENetHostClient(size_t peerCount, bool isTraversalClient = false); + ENetHostClient(size_t peerCount, u16 port, bool isTraversalClient = false); ~ENetHostClient(); void RunOnThread(std::function func); void CreateThread(); @@ -91,8 +91,8 @@ class TraversalClient : public ENetHostClient ConnectFailedError = 0x400, }; - TraversalClient(const std::string& server); - void Reset() ON(NET); + TraversalClient(const std::string& server, u16 port); + void Reset(); void ConnectToClient(const std::string& host) ON(NET); void ReconnectToServer(); u16 GetPort(); @@ -126,4 +126,5 @@ class TraversalClient : public ENetHostClient }; extern std::unique_ptr g_TraversalClient; -void EnsureTraversalClient(const std::string& server); +void EnsureTraversalClient(const std::string& server, u16 port); +void ReleaseTraversalClient(); diff --git a/Source/Core/Core/Src/ConfigManager.cpp b/Source/Core/Core/Src/ConfigManager.cpp index 4ba0e2430e03..36a226cb19f6 100644 --- a/Source/Core/Core/Src/ConfigManager.cpp +++ b/Source/Core/Core/Src/ConfigManager.cpp @@ -190,9 +190,10 @@ void SConfig::SaveSettings() ini.Set("Interface", "ShowLogConfigWindow", m_InterfaceLogConfigWindow); ini.Set("Interface", "ShowConsole", m_InterfaceConsole); ini.Set("Interface", "ThemeName40", m_LocalCoreStartupParameter.theme_name); - ini.Set("NetPlay", "LastHost", m_LocalCoreStartupParameter.strNetplayHost); - ini.Set("NetPlay", "Nickname", m_LocalCoreStartupParameter.strNetplayNickname); - ini.Set("NetPlay", "CentralServer", m_LocalCoreStartupParameter.strNetplayCentralServer); + ini.Set("NetPlay", "LastHost", m_LocalCoreStartupParameter.strNetPlayHost); + ini.Set("NetPlay", "Nickname", m_LocalCoreStartupParameter.strNetPlayNickname); + ini.Set("NetPlay", "CentralServer", m_LocalCoreStartupParameter.strNetPlayCentralServer); + ini.Set("NetPlay", "ListenPort", m_LocalCoreStartupParameter.iNetPlayListenPort); // Hotkeys for (int i = 0; i < NUM_HOTKEYS; i++) @@ -346,9 +347,10 @@ void SConfig::LoadSettings() ini.Get("Interface", "ShowLogConfigWindow", &m_InterfaceLogConfigWindow, false); ini.Get("Interface", "ShowConsole", &m_InterfaceConsole, false); ini.Get("Interface", "ThemeName40", &m_LocalCoreStartupParameter.theme_name, "Clean"); - ini.Get("NetPlay", "LastHost", &m_LocalCoreStartupParameter.strNetplayHost, "8.8.8.8:1234"); - ini.Get("NetPlay", "Nickname", &m_LocalCoreStartupParameter.strNetplayNickname, ""); - ini.Get("NetPlay", "CentralServer", &m_LocalCoreStartupParameter.strNetplayCentralServer, "dolphin-emu.org"); + ini.Get("NetPlay", "LastHost", &m_LocalCoreStartupParameter.strNetPlayHost, "8.8.8.8:1234"); + ini.Get("NetPlay", "Nickname", &m_LocalCoreStartupParameter.strNetPlayNickname, ""); + ini.Get("NetPlay", "CentralServer", &m_LocalCoreStartupParameter.strNetPlayCentralServer, "dolphin-emu.org"); + ini.Get("NetPlay", "ListenPort", &m_LocalCoreStartupParameter.iNetPlayListenPort, 0); // Hotkeys for (int i = 0; i < NUM_HOTKEYS; i++) diff --git a/Source/Core/Core/Src/CoreParameter.h b/Source/Core/Core/Src/CoreParameter.h index f9940506e955..22b48a4591ce 100644 --- a/Source/Core/Core/Src/CoreParameter.h +++ b/Source/Core/Core/Src/CoreParameter.h @@ -149,8 +149,9 @@ struct SCoreStartupParameter // Interface settings bool bConfirmStop, bHideCursor, bAutoHideCursor, bUsePanicHandlers, bOnScreenDisplayMessages; std::string theme_name; - std::string strNetplayHost, strNetplayNickname; - std::string strNetplayCentralServer; + std::string strNetPlayHost, strNetPlayNickname; + std::string strNetPlayCentralServer; + int iNetPlayListenPort; // Hotkeys int iHotkey[NUM_HOTKEYS]; diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index fc0456ecc866..019e22edd501 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -51,8 +51,8 @@ NetPlayClient::~NetPlayClient() if (m_is_running) StopGame(); - if (!m_direct_connection && g_TraversalClient) - g_TraversalClient->Reset(); + if (!m_direct_connection) + ReleaseTraversalClient(); } NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& name, std::function stateCallback) @@ -72,7 +72,7 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam { // Direct or local connection. Don't use TraversalClient. m_direct_connection = true; - m_host_client.reset(new ENetHostClient(1)); + m_host_client.reset(new ENetHostClient(/*peerCount=*/1, /*port=*/0)); if (!m_host_client->m_Host) return; m_host_client->m_Client = this; @@ -90,10 +90,8 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam else { m_direct_connection = false; - std::string server = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayCentralServer; - EnsureTraversalClient(server); - if (!g_TraversalClient || - g_TraversalClient->m_State == TraversalClient::InitFailure) + EnsureTraversalClient(SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayCentralServer, 0); + if (!g_TraversalClient) return; // If we were disconnected in the background, reconnect. if (g_TraversalClient->m_State == TraversalClient::Failure) diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index c8b556d67036..4af4a666e89d 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -4,6 +4,7 @@ #include "NetPlayServer.h" #include "NetPlayClient.h" // for NetPlayUI +#include "ConfigManager.h" #if defined(__APPLE__) #include @@ -24,7 +25,7 @@ NetPlayServer::~NetPlayServer() CFRelease(m_prefs); #endif // leave the host open for future use - g_TraversalClient->Reset(); + ReleaseTraversalClient(); } // called from ---GUI--- thread @@ -41,6 +42,12 @@ NetPlayServer::NetPlayServer() m_prefs = SCPreferencesCreate(NULL, CFSTR("NetPlayServer"), NULL); #endif + EnsureTraversalClient( + SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayCentralServer, + SConfig::GetInstance().m_LocalCoreStartupParameter.iNetPlayListenPort); + if (!g_TraversalClient) + return; + g_TraversalClient->m_Client = this; m_host = g_TraversalClient->m_Host; diff --git a/Source/Core/DolphinWX/Src/ConfigMain.cpp b/Source/Core/DolphinWX/Src/ConfigMain.cpp index a30153452222..931244b8e667 100644 --- a/Source/Core/DolphinWX/Src/ConfigMain.cpp +++ b/Source/Core/DolphinWX/Src/ConfigMain.cpp @@ -119,6 +119,8 @@ EVT_CHECKBOX(ID_IDLESKIP, CConfigMain::CoreSettingsChanged) EVT_CHECKBOX(ID_ENABLECHEATS, CConfigMain::CoreSettingsChanged) EVT_CHOICE(ID_FRAMELIMIT, CConfigMain::CoreSettingsChanged) EVT_CHECKBOX(ID_FRAMELIMIT_USEFPSFORLIMITING, CConfigMain::CoreSettingsChanged) +EVT_CHECKBOX(ID_NETPLAY_PORT_ENABLED, CConfigMain::CoreSettingsChanged) +EVT_TEXT(ID_NETPLAY_PORT, CConfigMain::CoreSettingsChanged) EVT_RADIOBOX(ID_CPUENGINE, CConfigMain::CoreSettingsChanged) EVT_CHECKBOX(ID_NTSCJ, CConfigMain::CoreSettingsChanged) @@ -331,6 +333,9 @@ void CConfigMain::InitializeGUIValues() if (CPUCores[a].CPUid == startup_params.iCPUCore) CPUEngine->SetSelection(a); _NTSCJ->SetValue(startup_params.bForceNTSCJ); + NetPlayPortEnabled->SetValue(startup_params.iNetPlayListenPort != 0); + NetPlayPort->SetValue(startup_params.iNetPlayListenPort); + NetPlayPort->Enable(NetPlayPortEnabled->IsChecked()); // Display - Interface @@ -553,6 +558,8 @@ void CConfigMain::CreateGUIControls() // Core Settings - Advanced CPUEngine = new wxRadioBox(GeneralPage, ID_CPUENGINE, _("CPU Emulator Engine"), wxDefaultPosition, wxDefaultSize, arrayStringFor_CPUEngine, 0, wxRA_SPECIFY_ROWS); _NTSCJ = new wxCheckBox(GeneralPage, ID_NTSCJ, _("Force Console as NTSC-J"), wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator); + NetPlayPortEnabled = new wxCheckBox(GeneralPage, ID_NETPLAY_PORT_ENABLED, _("Force Netplay Listen Port: ")); + NetPlayPort = new wxSpinCtrl(GeneralPage, ID_NETPLAY_PORT, "", wxDefaultPosition, wxSize(80, -1), wxSP_ARROW_KEYS, 1, 65535); // Populate the General settings wxBoxSizer* sFramelimit = new wxBoxSizer(wxHORIZONTAL); @@ -569,6 +576,11 @@ void CConfigMain::CreateGUIControls() sbAdvanced->Add(CPUEngine, 0, wxALL, 5); sbAdvanced->Add(_NTSCJ, 0, wxALL, 5); + wxBoxSizer* sNetPlayPort = new wxBoxSizer(wxHORIZONTAL); + sNetPlayPort->Add(NetPlayPortEnabled, 0, wxCENTER); + sNetPlayPort->Add(NetPlayPort, 0, wxCENTER); + sbAdvanced->Add(sNetPlayPort, 0, wxALL, 5); + wxBoxSizer* const sGeneralPage = new wxBoxSizer(wxVERTICAL); sGeneralPage->Add(sbBasic, 0, wxEXPAND | wxALL, 5); sGeneralPage->Add(sbAdvanced, 0, wxEXPAND | wxALL, 5); @@ -903,6 +915,12 @@ void CConfigMain::CoreSettingsChanged(wxCommandEvent& event) case ID_NTSCJ: SConfig::GetInstance().m_LocalCoreStartupParameter.bForceNTSCJ = _NTSCJ->IsChecked(); break; + + case ID_NETPLAY_PORT_ENABLED: + case ID_NETPLAY_PORT: + SConfig::GetInstance().m_LocalCoreStartupParameter.iNetPlayListenPort = NetPlayPortEnabled->IsChecked() ? NetPlayPort->GetValue() : 0; + NetPlayPort->Enable(NetPlayPortEnabled->IsChecked()); + break; } } diff --git a/Source/Core/DolphinWX/Src/ConfigMain.h b/Source/Core/DolphinWX/Src/ConfigMain.h index 094fb6df857f..0cc60dd602fe 100644 --- a/Source/Core/DolphinWX/Src/ConfigMain.h +++ b/Source/Core/DolphinWX/Src/ConfigMain.h @@ -59,6 +59,8 @@ class CConfigMain : public wxDialog ID_DSPTHREAD, ID_NTSCJ, + ID_NETPLAY_PORT_ENABLED, + ID_NETPLAY_PORT, // Audio Settings ID_DSPENGINE, @@ -130,6 +132,8 @@ class CConfigMain : public wxDialog wxRadioBox* CPUEngine; wxCheckBox* DSPThread; wxCheckBox* _NTSCJ; + wxCheckBox* NetPlayPortEnabled; + wxSpinCtrl* NetPlayPort; wxBoxSizer* sDisplayPage; // Display settings diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 6cab8fbe21b1..263527b70421 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -102,7 +102,7 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const // chat wxBoxSizer* const nickname_szr = new wxBoxSizer(wxHORIZONTAL); nickname_szr->Add(new wxStaticText(panel, wxID_ANY, _("Nickname: ")), 0, wxCENTER); - m_name_text = new wxTextCtrl(panel, wxID_ANY, StrToWxStr(SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayNickname)); + m_name_text = new wxTextCtrl(panel, wxID_ANY, StrToWxStr(SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayNickname)); m_name_text->Bind(wxEVT_KILL_FOCUS, &NetPlayDiag::OnDefocusName, this); nickname_szr->Add(m_name_text, 1, wxCENTER); @@ -491,7 +491,7 @@ void NetPlayDiag::OnConfigPads(wxCommandEvent&) void NetPlayDiag::OnDefocusName(wxFocusEvent&) { std::string name = StripSpaces(WxStrToStr(m_name_text->GetValue())); - std::string* cur = &SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayNickname; + std::string* cur = &SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayNickname; if (*cur != name) { *cur = name; @@ -524,10 +524,10 @@ bool NetPlayDiag::IsRecording() } ConnectDiag::ConnectDiag(wxWindow* parent) - : wxDialog(parent, wxID_ANY, _("Connect to Netplay"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + : wxDialog(parent, wxID_ANY, _("Connect to NetPlay"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { wxBoxSizer* sizerTop = new wxBoxSizer(wxVERTICAL); - std::string host = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayHost; + std::string host = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayHost; wxStaticText* hostLabel = new wxStaticText(this, wxID_ANY, _("Host or ID:")); m_HostCtrl = new wxTextCtrl(this, wxID_ANY, StrToWxStr(host)); // focus and select all @@ -556,7 +556,7 @@ bool ConnectDiag::Validate() return false; } std::string hostSpec = GetHost(); - std::string nickname = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayNickname; + std::string nickname = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayNickname; netplay_client.reset(new NetPlayClient(GetHost(), nickname, [=](NetPlayClient* npc) { auto state = npc->m_state; if (state == NetPlayClient::Connected || state == NetPlayClient::Failure) @@ -627,7 +627,7 @@ ConnectDiag::~ConnectDiag() if (GetReturnCode() != 0) netplay_client.reset(); } - SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayHost = StripSpaces(WxStrToStr(m_HostCtrl->GetValue())); + SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayHost = StripSpaces(WxStrToStr(m_HostCtrl->GetValue())); SConfig::GetInstance().SaveSettings(); } @@ -747,18 +747,27 @@ void NetPlay::StartHosting(std::string id, wxWindow* parent) return; } - std::string server = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayCentralServer; - EnsureTraversalClient(server); + netplay_server.reset(new NetPlayServer()); + if (!g_TraversalClient) { - wxMessageBox(_("Failed to init traversal client. This shouldn't happen..."), _("Error"), wxOK, parent); + netplay_server.reset(); + wxString error; + if (SConfig::GetInstance().m_LocalCoreStartupParameter.iNetPlayListenPort != 0) + { + error = _("Failed to init traversal client. Force Netplay Listen Port is enabled; someone is probably already listening on that port."); + } + else + { + error = _("Failed to init traversal client. This shouldn't happen..."); + } + wxMessageBox(error, _("Error"), wxOK, parent); return; } - netplay_server.reset(new NetPlayServer()); netplay_server->ChangeGame(id); netplay_server->AdjustPadBufferSize(INITIAL_PAD_BUFFER_SIZE); - std::string nickname = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetplayNickname; + std::string nickname = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayNickname; Common::Event ev; char buf[64]; sprintf(buf, "127.0.0.1:%d", g_TraversalClient->GetPort()); From ae7fbc427ef6e23c5ee3cb9482ec5dacd5005067 Mon Sep 17 00:00:00 2001 From: comex Date: Wed, 16 Oct 2013 20:27:31 -0400 Subject: [PATCH 035/202] Fix for gratuitously incompatible Android. --- Source/Core/Core/Src/NetPlayServer.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 4af4a666e89d..eda9b4861265 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -12,7 +12,9 @@ #elif !defined(_WIN32) #include #include +#ifndef ANDROID #include +#endif #include #endif @@ -139,6 +141,9 @@ std::vector> NetPlayServer::GetInterfaceList } CFRelease(ary); } +#elif defined(ANDROID) + // Android has no getifaddrs for some stupid reason. If this + // functionality ends up actually being used on Android, fix this. #else struct ifaddrs* ifp; char buf[512]; From 4f22ebcfb42bd17d95a361136eaa7a8a32d3d20d Mon Sep 17 00:00:00 2001 From: comex Date: Sat, 21 Sep 2013 15:23:50 -0400 Subject: [PATCH 036/202] Start refactoring IniFile: - Don't do a linear search in Delete. - Move less frequently used methods into Section rather than being inconsistent or duplicating. --- Source/Core/Common/Src/IniFile.cpp | 315 +++++------------- Source/Core/Common/Src/IniFile.h | 141 ++++---- Source/Core/Common/Src/StringUtil.cpp | 6 + Source/Core/Common/Src/StringUtil.h | 1 + Source/Core/Core/Src/ActionReplay.cpp | 29 +- Source/Core/Core/Src/BootManager.cpp | 2 +- Source/Core/Core/Src/GeckoCodeConfig.cpp | 13 +- .../Core/Src/HW/WiimoteReal/WiimoteReal.cpp | 4 +- Source/Core/Core/Src/PatchEngine.cpp | 61 ++-- .../Src/Debugger/BreakpointWindow.cpp | 18 +- Source/Core/DolphinWX/Src/ISOProperties.cpp | 22 +- .../Software/Src/SWVideoConfig.cpp | 6 +- Source/Core/VideoCommon/Src/VideoConfig.cpp | 36 +- 13 files changed, 273 insertions(+), 381 deletions(-) diff --git a/Source/Core/Common/Src/IniFile.cpp b/Source/Core/Common/Src/IniFile.cpp index fbce8254e3c8..e57b0203d76d 100644 --- a/Source/Core/Common/Src/IniFile.cpp +++ b/Source/Core/Common/Src/IniFile.cpp @@ -37,48 +37,53 @@ void ParseLine(const std::string& line, std::string* keyOut, std::string* valueO } -void IniFile::Section::Set(const char* key, const char* newValue) +std::string* IniFile::Section::GetLine(const char* key, std::string* valueOut) { - auto it = values.find(key); - if (it != values.end()) - it->second = newValue; - else + if (!parsed) { - values[key] = newValue; - keys_order.push_back(key); + parsed = true; + for (auto iter = lines.begin(); iter != lines.end(); ++iter) + { + const std::string& line = *iter; + std::string _key, value; + bool raw = line.size() >= 1 && (line[0] == '$' || line[0] == '+' || line[0] == '*'); + if (!raw) + { + ParseLine(line, &_key, &value); + if (_key.size()) + { + keys[_key] = std::make_pair(iter - lines.begin(), value); + } + } + } + } + auto it = keys.find(key); + if (it != keys.end()) + { + if (valueOut) + *valueOut = it->second.second; + return &lines[it->second.first]; } -} - -void IniFile::Section::Set(const char* key, const std::string& newValue, const std::string& defaultValue) -{ - if (newValue != defaultValue) - Set(key, newValue); - else - Delete(key); -} - -void IniFile::Section::Set(const char* key, const float newValue, const float defaultValue) -{ - if (newValue != defaultValue) - Set(key, newValue); - else - Delete(key); -} -void IniFile::Section::Set(const char* key, int newValue, int defaultValue) -{ - if (newValue != defaultValue) - Set(key, newValue); - else - Delete(key); + return 0; } -void IniFile::Section::Set(const char* key, bool newValue, bool defaultValue) +void IniFile::Section::Set(const char* key, const char* newValue) { - if (newValue != defaultValue) - Set(key, newValue); + std::string value; + std::string* line = GetLine(key, &value); + if (line) + { + // Change the value - keep the key and comment + *line = StripSpaces(key) + " = " + newValue; + keys[key].second = newValue; + } else - Delete(key); + { + // The key did not already exist in this section - let's add it. + keys[key] = std::make_pair(lines.size(), newValue); + lines.push_back(std::string(key) + " = " + newValue); + } } void IniFile::Section::Set(const char* key, const std::vector& newValues) @@ -95,24 +100,7 @@ void IniFile::Section::Set(const char* key, const std::vector& newV Set(key, temp.c_str()); } -bool IniFile::Section::Get(const char* key, std::string* value, const char* defaultValue) -{ - auto it = values.find(key); - if (it != values.end()) - { - *value = it->second; - return true; - } - else if (defaultValue) - { - *value = defaultValue; - return true; - } - else - return false; -} - -bool IniFile::Section::Get(const char* key, std::vector& out) +bool IniFile::Section::Get(const char* key, std::vector& out) const { std::string temp; bool retval = Get(key, &temp, 0); @@ -140,70 +128,56 @@ bool IniFile::Section::Get(const char* key, std::vector& out) return true; } -bool IniFile::Section::Get(const char* key, int* value, int defaultValue) +bool IniFile::Section::Exists(const char *key) const { - std::string temp; - bool retval = Get(key, &temp, 0); - if (retval && TryParse(temp.c_str(), value)) - return true; - *value = defaultValue; - return false; + return keys.find(key) != keys.end(); } -bool IniFile::Section::Get(const char* key, u32* value, u32 defaultValue) +bool IniFile::Section::Delete(const char *key) { - std::string temp; - bool retval = Get(key, &temp, 0); - if (retval && TryParse(temp, value)) - return true; - *value = defaultValue; + std::string* line = GetLine(key, 0); + if (line) + { + *line = "["; + keys.erase(key); + } return false; } -bool IniFile::Section::Get(const char* key, bool* value, bool defaultValue) +// Return a list of all lines in a section +std::vector IniFile::Section::GetLines(const bool remove_comments) const { - std::string temp; - bool retval = Get(key, &temp, 0); - if (retval && TryParse(temp.c_str(), value)) - return true; - *value = defaultValue; - return false; -} + std::vector stripped; -bool IniFile::Section::Get(const char* key, float* value, float defaultValue) -{ - std::string temp; - bool retval = Get(key, &temp, 0); - if (retval && TryParse(temp.c_str(), value)) - return true; - *value = defaultValue; - return false; -} + for (std::vector::const_iterator iter = lines.begin(); iter != lines.end(); ++iter) + { + std::string line = StripSpaces(*iter); -bool IniFile::Section::Get(const char* key, double* value, double defaultValue) -{ - std::string temp; - bool retval = Get(key, &temp, 0); - if (retval && TryParse(temp.c_str(), value)) - return true; - *value = defaultValue; - return false; -} + if (remove_comments) + { + int commentPos = (int)line.find('#'); + if (commentPos == 0) + { + continue; + } -bool IniFile::Section::Exists(const char *key) const -{ - return values.find(key) != values.end(); + if (commentPos != (int)std::string::npos) + { + line = StripSpaces(line.substr(0, commentPos)); + } + } + + stripped.push_back(line); + } + + return std::move(stripped); } -bool IniFile::Section::Delete(const char *key) +void IniFile::Section::SetLines(std::vector _lines) { - auto it = values.find(key); - if (it == values.end()) - return false; - - values.erase(it); - keys_order.erase(std::find(keys_order.begin(), keys_order.end(), key)); - return true; + lines = _lines; + keys.clear(); + parsed = false; } // IniFile @@ -259,12 +233,6 @@ bool IniFile::Exists(const char* sectionName, const char* key) const return section->Exists(key); } -void IniFile::SetLines(const char* sectionName, const std::vector &lines) -{ - Section* section = GetOrCreateSection(sectionName); - section->lines = lines; -} - bool IniFile::DeleteKey(const char* sectionName, const char* key) { Section* section = GetSection(sectionName); @@ -273,49 +241,6 @@ bool IniFile::DeleteKey(const char* sectionName, const char* key) return section->Delete(key); } -// Return a list of all keys in a section -bool IniFile::GetKeys(const char* sectionName, std::vector& keys) const -{ - const Section* section = GetSection(sectionName); - if (!section) - return false; - keys = section->keys_order; - return true; -} - -// Return a list of all lines in a section -bool IniFile::GetLines(const char* sectionName, std::vector& lines, const bool remove_comments) const -{ - const Section* section = GetSection(sectionName); - if (!section) - return false; - - lines.clear(); - for (std::vector::const_iterator iter = section->lines.begin(); iter != section->lines.end(); ++iter) - { - std::string line = StripSpaces(*iter); - - if (remove_comments) - { - int commentPos = (int)line.find('#'); - if (commentPos == 0) - { - continue; - } - - if (commentPos != (int)std::string::npos) - { - line = StripSpaces(line.substr(0, commentPos)); - } - } - - lines.push_back(line); - } - - return true; -} - - void IniFile::SortSections() { std::sort(sections.begin(), sections.end()); @@ -364,22 +289,9 @@ bool IniFile::Load(const char* filename, bool keep_current_data) current_section = GetOrCreateSection(sub.c_str()); } } - else + else if (current_section) { - if (current_section) - { - std::string key, value; - ParseLine(line, &key, &value); - - // Lines starting with '$', '*' or '+' are kept verbatim. - // Kind of a hack, but the support for raw lines inside an - // INI is a hack anyway. - if ((key == "" && value == "") - || (line.size() >= 1 && (line[0] == '$' || line[0] == '+' || line[0] == '*'))) - current_section->lines.push_back(line.c_str()); - else - current_section->Set(key, value.c_str()); - } + current_section->lines.push_back(line); } } } @@ -403,24 +315,14 @@ bool IniFile::Save(const char* filename) { const Section& section = *iter; - if (section.keys_order.size() != 0 || section.lines.size() != 0) + if (section.keys.size() != 0 || section.lines.size() != 0) out << "[" << section.name << "]" << std::endl; - if (section.keys_order.size() == 0) + for (auto liter = section.lines.begin(); liter != section.lines.end(); ++liter) { - for (auto liter = section.lines.begin(); liter != section.lines.end(); ++liter) - { - std::string s = *liter; + std::string s = *liter; + if (s != "[") // deleted out << s << std::endl; - } - } - else - { - for (auto kvit = section.keys_order.begin(); kvit != section.keys_order.end(); ++kvit) - { - auto pair = section.values.find(*kvit); - out << pair->first << " = " << pair->second << std::endl; - } } } @@ -430,59 +332,6 @@ bool IniFile::Save(const char* filename) } -bool IniFile::Get(const char* sectionName, const char* key, std::string* value, const char* defaultValue) -{ - Section* section = GetSection(sectionName); - if (!section) { - if (defaultValue) { - *value = defaultValue; - } - return false; - } - return section->Get(key, value, defaultValue); -} - -bool IniFile::Get(const char *sectionName, const char* key, std::vector& values) -{ - Section *section = GetSection(sectionName); - if (!section) - return false; - return section->Get(key, values); -} - -bool IniFile::Get(const char* sectionName, const char* key, int* value, int defaultValue) -{ - Section *section = GetSection(sectionName); - if (!section) { - *value = defaultValue; - return false; - } else { - return section->Get(key, value, defaultValue); - } -} - -bool IniFile::Get(const char* sectionName, const char* key, u32* value, u32 defaultValue) -{ - Section *section = GetSection(sectionName); - if (!section) { - *value = defaultValue; - return false; - } else { - return section->Get(key, value, defaultValue); - } -} - -bool IniFile::Get(const char* sectionName, const char* key, bool* value, bool defaultValue) -{ - Section *section = GetSection(sectionName); - if (!section) { - *value = defaultValue; - return false; - } else { - return section->Get(key, value, defaultValue); - } -} - // Unit test. TODO: Move to the real unit test framework. /* diff --git a/Source/Core/Common/Src/IniFile.h b/Source/Core/Common/Src/IniFile.h index 0cf4ab902bf8..e5564dde1f14 100644 --- a/Source/Core/Common/Src/IniFile.h +++ b/Source/Core/Common/Src/IniFile.h @@ -10,6 +10,7 @@ #include #include #include +#include #include "StringUtil.h" @@ -24,65 +25,103 @@ struct CaseInsensitiveStringCompare class IniFile { public: + enum Mode + { + MODE_READ, + MODE_WRITE, + MODE_PATCH // read, but don't apply defaults + }; class Section { friend class IniFile; public: - Section() {} - Section(const std::string& _name) : name(_name) {} + Section() : parsed(false) {} + Section(const std::string& _name) : name(_name), parsed(false) {} bool Exists(const char *key) const; bool Delete(const char *key); void Set(const char* key, const char* newValue); - void Set(const char* key, const std::string& newValue, const std::string& defaultValue); void Set(const std::string &key, const std::string &value) { Set(key.c_str(), value.c_str()); } - bool Get(const char* key, std::string* value, const char* defaultValue); - void Set(const char* key, u32 newValue) { Set(key, StringFromFormat("0x%08x", newValue).c_str()); } void Set(const char* key, float newValue) { Set(key, StringFromFormat("%f", newValue).c_str()); } - void Set(const char* key, const float newValue, const float defaultValue); void Set(const char* key, double newValue) { Set(key, StringFromFormat("%f", newValue).c_str()); } - - void Set(const char* key, int newValue, int defaultValue); void Set(const char* key, int newValue) { Set(key, StringFromInt(newValue).c_str()); } - - void Set(const char* key, bool newValue, bool defaultValue); void Set(const char* key, bool newValue) { Set(key, StringFromBool(newValue).c_str()); } void Set(const char* key, const std::vector& newValues); - bool Get(const char* key, int* value, int defaultValue = 0); - bool Get(const char* key, u32* value, u32 defaultValue = 0); - bool Get(const char* key, bool* value, bool defaultValue = false); - bool Get(const char* key, float* value, float defaultValue = false); - bool Get(const char* key, double* value, double defaultValue = false); - bool Get(const char* key, std::vector& values); + template + void Set(const char* key, T newValue, U defaultValue) + { + if (newValue == defaultValue) + Delete(key); + else + Set(key, newValue); + } + + template + bool Get(const char* key, T* value, U defaultValue) const + { + std::string temp; + if (const_cast(this)->GetLine(key, &temp) && + TryParse(temp, value)) + { + return true; + } + *value = defaultValue; + return false; + } + bool Get(const char* key, std::vector& values) const; + + std::vector GetLines(const bool remove_comments = true) const; + void SetLines(std::vector lines); + + template + void Do(Mode mode, const char* key, T* value, T defaultValue = 0) + { + switch (mode) + { + case MODE_WRITE: + Set(key, *value, defaultValue); + break; + case MODE_PATCH: + T temp; + if (Get(key, &temp)) + *value = temp; + break; + case MODE_READ: + Get(key, value, defaultValue); + break; + } + } bool operator < (const Section& other) const { return name < other.name; } - protected: std::string name; - - std::vector keys_order; - std::map values; - std::vector lines; + std::map, CaseInsensitiveStringCompare> keys; + private: + std::string* GetLine(const char* key, std::string* valueOut); + bool parsed; }; /** @@ -101,44 +140,33 @@ class IniFile // Returns true if key exists in section bool Exists(const char* sectionName, const char* key) const; - // TODO: Get rid of these, in favor of the Section ones. - void Set(const char* sectionName, const char* key, const char* newValue) { - GetOrCreateSection(sectionName)->Set(key, newValue); - } - void Set(const char* sectionName, const char* key, const std::string& newValue) { - GetOrCreateSection(sectionName)->Set(key, newValue.c_str()); - } - void Set(const char* sectionName, const char* key, int newValue) { - GetOrCreateSection(sectionName)->Set(key, newValue); - } - void Set(const char* sectionName, const char* key, u32 newValue) { - GetOrCreateSection(sectionName)->Set(key, newValue); - } - void Set(const char* sectionName, const char* key, bool newValue) { - GetOrCreateSection(sectionName)->Set(key, newValue); - } - void Set(const char* sectionName, const char* key, const std::vector& newValues) { - GetOrCreateSection(sectionName)->Set(key, newValues); + template + bool Get(const char* section, const char* key, T* value) const + { + return Get(section, key, value, T()); } - // TODO: Get rid of these, in favor of the Section ones. - bool Get(const char* sectionName, const char* key, std::string* value, const char* defaultValue = ""); - bool Get(const char* sectionName, const char* key, int* value, int defaultValue = 0); - bool Get(const char* sectionName, const char* key, u32* value, u32 defaultValue = 0); - bool Get(const char* sectionName, const char* key, bool* value, bool defaultValue = false); - bool Get(const char* sectionName, const char* key, std::vector& values); - - template bool GetIfExists(const char* sectionName, const char* key, T value) + template + bool Get(const char* section, const char* key, T* value, U defaultValue) const { - if (Exists(sectionName, key)) - return Get(sectionName, key, value); - return false; + const Section* sect = GetSection(section); + if (sect) + { + return sect->Get(key, value, defaultValue); + } + else + { + *value = defaultValue; + return false; + } } - bool GetKeys(const char* sectionName, std::vector& keys) const; - - void SetLines(const char* sectionName, const std::vector &lines); - bool GetLines(const char* sectionName, std::vector& lines, const bool remove_comments = true) const; + // temporary + template + void Set(const char* section, const char* key, T value) + { + return GetOrCreateSection(section)->Set(key, value); + } bool DeleteKey(const char* sectionName, const char* key); bool DeleteSection(const char* sectionName); @@ -146,13 +174,12 @@ class IniFile void SortSections(); Section* GetOrCreateSection(const char* section); + const Section* GetSection(const char* section) const; + Section* GetSection(const char* section); private: std::vector
sections; - const Section* GetSection(const char* section) const; - Section* GetSection(const char* section); - std::string* GetLine(const char* section, const char* key); void CreateSection(const char* section); }; diff --git a/Source/Core/Common/Src/StringUtil.cpp b/Source/Core/Common/Src/StringUtil.cpp index 189dfe98bc26..80e28b34b127 100644 --- a/Source/Core/Common/Src/StringUtil.cpp +++ b/Source/Core/Common/Src/StringUtil.cpp @@ -186,6 +186,12 @@ bool TryParse(const std::string &str, bool *const output) return true; } +bool TryParse(const std::string &str, std::string *output) +{ + *output = str; + return true; +} + std::string StringFromInt(int value) { char temp[16]; diff --git a/Source/Core/Common/Src/StringUtil.h b/Source/Core/Common/Src/StringUtil.h index 56c223641b4d..abc4bb4eb231 100644 --- a/Source/Core/Common/Src/StringUtil.h +++ b/Source/Core/Common/Src/StringUtil.h @@ -53,6 +53,7 @@ std::string StringFromBool(bool value); bool TryParse(const std::string &str, bool *output); bool TryParse(const std::string &str, u32 *output); +bool TryParse(const std::string &str, std::string *output); template static bool TryParse(const std::string &str, N *const output) diff --git a/Source/Core/Core/Src/ActionReplay.cpp b/Source/Core/Core/Src/ActionReplay.cpp index 771ef5700b17..3f63b4d0a1da 100644 --- a/Source/Core/Core/Src/ActionReplay.cpp +++ b/Source/Core/Core/Src/ActionReplay.cpp @@ -120,32 +120,33 @@ void LoadCodes(IniFile &globalIni, IniFile &localIni, bool forceLoad) arCodes.clear(); - std::vector enabledLines; std::set enabledNames; - localIni.GetLines("ActionReplay_Enabled", enabledLines); - for (auto iter = enabledLines.begin(); iter != enabledLines.end(); ++iter) + if (const IniFile::Section* sect = localIni.GetSection("ActionReplay_Enabled")) { - const std::string& line = *iter; - if (line.size() != 0 && line[0] == '$') + auto enabledLines = sect->GetLines(); + for (auto iter = enabledLines.begin(); iter != enabledLines.end(); ++iter) { - std::string name = line.substr(1, line.size() - 1); - enabledNames.insert(name); + const std::string& line = *iter; + if (line.size() != 0 && line[0] == '$') + { + std::string name = line.substr(1, line.size() - 1); + enabledNames.insert(name); + } } } IniFile* inis[] = {&globalIni, &localIni}; for (size_t i = 0; i < ArraySize(inis); ++i) { - std::vector lines; std::vector encryptedLines; ARCode currentCode; - - inis[i]->GetLines("ActionReplay", lines); - std::vector::const_iterator - it = lines.begin(), - lines_end = lines.end(); - for (; it != lines_end; ++it) + IniFile::Section* sect = inis[i]->GetSection("ActionReplay"); + if (!sect) + continue; + std::vector lines = sect->GetLines(); + + for (auto it = lines.begin(); it != lines.end(); ++it) { const std::string line = *it; diff --git a/Source/Core/Core/Src/BootManager.cpp b/Source/Core/Core/Src/BootManager.cpp index a2e7688ca9ca..5082b3a860eb 100644 --- a/Source/Core/Core/Src/BootManager.cpp +++ b/Source/Core/Core/Src/BootManager.cpp @@ -122,7 +122,7 @@ bool BootCore(const std::string& _rFilename) game_ini.Get("Core", "BlockMerging", &StartUp.bMergeBlocks, StartUp.bMergeBlocks); game_ini.Get("Core", "DSPHLE", &StartUp.bDSPHLE, StartUp.bDSPHLE); game_ini.Get("Core", "DSPThread", &StartUp.bDSPThread, StartUp.bDSPThread); - game_ini.Get("Core", "GFXBackend", &StartUp.m_strVideoBackend, StartUp.m_strVideoBackend.c_str()); + game_ini.Get("Core", "GFXBackend", &StartUp.m_strVideoBackend, StartUp.m_strVideoBackend); game_ini.Get("Core", "CPUCore", &StartUp.iCPUCore, StartUp.iCPUCore); game_ini.Get("Core", "HLE_BS2", &StartUp.bHLE_BS2, StartUp.bHLE_BS2); game_ini.Get("DSP", "Volume", &SConfig::GetInstance().m_Volume, SConfig::GetInstance().m_Volume); diff --git a/Source/Core/Core/Src/GeckoCodeConfig.cpp b/Source/Core/Core/Src/GeckoCodeConfig.cpp index 58b5b46968ca..12758a1935a4 100644 --- a/Source/Core/Core/Src/GeckoCodeConfig.cpp +++ b/Source/Core/Core/Src/GeckoCodeConfig.cpp @@ -19,7 +19,9 @@ void LoadCodes(const IniFile& globalIni, const IniFile& localIni, std::vector lines; - inis[i]->GetLines("Gecko", lines, false); + const IniFile::Section* sect = inis[i]->GetSection("Gecko"); + if (sect) + lines = sect->GetLines(false); GeckoCode gcode; @@ -73,7 +75,10 @@ void LoadCodes(const IniFile& globalIni, const IniFile& localIni, std::vectorGetLines("Gecko_Enabled", lines, false); + sect = inis[i]->GetSection("Gecko_Enabled"); + lines.clear(); + if (sect) + lines = sect->GetLines(false); for (auto lines_iter = lines.begin(); lines_iter!=lines.end(); ++lines_iter) { @@ -148,8 +153,8 @@ void SaveCodes(IniFile& inifile, const std::vector& gcodes) SaveGeckoCode(lines, enabledLines, *gcodes_iter); } - inifile.SetLines("Gecko", lines); - inifile.SetLines("Gecko_Enabled", enabledLines); + inifile.GetOrCreateSection("Gecko")->SetLines(lines); + inifile.GetOrCreateSection("Gecko_Enabled")->SetLines(enabledLines); } }; diff --git a/Source/Core/Core/Src/HW/WiimoteReal/WiimoteReal.cpp b/Source/Core/Core/Src/HW/WiimoteReal/WiimoteReal.cpp index 91a787c555b6..9adede46c7c6 100644 --- a/Source/Core/Core/Src/HW/WiimoteReal/WiimoteReal.cpp +++ b/Source/Core/Core/Src/HW/WiimoteReal/WiimoteReal.cpp @@ -563,12 +563,12 @@ void LoadSettings() secname += (char)('1' + i); IniFile::Section& sec = *inifile.GetOrCreateSection(secname.c_str()); - sec.Get("Source", &g_wiimote_sources[i], i ? WIIMOTE_SRC_NONE : WIIMOTE_SRC_EMU); + sec.Get("Source", &g_wiimote_sources[i], (unsigned int) (i ? WIIMOTE_SRC_NONE : WIIMOTE_SRC_EMU)); } std::string secname("BalanceBoard"); IniFile::Section& sec = *inifile.GetOrCreateSection(secname.c_str()); - sec.Get("Source", &g_wiimote_sources[WIIMOTE_BALANCE_BOARD], WIIMOTE_SRC_NONE); + sec.Get("Source", &g_wiimote_sources[WIIMOTE_BALANCE_BOARD], (unsigned int) WIIMOTE_SRC_NONE); } // config dialog calls this when some settings change diff --git a/Source/Core/Core/Src/PatchEngine.cpp b/Source/Core/Core/Src/PatchEngine.cpp index e266e0d09882..71eceda2f573 100644 --- a/Source/Core/Core/Src/PatchEngine.cpp +++ b/Source/Core/Core/Src/PatchEngine.cpp @@ -51,16 +51,18 @@ void LoadPatchSection(const char *section, std::vector &patches, { // Load the name of all enabled patches std::string enabledSectionName = std::string(section) + "_Enabled"; - std::vector enabledLines; std::set enabledNames; - localIni.GetLines(enabledSectionName.c_str(), enabledLines); - for (auto iter = enabledLines.begin(); iter != enabledLines.end(); ++iter) + if (const IniFile::Section* sect = localIni.GetSection(enabledSectionName.c_str())) { - const std::string& line = *iter; - if (line.size() != 0 && line[0] == '$') + std::vector enabledLines = sect->GetLines(); + for (auto iter = enabledLines.begin(); iter != enabledLines.end(); ++iter) { - std::string name = line.substr(1, line.size() - 1); - enabledNames.insert(name); + const std::string& line = *iter; + if (line.size() != 0 && line[0] == '$') + { + std::string name = line.substr(1, line.size() - 1); + enabledNames.insert(name); + } } } @@ -68,9 +70,14 @@ void LoadPatchSection(const char *section, std::vector &patches, for (size_t i = 0; i < ArraySize(inis); ++i) { - std::vector lines; Patch currentPatch; - inis[i]->GetLines(section, lines); + const IniFile::Section* sect = localIni.GetSection(section); + if (!sect) + { + continue; + } + + std::vector lines = sect->GetLines(); for (auto iter = lines.begin(); iter != lines.end(); ++iter) { @@ -123,13 +130,14 @@ void LoadPatchSection(const char *section, std::vector &patches, static void LoadDiscList(const char *section, std::vector &_discList, IniFile &ini) { - std::vector lines; - if (!ini.GetLines(section, lines)) + const IniFile::Section* sect = ini.GetSection(section); + if (!sect) return; + std::vector lines = sect->GetLines(); - for (std::vector::const_iterator iter = lines.begin(); iter != lines.end(); ++iter) + for (auto itr = lines.begin(); itr != lines.end(); ++itr) { - std::string line = *iter; + std::string line = *itr; if (line.size()) _discList.push_back(line); } @@ -138,22 +146,19 @@ static void LoadDiscList(const char *section, std::vector &_discLis static void LoadSpeedhacks(const char *section, std::map &hacks, IniFile &ini) { std::vector keys; - ini.GetKeys(section, keys); - for (std::vector::const_iterator iter = keys.begin(); iter != keys.end(); ++iter) + IniFile::Section* sect = ini.GetOrCreateSection(section); + for (auto itr = sect->keys.begin(); itr != sect->keys.end(); ++itr) { - std::string key = *iter; - std::string value; - ini.Get(section, key.c_str(), &value, "BOGUS"); - if (value != "BOGUS") - { - u32 address; - u32 cycles; - bool success = true; - success &= TryParse(key, &address); - success &= TryParse(value, &cycles); - if (success) { - speedHacks[address] = (int)cycles; - } + std::string key = itr->first; + std::string value = itr->second.second; + + u32 address; + u32 cycles; + bool success = true; + success &= TryParse(key, &address); + success &= TryParse(value, &cycles); + if (success) { + speedHacks[address] = (int)cycles; } } } diff --git a/Source/Core/DolphinWX/Src/Debugger/BreakpointWindow.cpp b/Source/Core/DolphinWX/Src/Debugger/BreakpointWindow.cpp index a39196697c5f..9702589ca392 100644 --- a/Source/Core/DolphinWX/Src/Debugger/BreakpointWindow.cpp +++ b/Source/Core/DolphinWX/Src/Debugger/BreakpointWindow.cpp @@ -166,8 +166,8 @@ void CBreakPointWindow::SaveAll() IniFile ini; if (ini.Load(File::GetUserPath(F_DEBUGGERCONFIG_IDX))) { - ini.SetLines("BreakPoints", PowerPC::breakpoints.GetStrings()); - ini.SetLines("MemoryChecks", PowerPC::memchecks.GetStrings()); + ini.GetOrCreateSection("Breakpoints")->SetLines(PowerPC::breakpoints.GetStrings()); + ini.GetOrCreateSection("MemoryChecks")->SetLines(PowerPC::memchecks.GetStrings()); ini.Save(File::GetUserPath(F_DEBUGGERCONFIG_IDX)); } } @@ -175,16 +175,14 @@ void CBreakPointWindow::SaveAll() void CBreakPointWindow::LoadAll(wxCommandEvent& WXUNUSED(event)) { IniFile ini; - BreakPoints::TBreakPointsStr newbps; - MemChecks::TMemChecksStr newmcs; - + if (!ini.Load(File::GetUserPath(F_DEBUGGERCONFIG_IDX))) return; - - if (ini.GetLines("BreakPoints", newbps, false)) - PowerPC::breakpoints.AddFromStrings(newbps); - if (ini.GetLines("MemoryChecks", newmcs, false)) - PowerPC::memchecks.AddFromStrings(newmcs); + + if (IniFile::Section* newbps = ini.GetSection("BreakPoints")) + PowerPC::breakpoints.AddFromStrings(newbps->GetLines(false)); + if (IniFile::Section* newmcs = ini.GetSection("MemoryChecks")) + PowerPC::memchecks.AddFromStrings(newmcs->GetLines(false)); NotifyUpdate(); } diff --git a/Source/Core/DolphinWX/Src/ISOProperties.cpp b/Source/Core/DolphinWX/Src/ISOProperties.cpp index e5943ef8cfb1..963f8a792c10 100644 --- a/Source/Core/DolphinWX/Src/ISOProperties.cpp +++ b/Source/Core/DolphinWX/Src/ISOProperties.cpp @@ -1019,32 +1019,32 @@ void CISOProperties::LoadGameConfig() PHackEnable->SetValue(iTemp); GameIniDefault.Get("Video", "PH_SZNear", &PHack_Data.PHackSZNear); - if (GameIniLocal.GetIfExists("Video", "PH_SZNear", &iTemp)) + if (GameIniLocal.Get("Video", "PH_SZNear", &iTemp)) PHack_Data.PHackSZNear = iTemp; GameIniDefault.Get("Video", "PH_SZFar", &PHack_Data.PHackSZFar); - if (GameIniLocal.GetIfExists("Video", "PH_SZFar", &iTemp)) + if (GameIniLocal.Get("Video", "PH_SZFar", &iTemp)) PHack_Data.PHackSZFar = iTemp; GameIniDefault.Get("Video", "PH_ExtraParam", &PHack_Data.PHackExP); - if (GameIniLocal.GetIfExists("Video", "PH_ExtraParam", &iTemp)) + if (GameIniLocal.Get("Video", "PH_ExtraParam", &iTemp)) PHack_Data.PHackExP = iTemp; std::string sTemp; GameIniDefault.Get("Video", "PH_ZNear", &PHack_Data.PHZNear); - if (GameIniLocal.GetIfExists("Video", "PH_ZNear", &sTemp)) + if (GameIniLocal.Get("Video", "PH_ZNear", &sTemp)) PHack_Data.PHZNear = sTemp; GameIniDefault.Get("Video", "PH_ZFar", &PHack_Data.PHZFar); - if (GameIniLocal.GetIfExists("Video", "PH_ZFar", &sTemp)) + if (GameIniLocal.Get("Video", "PH_ZFar", &sTemp)) PHack_Data.PHZFar = sTemp; GameIniDefault.Get("EmuState", "EmulationStateId", &iTemp, 0/*Not Set*/); EmuState->SetSelection(iTemp); - if (GameIniLocal.GetIfExists("EmuState", "EmulationStateId", &iTemp)) + if (GameIniLocal.Get("EmuState", "EmulationStateId", &iTemp)) EmuState->SetSelection(iTemp); GameIniDefault.Get("EmuState", "EmulationIssues", &sTemp); if (!sTemp.empty()) EmuIssues->SetValue(StrToWxStr(sTemp)); - if (GameIniLocal.GetIfExists("EmuState", "EmulationIssues", &sTemp)) + if (GameIniLocal.Get("EmuState", "EmulationIssues", &sTemp)) EmuIssues->SetValue(StrToWxStr(sTemp)); EmuIssues->Enable(EmuState->GetSelection() != 0); @@ -1256,8 +1256,8 @@ void CISOProperties::PatchList_Save() } ++index; } - GameIniLocal.SetLines("OnFrame_Enabled", enabledLines); - GameIniLocal.SetLines("OnFrame", lines); + GameIniLocal.GetOrCreateSection("OnFrame_Enabled")->SetLines(enabledLines); + GameIniLocal.GetOrCreateSection("OnFrame")->SetLines(lines); } void CISOProperties::PHackButtonClicked(wxCommandEvent& event) @@ -1348,8 +1348,8 @@ void CISOProperties::ActionReplayList_Save() } ++index; } - GameIniLocal.SetLines("ActionReplay_Enabled", enabledLines); - GameIniLocal.SetLines("ActionReplay", lines); + GameIniLocal.GetOrCreateSection("ActionReplay_Enabled")->SetLines(enabledLines); + GameIniLocal.GetOrCreateSection("ActionReplay")->SetLines(lines); } void CISOProperties::ActionReplayButtonClicked(wxCommandEvent& event) diff --git a/Source/Core/VideoBackends/Software/Src/SWVideoConfig.cpp b/Source/Core/VideoBackends/Software/Src/SWVideoConfig.cpp index a6ebb97eb99a..ab8c327a2745 100644 --- a/Source/Core/VideoBackends/Software/Src/SWVideoConfig.cpp +++ b/Source/Core/VideoBackends/Software/Src/SWVideoConfig.cpp @@ -37,7 +37,7 @@ void SWVideoConfig::Load(const char* ini_file) IniFile iniFile; iniFile.Load(ini_file); - iniFile.Get("Hardware", "Fullscreen", &bFullscreen, 0); // Hardware + iniFile.Get("Hardware", "Fullscreen", &bFullscreen, false); // Hardware iniFile.Get("Hardware", "RenderToMainframe", &renderToMainframe, false); iniFile.Get("Rendering", "HwRasterizer", &bHwRasterizer, false); @@ -52,8 +52,8 @@ void SWVideoConfig::Load(const char* ini_file) iniFile.Get("Utility", "DumpTevStages", &bDumpTevStages, false); iniFile.Get("Utility", "DumpTevTexFetches", &bDumpTevTextureFetches, false); - iniFile.Get("Misc", "DrawStart", &drawStart, 0); - iniFile.Get("Misc", "DrawEnd", &drawEnd, 100000); + iniFile.Get("Misc", "DrawStart", &drawStart, 0u); + iniFile.Get("Misc", "DrawEnd", &drawEnd, 100000u); } void SWVideoConfig::Save(const char* ini_file) diff --git a/Source/Core/VideoCommon/Src/VideoConfig.cpp b/Source/Core/VideoCommon/Src/VideoConfig.cpp index 93840d75868f..e8b3362a7134 100644 --- a/Source/Core/VideoCommon/Src/VideoConfig.cpp +++ b/Source/Core/VideoCommon/Src/VideoConfig.cpp @@ -44,12 +44,12 @@ void VideoConfig::Load(const char *ini_file) IniFile iniFile; iniFile.Load(ini_file); - iniFile.Get("Hardware", "VSync", &bVSync, 0); // Hardware + iniFile.Get("Hardware", "VSync", &bVSync, false); // Hardware iniFile.Get("Settings", "wideScreenHack", &bWidescreenHack, false); iniFile.Get("Settings", "AspectRatio", &iAspectRatio, (int)ASPECT_AUTO); iniFile.Get("Settings", "Crop", &bCrop, false); - iniFile.Get("Settings", "UseXFB", &bUseXFB, 0); - iniFile.Get("Settings", "UseRealXFB", &bUseRealXFB, 0); + iniFile.Get("Settings", "UseXFB", &bUseXFB, false); + iniFile.Get("Settings", "UseRealXFB", &bUseRealXFB, false); iniFile.Get("Settings", "SafeTextureCacheColorSamples", &iSafeTextureCache_ColorSamples,128); iniFile.Get("Settings", "ShowFPS", &bShowFPS, false); // Settings iniFile.Get("Settings", "LogFPSToFile", &bLogFPSToFile, false); @@ -58,17 +58,17 @@ void VideoConfig::Load(const char *ini_file) iniFile.Get("Settings", "OverlayProjStats", &bOverlayProjStats, false); iniFile.Get("Settings", "ShowEFBCopyRegions", &bShowEFBCopyRegions, false); iniFile.Get("Settings", "DLOptimize", &iCompileDLsLevel, 0); - iniFile.Get("Settings", "DumpTextures", &bDumpTextures, 0); - iniFile.Get("Settings", "HiresTextures", &bHiresTextures, 0); - iniFile.Get("Settings", "DumpEFBTarget", &bDumpEFBTarget, 0); - iniFile.Get("Settings", "DumpFrames", &bDumpFrames, 0); - iniFile.Get("Settings", "FreeLook", &bFreeLook, 0); - iniFile.Get("Settings", "UseFFV1", &bUseFFV1, 0); + iniFile.Get("Settings", "DumpTextures", &bDumpTextures, false); + iniFile.Get("Settings", "HiresTextures", &bHiresTextures, false); + iniFile.Get("Settings", "DumpEFBTarget", &bDumpEFBTarget, false); + iniFile.Get("Settings", "DumpFrames", &bDumpFrames, false); + iniFile.Get("Settings", "FreeLook", &bFreeLook, false); + iniFile.Get("Settings", "UseFFV1", &bUseFFV1, false); iniFile.Get("Settings", "AnaglyphStereo", &bAnaglyphStereo, false); iniFile.Get("Settings", "AnaglyphStereoSeparation", &iAnaglyphStereoSeparation, 200); iniFile.Get("Settings", "AnaglyphFocalAngle", &iAnaglyphFocalAngle, 0); - iniFile.Get("Settings", "EnablePixelLighting", &bEnablePixelLighting, 0); - iniFile.Get("Settings", "HackedBufferUpload", &bHackedBufferUpload, 0); + iniFile.Get("Settings", "EnablePixelLighting", &bEnablePixelLighting, false); + iniFile.Get("Settings", "HackedBufferUpload", &bHackedBufferUpload, false); iniFile.Get("Settings", "FastDepthCalc", &bFastDepthCalc, true); iniFile.Get("Settings", "MSAA", &iMultisampleMode, 0); @@ -76,19 +76,19 @@ void VideoConfig::Load(const char *ini_file) iniFile.Get("Settings", "DstAlphaPass", &bDstAlphaPass, false); - iniFile.Get("Settings", "TexFmtOverlayEnable", &bTexFmtOverlayEnable, 0); - iniFile.Get("Settings", "TexFmtOverlayCenter", &bTexFmtOverlayCenter, 0); - iniFile.Get("Settings", "WireFrame", &bWireFrame, 0); - iniFile.Get("Settings", "DisableFog", &bDisableFog, 0); + iniFile.Get("Settings", "TexFmtOverlayEnable", &bTexFmtOverlayEnable, false); + iniFile.Get("Settings", "TexFmtOverlayCenter", &bTexFmtOverlayCenter, false); + iniFile.Get("Settings", "WireFrame", &bWireFrame, false); + iniFile.Get("Settings", "DisableFog", &bDisableFog, false); iniFile.Get("Settings", "EnableOpenCL", &bEnableOpenCL, false); iniFile.Get("Settings", "OMPDecoder", &bOMPDecoder, false); iniFile.Get("Settings", "EnableShaderDebugging", &bEnableShaderDebugging, false); - iniFile.Get("Enhancements", "ForceFiltering", &bForceFiltering, 0); + iniFile.Get("Enhancements", "ForceFiltering", &bForceFiltering, false); iniFile.Get("Enhancements", "MaxAnisotropy", &iMaxAnisotropy, 0); // NOTE - this is x in (1 << x) - iniFile.Get("Enhancements", "PostProcessingShader", &sPostProcessingShader, ""); + iniFile.Get("Enhancements", "PostProcessingShader", &sPostProcessingShader, std::string()); iniFile.Get("Enhancements", "Enable3dVision", &b3DVision, false); iniFile.Get("Hacks", "EFBAccessEnable", &bEFBAccessEnable, true); @@ -128,7 +128,7 @@ void VideoConfig::GameIniLoad() // XXX: This will add an OSD message for each projection hack value... meh #define CHECK_SETTING(section, key, var) do { \ decltype(var) temp = var; \ - if (iniFile.GetIfExists(section, key, &var) && var != temp) { \ + if (iniFile.Get(section, key, &var) && var != temp) { \ char buf[256]; \ snprintf(buf, sizeof(buf), "Note: Option \"%s\" is overridden by game ini.", key); \ OSD::AddMessage(buf, 7500); \ From 02f19a89c3dd6a531bd6b00a85dd18ffb112ee18 Mon Sep 17 00:00:00 2001 From: comex Date: Wed, 16 Oct 2013 23:14:48 -0400 Subject: [PATCH 037/202] Fix out-of-tree build on OS X. --- Source/Core/DolphinWX/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/DolphinWX/CMakeLists.txt b/Source/Core/DolphinWX/CMakeLists.txt index 1aa7ea0aefc2..c8243ddeee82 100644 --- a/Source/Core/DolphinWX/CMakeLists.txt +++ b/Source/Core/DolphinWX/CMakeLists.txt @@ -248,7 +248,7 @@ else() ) else() add_custom_command(OUTPUT ${BUNDLE_PATH}/Contents/Resources/Sys - COMMAND ln -nfs ../../../../Data/Sys ${BUNDLE_PATH}/Contents/Resources/Sys + COMMAND ln -nfs ${CMAKE_SOURCE_DIR}/Data/Sys ${BUNDLE_PATH}/Contents/Resources/Sys VERBATIM ) add_custom_target(CopyDataIntoBundle ALL From 03589cdb84525f4269307d1465991e9c6f3222d5 Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 17 Oct 2013 00:35:01 -0400 Subject: [PATCH 038/202] Put a lock around writes to the FifoQueue. --- Source/Core/Common/Src/TraversalClient.cpp | 5 ++++- Source/Core/Common/Src/TraversalClient.h | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index ec6b194ef7fd..3f51b3cbc3b1 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -96,7 +96,10 @@ ENetHostClient::~ENetHostClient() void ENetHostClient::RunOnThread(std::function func) { - m_RunQueue.Push(func); + { + std::lock_guard lk(m_RunQueueWriteLock); + m_RunQueue.Push(func); + } ENetUtil::Wakeup(m_Host); } diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index a209adbbc459..58e052d791f0 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -64,8 +64,8 @@ class ENetHostClient void ThreadFunc() /* ON(NET) */; Common::FifoQueue, false> m_RunQueue; + std::mutex m_RunQueueWriteLock; std::thread m_Thread; - // *sigh* Common::Event m_ResetEvent; bool m_ShouldEndThread ACCESS_ON(NET); bool m_isTraversalClient; From 9f4bd9c5fc112acd240dd59e7ceb6c0a1a642241 Mon Sep 17 00:00:00 2001 From: comex Date: Sun, 20 Oct 2013 00:51:23 -0400 Subject: [PATCH 039/202] When adding new GC controller-like SI devices, why not copy hundreds of lines of code verbatim from GCController rather than subclassing? --- Source/Core/Core/Src/HW/SI_DeviceDanceMat.cpp | 235 +-------------- Source/Core/Core/Src/HW/SI_DeviceDanceMat.h | 92 +----- .../Core/Src/HW/SI_DeviceGCController.cpp | 40 ++- .../Core/Core/Src/HW/SI_DeviceGCController.h | 5 +- .../Core/Src/HW/SI_DeviceGCSteeringWheel.cpp | 281 +----------------- .../Core/Src/HW/SI_DeviceGCSteeringWheel.h | 70 +---- Source/Core/Core/Src/NetPlayClient.cpp | 22 -- 7 files changed, 66 insertions(+), 679 deletions(-) diff --git a/Source/Core/Core/Src/HW/SI_DeviceDanceMat.cpp b/Source/Core/Core/Src/HW/SI_DeviceDanceMat.cpp index d21371fe3698..f6c817c0a00c 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceDanceMat.cpp +++ b/Source/Core/Core/Src/HW/SI_DeviceDanceMat.cpp @@ -23,242 +23,37 @@ // --- Dance mat gamecube controller --- CSIDevice_DanceMat::CSIDevice_DanceMat(SIDevices device, int _iDeviceNumber) - : ISIDevice(device, _iDeviceNumber) - , m_TButtonComboStart(0) - , m_TButtonCombo(0) - , m_LastButtonCombo(COMBO_NONE) -{ - memset(&m_Origin, 0, sizeof(SOrigin)); - m_Origin.uCommand = CMD_ORIGIN; - m_Origin.uOriginStickX = 0x80; // center - m_Origin.uOriginStickY = 0x80; - m_Origin.uSubStickStickX = 0x80; - m_Origin.uSubStickStickY = 0x80; - m_Origin.uTrigger_L = 0x00; - m_Origin.uTrigger_R = 0x00; - - // Dunno if we need to do this, game/lib should set it? - m_Mode = 0x03; -} - -int CSIDevice_DanceMat::RunBuffer(u8* _pBuffer, int _iLength) -{ - // For debug logging only - ISIDevice::RunBuffer(_pBuffer, _iLength); - - // Read the command - EBufferCommands command = static_cast(_pBuffer[3]); - - // Handle it - switch (command) - { - case CMD_RESET: - *(u32*)&_pBuffer[0] = SI_DANCEMAT; - break; - - case CMD_DIRECT: - { - INFO_LOG(SERIALINTERFACE, "PAD - Direct (Length: %d)", _iLength); - u32 high, low; - GetData(high, low); - for (int i = 0; i < (_iLength - 1) / 2; i++) - { - _pBuffer[0 + i] = (high >> (i * 8)) & 0xff; - _pBuffer[4 + i] = (low >> (i * 8)) & 0xff; - } - } - break; + : CSIDevice_GCController(device, _iDeviceNumber) {} - case CMD_ORIGIN: - { - INFO_LOG(SERIALINTERFACE, "PAD - Get Origin"); - u8* pCalibration = reinterpret_cast(&m_Origin); - for (int i = 0; i < (int)sizeof(SOrigin); i++) - { - _pBuffer[i ^ 3] = *pCalibration++; - } - } - break; - // Recalibrate (FiRES: i am not 100 percent sure about this) - case CMD_RECALIBRATE: - { - INFO_LOG(SERIALINTERFACE, "PAD - Recalibrate"); - u8* pCalibration = reinterpret_cast(&m_Origin); - for (int i = 0; i < (int)sizeof(SOrigin); i++) - { - _pBuffer[i ^ 3] = *pCalibration++; - } - } - break; - - // DEFAULT - default: - { - ERROR_LOG(SERIALINTERFACE, "Unknown SI command (0x%x)", command); - PanicAlert("SI: Unknown command (0x%x)", command); - } - break; - } - - return _iLength; -} - - -// GetData - -// Return true on new data (max 7 Bytes and 6 bits ;) -// [00?SYXBA] [1LRZUDRL] [x] [y] [cx] [cy] [l] [r] -// |\_ ERR_LATCH (error latched - check SISR) -// |_ ERR_STATUS (error on last GetData or SendCmd?) -bool CSIDevice_DanceMat::GetData(u32& _Hi, u32& _Low) +u32 CSIDevice_DanceMat::MapPadStatus(const SPADStatus& padStatus) { - SPADStatus PadStatus; - memset(&PadStatus, 0, sizeof(PadStatus)); - - Pad::GetStatus(ISIDevice::m_iDeviceNumber, &PadStatus); - Movie::CallInputManip(&PadStatus, ISIDevice::m_iDeviceNumber); - - u32 netValues[2]; - if (NetPlay_GetInput(ISIDevice::m_iDeviceNumber, PadStatus, netValues)) - { - _Hi = netValues[0]; // first 4 bytes - _Low = netValues[1]; // last 4 bytes - return true; - } - - Movie::SetPolledDevice(); - - if(Movie::IsPlayingInput()) - { - Movie::PlayController(&PadStatus, ISIDevice::m_iDeviceNumber); - Movie::InputUpdate(); - } - else if(Movie::IsRecordingInput()) - { - Movie::RecordInput(&PadStatus, ISIDevice::m_iDeviceNumber); - Movie::InputUpdate(); - } - else - { - Movie::CheckPadStatus(&PadStatus, ISIDevice::m_iDeviceNumber); - } - // Map the dpad to the blue arrows, the buttons to the orange arrows // Z = + button, Start = - button u16 map = 0; - if (PadStatus.button & PAD_BUTTON_UP) + if (padStatus.button & PAD_BUTTON_UP) map |= 0x1000; - if (PadStatus.button & PAD_BUTTON_DOWN) + if (padStatus.button & PAD_BUTTON_DOWN) map |= 0x2; - if (PadStatus.button & PAD_BUTTON_LEFT) + if (padStatus.button & PAD_BUTTON_LEFT) map |= 0x8; - if (PadStatus.button & PAD_BUTTON_RIGHT) + if (padStatus.button & PAD_BUTTON_RIGHT) map |= 0x4; - if (PadStatus.button & PAD_BUTTON_Y) + if (padStatus.button & PAD_BUTTON_Y) map |= 0x200; - if (PadStatus.button & PAD_BUTTON_A) + if (padStatus.button & PAD_BUTTON_A) map |= 0x10; - if (PadStatus.button & PAD_BUTTON_B) + if (padStatus.button & PAD_BUTTON_B) map |= 0x100; - if (PadStatus.button & PAD_BUTTON_X) + if (padStatus.button & PAD_BUTTON_X) map |= 0x800; - if (PadStatus.button & PAD_TRIGGER_Z) + if (padStatus.button & PAD_TRIGGER_Z) map |= 0x400; - if (PadStatus.button & PAD_BUTTON_START) + if (padStatus.button & PAD_BUTTON_START) map |= 0x1; - _Hi = (u32)(map << 16) | 0x8080; - - // Low bits are packed differently per mode - if (m_Mode == 0 || m_Mode == 5 || m_Mode == 6 || m_Mode == 7) - { - _Low = (u8)(PadStatus.analogB >> 4); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.analogA >> 4) << 4); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.triggerRight >> 4) << 8); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.triggerLeft >> 4) << 12); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.substickY) << 16); // All 8 bits - _Low |= (u32)((u8)(PadStatus.substickX) << 24); // All 8 bits - } - else if (m_Mode == 1) - { - _Low = (u8)(PadStatus.analogB >> 4); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.analogA >> 4) << 4); // Top 4 bits - _Low |= (u32)((u8)PadStatus.triggerRight << 8); // All 8 bits - _Low |= (u32)((u8)PadStatus.triggerLeft << 16); // All 8 bits - _Low |= (u32)((u8)PadStatus.substickY << 24); // Top 4 bits - _Low |= (u32)((u8)PadStatus.substickX << 28); // Top 4 bits - } - else if (m_Mode == 2) - { - // Identifies the dance mat - _Low = 0x8080ffff; - } - else if (m_Mode == 3) - { - // Analog A/B are always 0 - _Low = (u8)PadStatus.triggerRight; // All 8 bits - _Low |= (u32)((u8)PadStatus.triggerLeft << 8); // All 8 bits - _Low |= (u32)((u8)PadStatus.substickY << 16); // All 8 bits - _Low |= (u32)((u8)PadStatus.substickX << 24); // All 8 bits - } - else if (m_Mode == 4) - { - _Low = (u8)(PadStatus.analogB); // All 8 bits - _Low |= (u32)((u8)(PadStatus.analogA) << 8); // All 8 bits - // triggerLeft/Right are always 0 - _Low |= (u32)((u8)PadStatus.substickY << 16); // All 8 bits - _Low |= (u32)((u8)PadStatus.substickX << 24); // All 8 bits - } - return true; + return (u32)(map << 16) | 0x8080; } - -// SendCommand -void CSIDevice_DanceMat::SendCommand(u32 _Cmd, u8 _Poll) -{ - UCommand command(_Cmd); - - switch (command.Command) - { - // Costis sent it in some demos :) - case 0x00: - break; - - case CMD_WRITE: - { - unsigned int uType = command.Parameter1; // 0 = stop, 1 = rumble, 2 = stop hard - unsigned int uStrength = command.Parameter2; - - // get the correct pad number that should rumble locally when using netplay - const u8 numPAD = NetPlay_InGamePadToLocalPad(ISIDevice::m_iDeviceNumber); - - if (numPAD < 4) - Pad::Rumble(numPAD, uType, uStrength); - - if (!_Poll) - { - m_Mode = command.Parameter2; - INFO_LOG(SERIALINTERFACE, "PAD %i set to mode %i", ISIDevice::m_iDeviceNumber, m_Mode); - } - } - break; - - default: - { - ERROR_LOG(SERIALINTERFACE, "Unknown direct command (0x%x)", _Cmd); - PanicAlert("SI: Unknown direct command"); - } - break; - } -} - -// Savestate support -void CSIDevice_DanceMat::DoState(PointerWrap& p) -{ - p.Do(m_Origin); - p.Do(m_Mode); - p.Do(m_TButtonComboStart); - p.Do(m_TButtonCombo); - p.Do(m_LastButtonCombo); -} +void CSIDevice_DanceMat::HandleButtonCombos(const SPADStatus& padStatus) +{} diff --git a/Source/Core/Core/Src/HW/SI_DeviceDanceMat.h b/Source/Core/Core/Src/HW/SI_DeviceDanceMat.h index 7c413026ec46..64d6bf6cf03d 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceDanceMat.h +++ b/Source/Core/Core/Src/HW/SI_DeviceDanceMat.h @@ -7,99 +7,15 @@ #include "SI_Device.h" #include "GCPadStatus.h" - +#include "SI_DeviceGCController.h" // standard gamecube controller -class CSIDevice_DanceMat : public ISIDevice +class CSIDevice_DanceMat : public CSIDevice_GCController { -private: - - // Commands - enum EBufferCommands - { - CMD_RESET = 0x00, - CMD_DIRECT = 0x40, - CMD_ORIGIN = 0x41, - CMD_RECALIBRATE = 0x42, - }; - - struct SOrigin - { - u8 uCommand;// Maybe should be button bits? - u8 unk_1; // ..and this would be the other half - u8 uOriginStickX; - u8 uOriginStickY; - u8 uSubStickStickX; - u8 uSubStickStickY; - u8 uTrigger_L; - u8 uTrigger_R; - u8 unk_4; - u8 unk_5; - u8 unk_6; - u8 unk_7; - }; - - enum EDirectCommands - { - CMD_WRITE = 0x40 - }; - - union UCommand - { - u32 Hex; - struct - { - u32 Parameter1 : 8; - u32 Parameter2 : 8; - u32 Command : 8; - u32 : 8; - }; - UCommand() {Hex = 0;} - UCommand(u32 _iValue) {Hex = _iValue;} - }; - - enum EButtonCombo - { - COMBO_NONE = 0, - COMBO_ORIGIN, - COMBO_RESET - }; - - // struct to compare input against - // Set on connection and (standard pad only) on button combo - SOrigin m_Origin; - - // PADAnalogMode - u8 m_Mode; - - // Timer to track special button combos: - // y, X, start for 3 seconds updates origin with current status - // Technically, the above is only on standard pad, wavebird does not support it for example - // b, x, start for 3 seconds triggers reset (PI reset button interrupt) - u64 m_TButtonComboStart, m_TButtonCombo; - // Type of button combo from the last/current poll - EButtonCombo m_LastButtonCombo; - public: - - // Constructor CSIDevice_DanceMat(SIDevices device, int _iDeviceNumber); - - // Run the SI Buffer - virtual int RunBuffer(u8* _pBuffer, int _iLength); - - // Send and Receive pad input from network - static bool NetPlay_GetInput(u8 numPAD, SPADStatus status, u32 *PADStatus); - static u8 NetPlay_InGamePadToLocalPad(u8 numPAD); - - // Return true on new data - virtual bool GetData(u32& _Hi, u32& _Low); - - // Send a command directly - virtual void SendCommand(u32 _Cmd, u8 _Poll); - - // Savestate support - virtual void DoState(PointerWrap& p); + virtual u32 MapPadStatus(const SPADStatus& padStatus) override; + virtual void HandleButtonCombos(const SPADStatus& padStatus) override; }; #endif diff --git a/Source/Core/Core/Src/HW/SI_DeviceGCController.cpp b/Source/Core/Core/Src/HW/SI_DeviceGCController.cpp index 0c4bf48dcbd4..7cc19681d194 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceGCController.cpp +++ b/Source/Core/Core/Src/HW/SI_DeviceGCController.cpp @@ -144,10 +144,7 @@ bool CSIDevice_GCController::GetData(u32& _Hi, u32& _Low) Movie::CheckPadStatus(&PadStatus, ISIDevice::m_iDeviceNumber); } - // Thankfully changing mode does not change the high bits ;) - _Hi = (u32)((u8)PadStatus.stickY); - _Hi |= (u32)((u8)PadStatus.stickX << 8); - _Hi |= (u32)((u16)(PadStatus.button | PAD_USE_ORIGIN) << 16); + _Hi = MapPadStatus(PadStatus); // Low bits are packed differently per mode if (m_Mode == 0 || m_Mode == 5 || m_Mode == 6 || m_Mode == 7) @@ -194,11 +191,27 @@ bool CSIDevice_GCController::GetData(u32& _Hi, u32& _Low) _Low |= (u32)((u8)PadStatus.substickX << 24); // All 8 bits } + HandleButtonCombos(PadStatus); + return true; +} + +u32 CSIDevice_GCController::MapPadStatus(const SPADStatus& padStatus) +{ + // Thankfully changing mode does not change the high bits ;) + u32 _Hi = 0; + _Hi = (u32)((u8)padStatus.stickY); + _Hi |= (u32)((u8)padStatus.stickX << 8); + _Hi |= (u32)((u16)(padStatus.button | PAD_USE_ORIGIN) << 16); + return _Hi; +} + +void CSIDevice_GCController::HandleButtonCombos(const SPADStatus& padStatus) +{ // Keep track of the special button combos (embedded in controller hardware... :( ) EButtonCombo tempCombo; - if ((PadStatus.button & 0xff00) == (PAD_BUTTON_Y|PAD_BUTTON_X|PAD_BUTTON_START)) + if ((padStatus.button & 0xff00) == (PAD_BUTTON_Y|PAD_BUTTON_X|PAD_BUTTON_START)) tempCombo = COMBO_ORIGIN; - else if ((PadStatus.button & 0xff00) == (PAD_BUTTON_B|PAD_BUTTON_X|PAD_BUTTON_START)) + else if ((padStatus.button & 0xff00) == (PAD_BUTTON_B|PAD_BUTTON_X|PAD_BUTTON_START)) tempCombo = COMBO_RESET; else tempCombo = COMBO_NONE; @@ -217,21 +230,18 @@ bool CSIDevice_GCController::GetData(u32& _Hi, u32& _Low) ProcessorInterface::ResetButton_Tap(); else if (m_LastButtonCombo == COMBO_ORIGIN) { - m_Origin.uOriginStickX = PadStatus.stickX; - m_Origin.uOriginStickY = PadStatus.stickY; - m_Origin.uSubStickStickX = PadStatus.substickX; - m_Origin.uSubStickStickY = PadStatus.substickY; - m_Origin.uTrigger_L = PadStatus.triggerLeft; - m_Origin.uTrigger_R = PadStatus.triggerRight; + m_Origin.uOriginStickX = padStatus.stickX; + m_Origin.uOriginStickY = padStatus.stickY; + m_Origin.uSubStickStickX = padStatus.substickX; + m_Origin.uSubStickStickY = padStatus.substickY; + m_Origin.uTrigger_L = padStatus.triggerLeft; + m_Origin.uTrigger_R = padStatus.triggerRight; } m_LastButtonCombo = COMBO_NONE; } } - - return true; } - // SendCommand void CSIDevice_GCController::SendCommand(u32 _Cmd, u8 _Poll) { diff --git a/Source/Core/Core/Src/HW/SI_DeviceGCController.h b/Source/Core/Core/Src/HW/SI_DeviceGCController.h index 9673041c6f21..8f5d08b61054 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceGCController.h +++ b/Source/Core/Core/Src/HW/SI_DeviceGCController.h @@ -12,7 +12,7 @@ // standard gamecube controller class CSIDevice_GCController : public ISIDevice { -private: +protected: // Commands enum EBufferCommands @@ -95,6 +95,9 @@ class CSIDevice_GCController : public ISIDevice // Return true on new data virtual bool GetData(u32& _Hi, u32& _Low); + virtual u32 MapPadStatus(const SPADStatus& padStatus); + virtual void HandleButtonCombos(const SPADStatus& padStatus); + // Send a command directly virtual void SendCommand(u32 _Cmd, u8 _Poll); diff --git a/Source/Core/Core/Src/HW/SI_DeviceGCSteeringWheel.cpp b/Source/Core/Core/Src/HW/SI_DeviceGCSteeringWheel.cpp index 4102d291930b..0da486f2cf4c 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceGCSteeringWheel.cpp +++ b/Source/Core/Core/Src/HW/SI_DeviceGCSteeringWheel.cpp @@ -23,218 +23,8 @@ // --- standard gamecube controller --- CSIDevice_GCSteeringWheel::CSIDevice_GCSteeringWheel(SIDevices device, int _iDeviceNumber) - : ISIDevice(device, _iDeviceNumber) - , m_TButtonComboStart(0) - , m_TButtonCombo(0) - , m_LastButtonCombo(COMBO_NONE) -{ - memset(&m_Origin, 0, sizeof(SOrigin)); - m_Origin.uCommand = CMD_ORIGIN; - m_Origin.uOriginStickX = 0x80; // center - m_Origin.uOriginStickY = 0x80; - m_Origin.uSubStickStickX = 0x80; - m_Origin.uSubStickStickY = 0x80; - m_Origin.uTrigger_L = 0x1F; // 0-30 is the lower deadzone - m_Origin.uTrigger_R = 0x1F; - - // Dunno if we need to do this, game/lib should set it? - m_Mode = 0x03; -} - -int CSIDevice_GCSteeringWheel::RunBuffer(u8* _pBuffer, int _iLength) -{ - // For debug logging only - ISIDevice::RunBuffer(_pBuffer, _iLength); - - // Read the command - EBufferCommands command = static_cast(_pBuffer[3]); - - // Handle it - switch (command) - { - case CMD_RESET: - *(u32*)&_pBuffer[0] = SI_GC_STEERING; - break; - - case CMD_ORIGIN: - { - INFO_LOG(SERIALINTERFACE, "PAD - Get Origin"); - u8* pCalibration = reinterpret_cast(&m_Origin); - for (int i = 0; i < (int)sizeof(SOrigin); i++) - { - _pBuffer[i ^ 3] = *pCalibration++; - } - } - break; - - // Recalibrate (FiRES: i am not 100 percent sure about this) - case CMD_RECALIBRATE: - { - INFO_LOG(SERIALINTERFACE, "PAD - Recalibrate"); - u8* pCalibration = reinterpret_cast(&m_Origin); - for (int i = 0; i < (int)sizeof(SOrigin); i++) - { - _pBuffer[i ^ 3] = *pCalibration++; - } - } - break; - - // Seen in F-Zero GX - case CMD_MOTOR_OFF: - break; - - // DEFAULT - default: - { - ERROR_LOG(SERIALINTERFACE, "Unknown SI command (0x%x)", command); - } - break; - } - - return _iLength; -} - - -// GetData - -// Return true on new data (max 7 Bytes and 6 bits ;) -// [00?SYXBA] [1LRZUDRL] [x] [y] [cx] [cy] [l] [r] -// |\_ ERR_LATCH (error latched - check SISR) -// |_ ERR_STATUS (error on last GetData or SendCmd?) -bool CSIDevice_GCSteeringWheel::GetData(u32& _Hi, u32& _Low) -{ - SPADStatus PadStatus; - memset(&PadStatus, 0, sizeof(PadStatus)); - - Pad::GetStatus(ISIDevice::m_iDeviceNumber, &PadStatus); - Movie::CallInputManip(&PadStatus, ISIDevice::m_iDeviceNumber); - - u32 netValues[2]; - if (NetPlay_GetInput(ISIDevice::m_iDeviceNumber, PadStatus, netValues)) - { - _Hi = netValues[0]; // first 4 bytes - _Low = netValues[1]; // last 4 bytes - return true; - } - - Movie::SetPolledDevice(); - - if(Movie::IsPlayingInput()) - { - Movie::PlayController(&PadStatus, ISIDevice::m_iDeviceNumber); - Movie::InputUpdate(); - } - else if(Movie::IsRecordingInput()) - { - Movie::RecordInput(&PadStatus, ISIDevice::m_iDeviceNumber); - Movie::InputUpdate(); - } - else - { - Movie::CheckPadStatus(&PadStatus, ISIDevice::m_iDeviceNumber); - } - - // Thankfully changing mode does not change the high bits ;) - _Hi = (u32)((u8)PadStatus.stickX); // Steering - _Hi |= 0x800; // Pedal connected flag - _Hi |= (u32)((u16)(PadStatus.button | PAD_USE_ORIGIN) << 16); - - // Low bits are packed differently per mode - if (m_Mode == 0 || m_Mode == 5 || m_Mode == 7) - { - _Low = (u8)(PadStatus.analogB >> 4); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.analogA >> 4) << 4); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.triggerRight >> 4) << 8); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.triggerLeft >> 4) << 12); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.substickY) << 16); // All 8 bits - _Low |= (u32)((u8)(PadStatus.substickX) << 24); // All 8 bits - } - else if (m_Mode == 1) - { - _Low = (u8)(PadStatus.analogB >> 4); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.analogA >> 4) << 4); // Top 4 bits - _Low |= (u32)((u8)PadStatus.triggerRight << 8); // All 8 bits - _Low |= (u32)((u8)PadStatus.triggerLeft << 16); // All 8 bits - _Low |= (u32)((u8)PadStatus.substickY << 24); // Top 4 bits - _Low |= (u32)((u8)PadStatus.substickX << 28); // Top 4 bits - } - else if (m_Mode == 2) - { - _Low = (u8)(PadStatus.analogB); // All 8 bits - _Low |= (u32)((u8)(PadStatus.analogA) << 8); // All 8 bits - _Low |= (u32)((u8)(PadStatus.triggerRight >> 4) << 16); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.triggerLeft >> 4) << 20); // Top 4 bits - _Low |= (u32)((u8)PadStatus.substickY << 24); // Top 4 bits - _Low |= (u32)((u8)PadStatus.substickX << 28); // Top 4 bits - } - else if (m_Mode == 3) - { - // Analog A/B are always 0 - _Low = (u8)PadStatus.triggerRight; // All 8 bits - _Low |= (u32)((u8)PadStatus.triggerLeft << 8); // All 8 bits - _Low |= (u32)((u8)PadStatus.substickY << 16); // All 8 bits - _Low |= (u32)((u8)PadStatus.substickX << 24); // All 8 bits - } - else if (m_Mode == 4) - { - _Low = (u8)(PadStatus.analogB); // All 8 bits - _Low |= (u32)((u8)(PadStatus.analogA) << 8); // All 8 bits - // triggerLeft/Right are always 0 - _Low |= (u32)((u8)PadStatus.substickY << 16); // All 8 bits - _Low |= (u32)((u8)PadStatus.substickX << 24); // All 8 bits - } - else if (m_Mode == 6) - { - _Low = (u8)PadStatus.triggerRight; // All 8 bits - _Low |= (u32)((u8)PadStatus.triggerLeft << 8); // All 8 bits - - // The GC Steering Wheel appears to have combined pedals - // (both the Accelerate and Brake pedals are mapped to a single axis) - // We use the stickY axis for the pedals. - if (PadStatus.stickY < 128) - _Low |= (u32)((u8)(255 - ((PadStatus.stickY & 0x7f) * 2)) << 16); // All 8 bits (Brake) - if (PadStatus.stickY >= 128) - _Low |= (u32)((u8)((PadStatus.stickY & 0x7f) * 2) << 24); // All 8 bits (Accelerate) - } - - // Keep track of the special button combos (embedded in controller hardware... :( ) - EButtonCombo tempCombo; - if ((PadStatus.button & 0xff00) == (PAD_BUTTON_Y|PAD_BUTTON_X|PAD_BUTTON_START)) - tempCombo = COMBO_ORIGIN; - else if ((PadStatus.button & 0xff00) == (PAD_BUTTON_B|PAD_BUTTON_X|PAD_BUTTON_START)) - tempCombo = COMBO_RESET; - else - tempCombo = COMBO_NONE; - if (tempCombo != m_LastButtonCombo) - { - m_LastButtonCombo = tempCombo; - if (m_LastButtonCombo != COMBO_NONE) - m_TButtonComboStart = CoreTiming::GetTicks(); - } - if (m_LastButtonCombo != COMBO_NONE) - { - m_TButtonCombo = CoreTiming::GetTicks(); - if ((m_TButtonCombo - m_TButtonComboStart) > SystemTimers::GetTicksPerSecond() * 3) - { - if (m_LastButtonCombo == COMBO_RESET) - { - ProcessorInterface::ResetButton_Tap(); - } - else if (m_LastButtonCombo == COMBO_ORIGIN) - { - m_Origin.uOriginStickX = PadStatus.stickX; - m_Origin.uOriginStickY = PadStatus.stickY; - m_Origin.uSubStickStickX = PadStatus.substickX; - m_Origin.uSubStickStickY = PadStatus.substickY; - m_Origin.uTrigger_L = PadStatus.triggerLeft; - m_Origin.uTrigger_R = PadStatus.triggerRight; - } - m_LastButtonCombo = COMBO_NONE; - } - } - - return true; -} + : CSIDevice_GCController(device, _iDeviceNumber) +{} // SendCommand @@ -242,64 +32,25 @@ void CSIDevice_GCSteeringWheel::SendCommand(u32 _Cmd, u8 _Poll) { UCommand command(_Cmd); - switch (command.Command) + if (command.Command == CMD_FORCE) { - // Costis sent it in some demos :) - case 0x00: - break; - - case CMD_FORCE: - { - unsigned int uStrength = command.Parameter1; // 0 = left strong, 127 = left weak, 128 = right weak, 255 = right strong - unsigned int uType = command.Parameter2; // 06 = motor on, 04 = motor off + unsigned int uStrength = command.Parameter1; // 0 = left strong, 127 = left weak, 128 = right weak, 255 = right strong + unsigned int uType = command.Parameter2; // 06 = motor on, 04 = motor off - // get the correct pad number that should rumble locally when using netplay - const u8 numPAD = NetPlay_InGamePadToLocalPad(ISIDevice::m_iDeviceNumber); + // get the correct pad number that should rumble locally when using netplay + const u8 numPAD = NetPlay_InGamePadToLocalPad(ISIDevice::m_iDeviceNumber); - if (numPAD < 4) - Pad::Motor(numPAD, uType, uStrength); + if (numPAD < 4) + Pad::Motor(numPAD, uType, uStrength); - if (!_Poll) - { - m_Mode = command.Parameter2; - INFO_LOG(SERIALINTERFACE, "PAD %i set to mode %i", ISIDevice::m_iDeviceNumber, m_Mode); - } - } - break; - - case CMD_WRITE: + if (!_Poll) { - unsigned int uType = command.Parameter1; // 0 = stop, 1 = rumble, 2 = stop hard - unsigned int uStrength = command.Parameter2; - - // get the correct pad number that should rumble locally when using netplay - const u8 numPAD = NetPlay_InGamePadToLocalPad(ISIDevice::m_iDeviceNumber); - - if (numPAD < 4) - Pad::Rumble(numPAD, uType, uStrength); - - if (!_Poll) - { - m_Mode = command.Parameter2; - INFO_LOG(SERIALINTERFACE, "PAD %i set to mode %i", ISIDevice::m_iDeviceNumber, m_Mode); - } + m_Mode = command.Parameter2; + INFO_LOG(SERIALINTERFACE, "PAD %i set to mode %i", ISIDevice::m_iDeviceNumber, m_Mode); } - break; - - default: - { - ERROR_LOG(SERIALINTERFACE, "Unknown direct command (0x%x)", _Cmd); - } - break; } -} - -// Savestate support -void CSIDevice_GCSteeringWheel::DoState(PointerWrap& p) -{ - p.Do(m_Origin); - p.Do(m_Mode); - p.Do(m_TButtonComboStart); - p.Do(m_TButtonCombo); - p.Do(m_LastButtonCombo); + else + { + return CSIDevice_GCController::SendCommand(_Cmd, _Poll); + } } diff --git a/Source/Core/Core/Src/HW/SI_DeviceGCSteeringWheel.h b/Source/Core/Core/Src/HW/SI_DeviceGCSteeringWheel.h index 2398b8f321a0..2e11d13319cf 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceGCSteeringWheel.h +++ b/Source/Core/Core/Src/HW/SI_DeviceGCSteeringWheel.h @@ -7,13 +7,12 @@ #include "SI_Device.h" #include "GCPadStatus.h" - +#include "SI_DeviceGCController.h" // standard gamecube controller -class CSIDevice_GCSteeringWheel : public ISIDevice +class CSIDevice_GCSteeringWheel : public CSIDevice_GCController { private: - // Commands enum EBufferCommands { @@ -23,84 +22,19 @@ class CSIDevice_GCSteeringWheel : public ISIDevice CMD_MOTOR_OFF = 0xff, }; - struct SOrigin - { - u8 uCommand;// Maybe should be button bits? - u8 unk_1; // ..and this would be the other half - u8 uOriginStickX; - u8 uOriginStickY; - u8 uSubStickStickX; - u8 uSubStickStickY; - u8 uTrigger_L; - u8 uTrigger_R; - u8 unk_4; - u8 unk_5; - u8 unk_6; - u8 unk_7; - }; - enum EDirectCommands { CMD_FORCE = 0x30, CMD_WRITE = 0x40 }; - union UCommand - { - u32 Hex; - struct - { - u32 Parameter1 : 8; - u32 Parameter2 : 8; - u32 Command : 8; - u32 : 8; - }; - UCommand() {Hex = 0;} - UCommand(u32 _iValue) {Hex = _iValue;} - }; - - enum EButtonCombo - { - COMBO_NONE = 0, - COMBO_ORIGIN, - COMBO_RESET - }; - - // struct to compare input against - // Set on connection and (standard pad only) on button combo - SOrigin m_Origin; - - // PADAnalogMode - u8 m_Mode; - - // Timer to track special button combos: - // y, X, start for 3 seconds updates origin with current status - // Technically, the above is only on standard pad, wavebird does not support it for example - // b, x, start for 3 seconds triggers reset (PI reset button interrupt) - u64 m_TButtonComboStart, m_TButtonCombo; - // Type of button combo from the last/current poll - EButtonCombo m_LastButtonCombo; - public: // Constructor CSIDevice_GCSteeringWheel(SIDevices device, int _iDeviceNumber); - // Run the SI Buffer - virtual int RunBuffer(u8* _pBuffer, int _iLength); - - // Send and Receive pad input from network - static bool NetPlay_GetInput(u8 numPAD, SPADStatus status, u32 *PADStatus); - static u8 NetPlay_InGamePadToLocalPad(u8 numPAD); - - // Return true on new data - virtual bool GetData(u32& _Hi, u32& _Low); - // Send a command directly virtual void SendCommand(u32 _Cmd, u8 _Poll); - - // Savestate support - virtual void DoState(PointerWrap& p); }; #endif diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 019e22edd501..0092781d6cea 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -11,8 +11,6 @@ // for gcpad #include "HW/SI.h" #include "HW/SI_DeviceGCController.h" -#include "HW/SI_DeviceGCSteeringWheel.h" -#include "HW/SI_DeviceDanceMat.h" // for gctime #include "HW/EXI_DeviceIPL.h" // for wiimote/ OSD messages @@ -980,16 +978,6 @@ bool WiimoteEmu::Wiimote::NetPlay_GetWiimoteData(int wiimote, u8* data, u8 size) return false; } -bool CSIDevice_GCSteeringWheel::NetPlay_GetInput(u8 numPAD, SPADStatus PadStatus, u32 *PADStatus) -{ - return CSIDevice_GCController::NetPlay_GetInput(numPAD, PadStatus, PADStatus); -} - -bool CSIDevice_DanceMat::NetPlay_GetInput(u8 numPAD, SPADStatus PadStatus, u32 *PADStatus) -{ - return CSIDevice_GCController::NetPlay_GetInput(numPAD, PadStatus, PADStatus); -} - // called from ---CPU--- thread // so all players' games get the same time u32 CEXIIPL::NetPlay_GetGCTime() @@ -1014,16 +1002,6 @@ u8 CSIDevice_GCController::NetPlay_InGamePadToLocalPad(u8 numPAD) return numPAD; } -u8 CSIDevice_GCSteeringWheel::NetPlay_InGamePadToLocalPad(u8 numPAD) -{ - return CSIDevice_GCController::NetPlay_InGamePadToLocalPad(numPAD); -} - -u8 CSIDevice_DanceMat::NetPlay_InGamePadToLocalPad(u8 numPAD) -{ - return CSIDevice_GCController::NetPlay_InGamePadToLocalPad(numPAD); -} - bool NetPlay::IsNetPlayRunning() { return netplay_client != NULL; From b65e89091aa1cd9de67d40ea043a7babf7c1422a Mon Sep 17 00:00:00 2001 From: comex Date: Sun, 25 Aug 2013 20:06:58 -0400 Subject: [PATCH 040/202] Improve ChunkFile.h: - Add support for std::set and std:pair. - Switch from std::is_pod to std::is_trivially_copyable, to allow for types that have constructors but trivial copy constructors. Easy, except there are two different nonstandard versions of it required on different platforms, in addition to the standard one. Conflicts: Source/Core/Common/Src/ChunkFile.h --- Source/Core/Common/Src/ChunkFile.h | 53 +++++++++++++++++-- .../Core/Src/IPC_HLE/WII_IPC_HLE_Device_usb.h | 7 --- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index b44f15329684..329518ececb7 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -16,6 +16,7 @@ // - Serialization code for anything complex has to be manually written. #include +#include #include #include #include @@ -119,6 +120,17 @@ class PWBuffer : public NonCopyable size_t m_Capacity; }; +// ewww +#if _LIBCPP_VERSION +#define IsTriviallyCopyable(T) std::is_trivially_copyable::value +#elif __GNUC__ +#define IsTriviallyCopyable(T) std::has_trivial_copy_constructor::value +#elif defined(_WIN32) +#define IsTriviallyCopyable(T) std::has_trivial_copy::value +#else +#error No version of is_trivially_copyable +#endif + // Wrapper class class PointerWrap { @@ -177,6 +189,33 @@ class PointerWrap } } + template + void Do(std::set& x) + { + u32 count = (u32)x.size(); + Do(count); + + switch (mode) + { + case MODE_READ: + for (x.clear(); count != 0; --count) + { + V value; + Do(value); + x.insert(value); + } + break; + + case MODE_WRITE: + case MODE_VERIFY: + for (auto itr = x.begin(); itr != x.end(); ++itr) + { + Do(*itr); + } + break; + } + } + template void DoContainer(T& x) { @@ -224,6 +263,13 @@ class PointerWrap DoContainer(x); } + template + void Do(std::pair& x) + { + Do(x.first); + Do(x.second); + } + template void DoArray(T* x, u32 count) { @@ -241,12 +287,11 @@ class PointerWrap template void Do(T& x) { - // Ideally this would be std::is_trivially_copyable, but not enough support yet - static_assert(std::is_pod::value, "Only sane for POD types"); - + static_assert(IsTriviallyCopyable(T), "Only sane for trivially copyable types"); + DoVoid((void*)&x, sizeof(x)); } - + template void DoPOD(T& x) { diff --git a/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_usb.h b/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_usb.h index 253f2d08dee3..f6abbc67fe49 100644 --- a/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_usb.h +++ b/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_usb.h @@ -39,13 +39,6 @@ struct SQueuedEvent } }; -// Hacks for ChunkFile to accept SQueuedEvent as POD -namespace std -{ -template <> -struct is_pod : std::true_type {}; -} - // Important to remember that this class is for /dev/usb/oh1/57e/305 ONLY // /dev/usb/oh1 -> internal usb bus // 57e/305 -> VendorID/ProductID of device on usb bus From 079147ca07cc59cc4c3e70441abddcabd5ad3681 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Thu, 24 Oct 2013 14:55:10 -0400 Subject: [PATCH 041/202] [Android] Refactor InputConfigFragment a little bit in preparation for the implementation of the new input overlay. This moves all of the dialog handling into the actual MotionAlertDialog class itself. This is something I should have done a long time ago. Also moved the Gamecube input binding preferences into their own PreferenceScreen. --- Source/Android/res/values-ja/strings.xml | 1 + Source/Android/res/values/strings.xml | 1 + Source/Android/res/xml/input_prefs.xml | 164 ++++++------- .../settings/InputConfigFragment.java | 225 ++++++++---------- 4 files changed, 181 insertions(+), 210 deletions(-) diff --git a/Source/Android/res/values-ja/strings.xml b/Source/Android/res/values-ja/strings.xml index bf19db23f02b..4ecd85fd6239 100644 --- a/Source/Android/res/values-ja/strings.xml +++ b/Source/Android/res/values-ja/strings.xml @@ -41,6 +41,7 @@ 入力 + ゲームキューブの入力バインディング 入力バインディング %1$sにバインドするための入力を移動または押してください。 Aボタン diff --git a/Source/Android/res/values/strings.xml b/Source/Android/res/values/strings.xml index 983f9e6ace27..b4943e86321d 100644 --- a/Source/Android/res/values/strings.xml +++ b/Source/Android/res/values/strings.xml @@ -41,6 +41,7 @@ Input + Gamecube Input Bindings Input Binding Press or move an input to bind it to %1$s. Button A diff --git a/Source/Android/res/xml/input_prefs.xml b/Source/Android/res/xml/input_prefs.xml index daddf93e3dad..899e559480a1 100644 --- a/Source/Android/res/xml/input_prefs.xml +++ b/Source/Android/res/xml/input_prefs.xml @@ -1,83 +1,87 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/InputConfigFragment.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/InputConfigFragment.java index 26e89f412d76..1a3bc1fb5637 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/InputConfigFragment.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/InputConfigFragment.java @@ -6,7 +6,6 @@ package org.dolphinemu.dolphinemu.settings; -import android.app.Activity; import android.app.AlertDialog; import android.app.Fragment; import android.content.Context; @@ -31,10 +30,6 @@ */ public final class InputConfigFragment extends PreferenceFragment { - private Activity m_activity; - private boolean firstEvent = true; - private static final ArrayList m_values = new ArrayList(); - /** * Gets the descriptor for the given {@link InputDevice}. * @@ -92,161 +87,131 @@ public void onCreate(Bundle savedInstanceState) } @Override - public boolean onPreferenceTreeClick(final PreferenceScreen screen, final Preference pref) + public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference pref) { - // Begin the creation of the input alert. - final MotionAlertDialog dialog = new MotionAlertDialog(m_activity); - - // Set the key listener - dialog.setOnKeyListener(new AlertDialog.OnKeyListener() + // If the user is on the preference screen to set Gamecube input bindings. + if (screen.getTitle().equals(getString(R.string.gamecube_bindings))) { - public boolean onKey(DialogInterface dlg, int keyCode, KeyEvent event) - { - Log.d("InputConfigFragment", "Received key event: " + event.getAction()); - switch (event.getAction()) - { - case KeyEvent.ACTION_DOWN: - case KeyEvent.ACTION_UP: - InputDevice input = event.getDevice(); - String bindStr = "Device '" + getInputDesc(input) + "'-Button " + event.getKeyCode(); - NativeLibrary.SetConfig("Dolphin.ini", "Android", pref.getKey(), bindStr); - pref.setSummary(bindStr); - dialog.dismiss(); - return true; - - default: - break; - } - - return false; - } - }); + // Begin the creation of the input alert. + final MotionAlertDialog dialog = new MotionAlertDialog(getActivity(), pref); - // Set the motion event listener. - dialog.setOnMotionEventListener(new MotionAlertDialog.OnMotionEventListener() - { - public boolean onMotion(MotionEvent event) + // Set the cancel button. + dialog.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.cancel), new AlertDialog.OnClickListener() { - if (event == null || (event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) - return false; - - Log.d("InputConfigFragment", "Received motion event: " + event.getAction()); - - InputDevice input = event.getDevice(); - List motions = input.getMotionRanges(); - if (firstEvent) - { - m_values.clear(); - - for (InputDevice.MotionRange range : motions) - { - m_values.add(event.getAxisValue(range.getAxis())); - } - - firstEvent = false; - } - else + @Override + public void onClick(DialogInterface dialog, int which) { - for (int a = 0; a < motions.size(); ++a) - { - InputDevice.MotionRange range = motions.get(a); - - if (m_values.get(a) > (event.getAxisValue(range.getAxis()) + 0.5f)) - { - String bindStr = "Device '" + InputConfigFragment.getInputDesc(input) + "'-Axis " + range.getAxis() + "-"; - NativeLibrary.SetConfig("Dolphin.ini", "Android", pref.getKey(), bindStr); - pref.setSummary(bindStr); - dialog.dismiss(); - } - else if (m_values.get(a) < (event.getAxisValue(range.getAxis()) - 0.5f)) - { - String bindStr = "Device '" + InputConfigFragment.getInputDesc(input) + "'-Axis " + range.getAxis() + "+"; - NativeLibrary.SetConfig("Dolphin.ini", "Android", pref.getKey(), bindStr); - pref.setSummary(bindStr); - dialog.dismiss(); - } - } + // Do nothing. Just makes the cancel button show up. } + }); - return true; - } - }); - - // Set the cancel button. - dialog.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.cancel), new AlertDialog.OnClickListener() - { - public void onClick(DialogInterface dialog, int which) - { - // Do nothing. This just makes the cancel button appear. - } - }); + // Set the title and description message. + dialog.setTitle(R.string.input_binding); + dialog.setMessage(String.format(getString(R.string.input_binding_descrip), pref.getTitle())); - // Set the title and description message. - dialog.setTitle(R.string.input_binding); - dialog.setMessage(String.format(getString(R.string.input_binding_descrip), pref.getTitle())); + // Don't allow the dialog to close when a user taps + // outside of it. They must press cancel or provide an input. + dialog.setCanceledOnTouchOutside(false); - // Don't allow the dialog to close when a user taps - // outside of it. They must press cancel or provide an input. - dialog.setCanceledOnTouchOutside(false); + // Everything is set, show the dialog. + dialog.show(); + } - // Everything is set, show the dialog. - dialog.show(); return true; } - @Override - public void onAttach(Activity activity) - { - super.onAttach(activity); - - // Cache the activity instance. - m_activity = activity; - } - /** - * {@link AlertDialog} class derivative that allows the motion listener - * to be set anonymously, so the creation of an explicit class for - * providing functionality is not necessary. + * {@link AlertDialog} derivative that listens for + * motion events from controllers and joysticks. */ private static final class MotionAlertDialog extends AlertDialog { - private OnMotionEventListener motionListener; + // The selected input preference + private final Preference inputPref; + + private boolean firstEvent = true; + private final ArrayList m_values = new ArrayList(); /** * Constructor * * @param ctx context to use this dialog in. */ - public MotionAlertDialog(Context ctx) + public MotionAlertDialog(Context ctx, Preference inputPref) { super(ctx); + + this.inputPref = inputPref; } - /** - * Interface which defines a callback method for general - * motion events. This allows motion event code to be set - * in the event anonymous classes of this dialog are used. - */ - public interface OnMotionEventListener + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { - /** - * Denotes the behavior that should happen when a motion event occurs. - * - * @param event Reference to the {@link MotionEvent} that occurred. - * - * @return true if the {@link MotionEvent} is consumed in this call; false otherwise. - */ - boolean onMotion(MotionEvent event); + Log.d("InputConfigFragment", "Received key event: " + event.getAction()); + switch (event.getAction()) + { + case KeyEvent.ACTION_DOWN: + case KeyEvent.ACTION_UP: + InputDevice input = event.getDevice(); + String bindStr = "Device '" + getInputDesc(input) + "'-Button " + event.getKeyCode(); + NativeLibrary.SetConfig("Dolphin.ini", "Android", inputPref.getKey(), bindStr); + inputPref.setSummary(bindStr); + dismiss(); + return true; + + default: + break; + } + + return false; } + - /** - * Sets the motion listener. - * - * @param listener The motion listener to set. - */ - public void setOnMotionEventListener(OnMotionEventListener listener) + // Method that will be within dispatchGeneticMotionEvent that listens for joystick/controller movements. + private boolean onMotionEvent(MotionEvent event) { - this.motionListener = listener; + if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) + return false; + + Log.d("InputConfigFragment", "Received motion event: " + event.getAction()); + + InputDevice input = event.getDevice(); + List motions = input.getMotionRanges(); + if (firstEvent) + { + m_values.clear(); + + for (InputDevice.MotionRange range : motions) + { + m_values.add(event.getAxisValue(range.getAxis())); + } + + firstEvent = false; + } + else + { + for (int a = 0; a < motions.size(); ++a) + { + InputDevice.MotionRange range = motions.get(a); + + if (m_values.get(a) > (event.getAxisValue(range.getAxis()) + 0.5f)) + { + String bindStr = "Device '" + InputConfigFragment.getInputDesc(input) + "'-Axis " + range.getAxis() + "-"; + NativeLibrary.SetConfig("Dolphin.ini", "Android", inputPref.getKey(), bindStr); + inputPref.setSummary(bindStr); + dismiss(); + } + else if (m_values.get(a) < (event.getAxisValue(range.getAxis()) - 0.5f)) + { + String bindStr = "Device '" + InputConfigFragment.getInputDesc(input) + "'-Axis " + range.getAxis() + "+"; + NativeLibrary.SetConfig("Dolphin.ini", "Android", inputPref.getKey(), bindStr); + inputPref.setSummary(bindStr); + dismiss(); + } + } + } + + return true; } @Override @@ -261,7 +226,7 @@ public boolean dispatchKeyEvent(KeyEvent event) @Override public boolean dispatchGenericMotionEvent(MotionEvent event) { - if (motionListener.onMotion(event)) + if (onMotionEvent(event)) return true; return super.dispatchGenericMotionEvent(event); From 1267877e80d08a818983cc7b0c01a6f3e5061330 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Thu, 24 Oct 2013 15:10:25 -0400 Subject: [PATCH 042/202] [Android] Documentation typo cleanups from the last commit. Also cleaned up the Javadoc for the constructor of MotionAlertDialog. --- .../dolphinemu/dolphinemu/settings/InputConfigFragment.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/InputConfigFragment.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/InputConfigFragment.java index 1a3bc1fb5637..a23f1b4a3f11 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/InputConfigFragment.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/InputConfigFragment.java @@ -135,7 +135,8 @@ private static final class MotionAlertDialog extends AlertDialog /** * Constructor * - * @param ctx context to use this dialog in. + * @param ctx The current {@link Context}. + * @param inputPref The Preference to show this dialog for. */ public MotionAlertDialog(Context ctx, Preference inputPref) { @@ -167,7 +168,8 @@ public boolean onKeyDown(int keyCode, KeyEvent event) } - // Method that will be within dispatchGeneticMotionEvent that listens for joystick/controller movements. + // Method that will be called within dispatchGenericMotionEvent + // that handles joystick/controller movements. private boolean onMotionEvent(MotionEvent event) { if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) From e464235e19bbd0dc6b903d4c10e05fa5c25dac0a Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 24 Oct 2013 16:31:49 -0400 Subject: [PATCH 043/202] Fix build on Mavericks. --- Source/Core/Core/Src/HW/WiimoteReal/IOdarwin.mm | 1 - Source/Core/Core/Src/HW/WiimoteReal/WiimoteRealBase.h | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/Src/HW/WiimoteReal/IOdarwin.mm b/Source/Core/Core/Src/HW/WiimoteReal/IOdarwin.mm index 4f015866bc8f..f59cffee2c07 100644 --- a/Source/Core/Core/Src/HW/WiimoteReal/IOdarwin.mm +++ b/Source/Core/Core/Src/HW/WiimoteReal/IOdarwin.mm @@ -1,5 +1,4 @@ #define BLUETOOTH_VERSION_USE_CURRENT -#import #include "Common.h" #include "WiimoteReal.h" diff --git a/Source/Core/Core/Src/HW/WiimoteReal/WiimoteRealBase.h b/Source/Core/Core/Src/HW/WiimoteReal/WiimoteRealBase.h index 0ad78610fe26..82c18f6ec2cf 100644 --- a/Source/Core/Core/Src/HW/WiimoteReal/WiimoteRealBase.h +++ b/Source/Core/Core/Src/HW/WiimoteReal/WiimoteRealBase.h @@ -8,6 +8,13 @@ #ifdef _WIN32 #include #elif defined(__APPLE__) + // Work around an Apple bug: for some reason, IOBluetooth.h errors on + // inclusion in Mavericks, but only in Objective-C++ C++11 mode. I filed + // this as ; in the meantime... + #import + #undef NS_ENUM_AVAILABLE + #define NS_ENUM_AVAILABLE(...) + // end hack #import #elif defined(__linux__) && HAVE_BLUEZ #include From 9f36081a8f84cbef302a210a4c779250133ec5ba Mon Sep 17 00:00:00 2001 From: Lioncash Date: Fri, 25 Oct 2013 16:52:46 -0400 Subject: [PATCH 044/202] [Android] Implement a very basic version of the input overlay configuration screen. Still a bit of a mess, but this will get cleaned up during finalizations. --- Source/Android/AndroidManifest.xml | 2 + Source/Android/res/drawable/button_a.png | Bin 0 -> 5178 bytes Source/Android/res/drawable/button_b.png | Bin 0 -> 4971 bytes Source/Android/res/drawable/button_start.png | Bin 0 -> 1511 bytes Source/Android/res/layout/emulation_view.xml | 8 + .../layout/input_overlay_config_layout.xml | 32 ++++ Source/Android/res/xml/input_prefs.xml | 166 +++++++++--------- .../dolphinemu/dolphinemu/AboutFragment.java | 2 +- .../emulation/EmulationActivity.java | 4 +- .../emulation/overlay/InputOverlay.java | 64 +++++++ .../emulation/overlay/InputOverlayItem.java | 107 +++++++++++ .../dolphinemu/settings/PrefsActivity.java | 3 + .../{ => cpu}/CPUSettingsFragment.java | 2 +- .../{ => input}/InputConfigFragment.java | 13 +- .../input/InputOverlayConfigActivity.java | 27 +++ .../input/InputOverlayConfigButton.java | 52 ++++++ .../{ => video}/VideoSettingsFragment.java | 2 +- 17 files changed, 396 insertions(+), 88 deletions(-) create mode 100644 Source/Android/res/drawable/button_a.png create mode 100644 Source/Android/res/drawable/button_b.png create mode 100644 Source/Android/res/drawable/button_start.png create mode 100644 Source/Android/res/layout/input_overlay_config_layout.xml create mode 100644 Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java create mode 100644 Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayItem.java rename Source/Android/src/org/dolphinemu/dolphinemu/settings/{ => cpu}/CPUSettingsFragment.java (96%) rename Source/Android/src/org/dolphinemu/dolphinemu/settings/{ => input}/InputConfigFragment.java (95%) create mode 100644 Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigActivity.java create mode 100644 Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigButton.java rename Source/Android/src/org/dolphinemu/dolphinemu/settings/{ => video}/VideoSettingsFragment.java (99%) diff --git a/Source/Android/AndroidManifest.xml b/Source/Android/AndroidManifest.xml index 2e3964c65f44..5b2a3cfee9b7 100644 --- a/Source/Android/AndroidManifest.xml +++ b/Source/Android/AndroidManifest.xml @@ -38,6 +38,8 @@ + + diff --git a/Source/Android/res/drawable/button_a.png b/Source/Android/res/drawable/button_a.png new file mode 100644 index 0000000000000000000000000000000000000000..7e685324ae8a4ad016eb1033d64b659966cfcd0c GIT binary patch literal 5178 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 literal 0 HcmV?d00001 diff --git a/Source/Android/res/drawable/button_b.png b/Source/Android/res/drawable/button_b.png new file mode 100644 index 0000000000000000000000000000000000000000..21da4c20f123fe6409b57cc6d837fd0f60786098 GIT binary patch literal 4971 zcmV-x6O`Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000u&Nkl#ha<5{UH9txuIOkZs809n`&%Ts0+5dL7Mp+hx=8#iZN56lus~=G z^GCTaaPJYVwOL@?*!;U~#ueG%;YhSLCE<7mhNB0Na5}l)^@HUA@@`!uHsv#ADq?zo zpYg=zwNdDhX!8d-c_aTCE$TUE&YT=*_Uzdn@p|suxjFOZ>sC!i70?@py>=vnsf* zABcEaGs-XfrT!6#bQYs~VAgKTFnuJ!bnJ?0)28K2ojNsV%9JU&(B#RJ)5w0N`*OK| z`t<2JGiT1ssjRH@!1;S5@3b(JN@!YiKc8p6`(ykD&dIg$6ywpV4`Zf9vFvPwk$;b-RBzM@e$?6 z$Wr6Kj#sx*H;kk5H85_N&9zL99XmEUX3Ut_op;_D8$EiorMO5+{3x7k%M;3nl;MCf)`3~PTphN*gfO?uFFfb?2Oo%oz0URm<_{0chZ(StP zy7A)7;!Z-5eOQ9|=>GQ#3c47zMPf055Qn6~`Fg)VYF()Ji^Krb0@CNj4)MQ7+@pI7 z#dtm>AoE0Vv_hZ9&?>Fx0ipFh-vYDFzD(4K);2o`R`S-nqw;^~(4k11VN)^LYMvyM zuZ)J|mf>gJyuI(hX_Ilj`(*GUpYu(#k%(N4=m6L3(|4-05#k#i8`h}8u zfdSTnU`@l*24pB09UW`tTteFdVUU+03lu$0AWuS z3Lbu|@%COl)MI>*T7o)|Z6GZLgu{+M6KfjBpyQr7wWYKAh7A-nm)K{_o3~0fE z1(rUi|N97(Q$;5Uuu@$CtVBR|1WY{#ZE7J<3Z*Iem>W*gm_!b~0S+8tc|L)t(PR+n zBKf6(P`B^H7A*?plyG=aZ6uEkX@fA|T8wuPQzc^dL{aa>ix;0JIzx2!k|j&d6I~#> z=>|;HqW+?D1jOe>eF02B^kyxzh_$5|ZO|}*Q6y8zM-xmqWj{Fr z!+;P~0)=CYAN?>A`-t;MlR&UZj!~<333GBto~P&G*gR4}@`ib&UMdhyg>f->ju^gB z%wD!^*|IN+1}tB`{94f<(U(Qnnb!k#&o%n|N`2>2z9TwE+Hxj9XuVznqbr)FXSD?c zfX5sp3_@qFt~qj&9st-r2~sLSiyp|@A0$%IR6hQ`vN}B|N8UMCL~M^s64*U9-ww%( zdLngg{46o>1;aQ@Uw`kt_ug{PJ@*XOb;!zeo7h^qzSB?97He1`#{o6&^U))Y*l%WeWx;F}hfb8oVs5k;o^b#D_fKE5X3cET{IzS>E)*@& z>qWfQbzW6fRi*Bkrh5To3_wUDZqZtp$7T2sqY1tBtS&fP0nm~VNmj|eBp?q2mr4I& zDq7Vj0D@n#($_Z@WH#+ zuV24PR3%!sVZ(+8MC}JzzH;5Vb<1?`0s%5ZU`!AYBL(8^_zgh7r(j}e8_y-j zAkhKR$)1%j|TsiBx z8*chZHUPNyzS*_=zaCw%P)f9VlFVU*wCX0Uf0dqdzCh?hcqCaOV-(~04geu$5G!{g z!v2hAkO&{Kx)cD3_-XAQ^%wITZ1VM9vCwZ%7AJ}Hm3m$z=?_BslH7PCh+4nhwfnz= zAZ*>b^}E})ZTp_C-{1b&V?TId(c)Kse`4v!!TVnB+wTLJ&@6#aE)c$IOaU!8QyS1y z8qiT*zm)(eU|;MHL9t(#NmI8-Mud#@1Hbx557UQO-;>Dw{PG6Gw8`DpZuxlN4u&w?tiak z8!vlv>9N^!)?*3+pp5l-juNG_$rz;pC7d0knH~UWr(P4LeE^sY(JS24n;rTO6e*9% zf-DRG!-o%#iwb0G#Nkc?fNTx>$NAkV0LI9P&64!**|>4zrngIbrvdQPQ&0Uu^opTh z?AWp6B|v%YimUz+1Ylc5#a012Lk{sy!XpV45rMQ>;kS!Spp7J6WRBV%0AYS0a83w6 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 + + + \ No newline at end of file diff --git a/Source/Android/res/layout/input_overlay_config_layout.xml b/Source/Android/res/layout/input_overlay_config_layout.xml new file mode 100644 index 000000000000..868f0b3166d5 --- /dev/null +++ b/Source/Android/res/layout/input_overlay_config_layout.xml @@ -0,0 +1,32 @@ + + + + + + + + + \ No newline at end of file diff --git a/Source/Android/res/xml/input_prefs.xml b/Source/Android/res/xml/input_prefs.xml index 899e559480a1..5dc15ac75ec4 100644 --- a/Source/Android/res/xml/input_prefs.xml +++ b/Source/Android/res/xml/input_prefs.xml @@ -2,86 +2,90 @@ + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/AboutFragment.java b/Source/Android/src/org/dolphinemu/dolphinemu/AboutFragment.java index 8b29e607d7e0..3695bfa40bfb 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/AboutFragment.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/AboutFragment.java @@ -20,7 +20,7 @@ import java.util.ArrayList; import java.util.List; -import org.dolphinemu.dolphinemu.settings.VideoSettingsFragment; +import org.dolphinemu.dolphinemu.settings.video.VideoSettingsFragment; /** * Represents the about screen. diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/EmulationActivity.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/EmulationActivity.java index 912fc021c5a6..8b8f4e05517e 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/EmulationActivity.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/EmulationActivity.java @@ -21,8 +21,8 @@ import org.dolphinemu.dolphinemu.NativeLibrary; import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.settings.InputConfigFragment; -import org.dolphinemu.dolphinemu.settings.VideoSettingsFragment; +import org.dolphinemu.dolphinemu.settings.input.InputConfigFragment; +import org.dolphinemu.dolphinemu.settings.video.VideoSettingsFragment; import java.util.List; diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java new file mode 100644 index 000000000000..c2272fdc5f6f --- /dev/null +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java @@ -0,0 +1,64 @@ +package org.dolphinemu.dolphinemu.emulation.overlay; + +import java.util.HashSet; +import java.util.Set; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.SurfaceView; +import android.view.View; +import android.view.View.OnTouchListener; + +/** + * Draws the interactive input overlay on top of the + * {@link NativeGLSurfaceView} that is rendering emulation. + */ +public final class InputOverlay extends SurfaceView implements OnTouchListener +{ + private final Set overlayItems = new HashSet(); + + /** + * Constructor + * + * @param context The current {@link Context}. + * @param attrs {@link AttributeSet} for parsing XML attributes. + */ + public InputOverlay(Context context, AttributeSet attrs) + { + super(context, attrs); + + // Force draw + setWillNotDraw(false); + + // Request focus for the overlay so it has priority on presses. + requestFocus(); + } + + @Override + public boolean onTouch(View v, MotionEvent event) + { + switch (event.getAction()) + { + case MotionEvent.ACTION_DOWN: + { + // TODO: Handle down presses. + return true; + } + } + + return false; + } + + @Override + public void onDraw(Canvas canvas) + { + super.onDraw(canvas); + + for (InputOverlayItem item : overlayItems) + { + item.draw(canvas); + } + } +} diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayItem.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayItem.java new file mode 100644 index 000000000000..a45578b263f4 --- /dev/null +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayItem.java @@ -0,0 +1,107 @@ +package org.dolphinemu.dolphinemu.emulation.overlay; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; + +/** + * Represents a drawable image for the {@link InputOverlay} + */ +public final class InputOverlayItem +{ + // The image as a BitmapDrawable + private BitmapDrawable drawable; + + // Width and height of the underlying image. + private int width; + private int height; + + // X and Y coordinates to display this item at. + private int x; + private int y; + + // Image scale factor. + private float scaleFactor = 1.0f; + + // Rectangle that we draw this item to. + private Rect drawRect; + + /** + * Constructor. + * + * @param res Reference to the app resources for fetching display metrics. + * @param resId Resource ID of the {@link BitmapDrawable} to encapsulate. + */ + public InputOverlayItem(Resources res, int resId) + { + // Idiot-proof the constructor. + if (res == null) + throw new IllegalArgumentException("res cannot be null"); + + // Everything is valid, decode the filename as a bitmap. + drawable = (BitmapDrawable) res.getDrawable(resId); + Bitmap image = drawable.getBitmap(); + + // Set width/height + width = image.getWidth(); + height = image.getHeight(); + + // Initialize rectangle to zero width, height, x, and y. + drawRect = new Rect(); + } + + /** + * Constructor + * + * @param res Reference to the app resources for fetching display metrics. + * @param resId Resource ID of the {@link BitmapDrawable} to encapsulate. + * @param x X coordinate on the screen to place the control. + * @param y Y coordinate on the screen to place the control. + */ + public InputOverlayItem(Resources res, int resId, int x, int y) + { + this(res, resId); + + setPosition(x, y); + } + + /** + * Sets the position of this item on the screen. + * + * @param x New x-coordinate for this image. + * @param y New y-coordinate for this image. + */ + public void setPosition(int x, int y) + { + this.x = x; + this.y = y; + + drawRect.set(x, y, x + (int)(width * scaleFactor), y + (int)(height * scaleFactor)); + drawable.setBounds(drawRect); + } + + /** + * Sets a new scaling factor for the current image. + * + * @param scaleFactor The new scaling factor. Note that 1.0 is normal size. + */ + public void setScaleFactor(float scaleFactor) + { + this.scaleFactor = scaleFactor; + + // Adjust for the new scale factor. + setPosition(x, y); + } + + /** + * Draws this item to a given canvas. + * + * @param canvas The canvas to draw this item to. + */ + public void draw(Canvas canvas) + { + drawable.draw(canvas); + } +} diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/PrefsActivity.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/PrefsActivity.java index 51153866e15b..5368475319be 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/PrefsActivity.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/PrefsActivity.java @@ -7,6 +7,9 @@ package org.dolphinemu.dolphinemu.settings; import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.settings.cpu.CPUSettingsFragment; +import org.dolphinemu.dolphinemu.settings.input.InputConfigFragment; +import org.dolphinemu.dolphinemu.settings.video.VideoSettingsFragment; import android.app.ActionBar; import android.app.ActionBar.Tab; diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/CPUSettingsFragment.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/cpu/CPUSettingsFragment.java similarity index 96% rename from Source/Android/src/org/dolphinemu/dolphinemu/settings/CPUSettingsFragment.java rename to Source/Android/src/org/dolphinemu/dolphinemu/settings/cpu/CPUSettingsFragment.java index 47f0cb7d3599..77caec2b83e5 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/CPUSettingsFragment.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/cpu/CPUSettingsFragment.java @@ -4,7 +4,7 @@ * Refer to the license.txt file included. */ -package org.dolphinemu.dolphinemu.settings; +package org.dolphinemu.dolphinemu.settings.cpu; import org.dolphinemu.dolphinemu.R; diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/InputConfigFragment.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java similarity index 95% rename from Source/Android/src/org/dolphinemu/dolphinemu/settings/InputConfigFragment.java rename to Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java index a23f1b4a3f11..2ada44ccff2f 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/InputConfigFragment.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java @@ -4,12 +4,13 @@ * Refer to the license.txt file included. */ -package org.dolphinemu.dolphinemu.settings; +package org.dolphinemu.dolphinemu.settings.input; import android.app.AlertDialog; import android.app.Fragment; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.preference.Preference; @@ -115,9 +116,17 @@ public void onClick(DialogInterface dialog, int which) // Everything is set, show the dialog. dialog.show(); + return true; + } + + if (pref.getKey().equals("inputOverlayConfigPref")) + { + Intent inputOverlayConfig = new Intent(getActivity(), InputOverlayConfigActivity.class); + startActivity(inputOverlayConfig); + return true; } - return true; + return false; } /** diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigActivity.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigActivity.java new file mode 100644 index 000000000000..3084c181d125 --- /dev/null +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigActivity.java @@ -0,0 +1,27 @@ +/** + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2 + * Refer to the license.txt file included. + */ + +package org.dolphinemu.dolphinemu.settings.input; + +import org.dolphinemu.dolphinemu.R; + +import android.app.Activity; +import android.os.Bundle; + +/** + * {@link Activity} used for configuring the input overlay. + */ +public final class InputOverlayConfigActivity extends Activity +{ + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + // Set the initial layout. + setContentView(R.layout.input_overlay_config_layout); + } +} diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigButton.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigButton.java new file mode 100644 index 000000000000..cca596e85cd1 --- /dev/null +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigButton.java @@ -0,0 +1,52 @@ +/** + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2 + * Refer to the license.txt file included. + */ + +package org.dolphinemu.dolphinemu.settings.input; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.Button; + +/** + * A movable {@link Button} for use within the + * input overlay configuration screen. + */ +public final class InputOverlayConfigButton extends Button implements OnTouchListener +{ + /** + * Constructor + * + * @param context The current {@link Context}. + * @param attribs {@link AttributeSet} for parsing XML attributes. + */ + public InputOverlayConfigButton(Context context, AttributeSet attribs) + { + super(context, attribs); + + // Set the button as its own OnTouchListener. + setOnTouchListener(this); + } + + @Override + public boolean onTouch(View v, MotionEvent event) + { + switch(event.getAction()) + { + // Only change the X/Y coordinates when we move the button. + case MotionEvent.ACTION_MOVE: + { + setX(getX() + event.getX()); + setY(getY() + event.getY()); + return true; + } + } + + return false; + } +} diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/VideoSettingsFragment.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/video/VideoSettingsFragment.java similarity index 99% rename from Source/Android/src/org/dolphinemu/dolphinemu/settings/VideoSettingsFragment.java rename to Source/Android/src/org/dolphinemu/dolphinemu/settings/video/VideoSettingsFragment.java index fffea888a784..47f9ef475f9b 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/VideoSettingsFragment.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/video/VideoSettingsFragment.java @@ -4,7 +4,7 @@ * Refer to the license.txt file included. */ -package org.dolphinemu.dolphinemu.settings; +package org.dolphinemu.dolphinemu.settings.video; import android.app.Activity; import android.content.SharedPreferences; From 88c797a9c0f752215ca324e9b5e457b27c7beaa1 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Fri, 25 Oct 2013 17:39:23 -0400 Subject: [PATCH 045/202] [Android] Slight cleanup for previous commit. Mostly UI adjustments. Makes the buttons appear uniformly on the overlay config activity. --- Source/Android/res/layout/emulation_view.xml | 2 +- .../res/layout/input_overlay_config_layout.xml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/Android/res/layout/emulation_view.xml b/Source/Android/res/layout/emulation_view.xml index c4648b577b2a..1d0472d52734 100644 --- a/Source/Android/res/layout/emulation_view.xml +++ b/Source/Android/res/layout/emulation_view.xml @@ -11,7 +11,7 @@ android:focusable="false" android:focusableInTouchMode="false"/> - + + android:layout_toRightOf="@+id/buttonA" + android:background="@drawable/button_b" /> + android:layout_centerHorizontal="true" + android:layout_toRightOf="@+id/buttonB" + android:background="@drawable/button_start" /> \ No newline at end of file From c8cf71c913bbd06f3e52a7fe66b01141976c7e34 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Fri, 25 Oct 2013 20:34:38 -0400 Subject: [PATCH 046/202] [Android] The emulation overlay now sets the button positions based upon the locations chosen in the input overlay configuration settings. Documented the hell out of how the initialization of the Drawables works inside InputOverlay.java. Also made the use of InputOverlayItem.java obsolete. So this is now removed. --- .../layout/input_overlay_config_layout.xml | 10 +- .../emulation/overlay/InputOverlay.java | 80 ++++++++++++- .../emulation/overlay/InputOverlayItem.java | 107 ------------------ .../input/InputOverlayConfigButton.java | 22 ++++ 4 files changed, 105 insertions(+), 114 deletions(-) delete mode 100644 Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayItem.java diff --git a/Source/Android/res/layout/input_overlay_config_layout.xml b/Source/Android/res/layout/input_overlay_config_layout.xml index 29b4f268b8f5..fc4b1bcf158d 100644 --- a/Source/Android/res/layout/input_overlay_config_layout.xml +++ b/Source/Android/res/layout/input_overlay_config_layout.xml @@ -5,7 +5,7 @@ android:layout_height="fill_parent" > \ No newline at end of file diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java index c2272fdc5f6f..d681a3c85cc0 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java @@ -3,8 +3,14 @@ import java.util.HashSet; import java.util.Set; +import org.dolphinemu.dolphinemu.R; + import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.preference.PreferenceManager; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.SurfaceView; @@ -17,7 +23,7 @@ */ public final class InputOverlay extends SurfaceView implements OnTouchListener { - private final Set overlayItems = new HashSet(); + private final Set overlayItems = new HashSet(); /** * Constructor @@ -29,6 +35,11 @@ public InputOverlay(Context context, AttributeSet attrs) { super(context, attrs); + // Add all the overlay items to the HashSet. + overlayItems.add(initializeOverlayDrawable(context, R.drawable.button_a)); + overlayItems.add(initializeOverlayDrawable(context, R.drawable.button_b)); + overlayItems.add(initializeOverlayDrawable(context, R.drawable.button_start)); + // Force draw setWillNotDraw(false); @@ -56,9 +67,74 @@ public void onDraw(Canvas canvas) { super.onDraw(canvas); - for (InputOverlayItem item : overlayItems) + // Draw all overlay items. + for (BitmapDrawable item : overlayItems) { item.draw(canvas); } } + + /** + * Initializes a drawable, given by resId, with all of the + * parameters set for it to be properly shown on the InputOverlay. + *

+ * This works due to the way the X and Y coordinates are stored within + * the {@link SharedPreferences}. + *

+ * In the input overlay configuration menu, + * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay). + * the X and Y coordinates of the button at the END of its touch event + * (when you remove your finger/stylus from the touchscreen) are then stored + * within a SharedPreferences instance so that those values can be retrieved here. + *

+ * This has a few benefits over the conventional way of storing the values + * (ie. within the Dolphin ini file). + *

    + *
  • No native calls
  • + *
  • Keeps Android-only values inside the Android environment
  • + *
+ *

+ * Technically no modifications should need to be performed on the returned + * BitmapDrawable. Simply add it to the HashSet of overlay items and wait + * for Android to call the onDraw method. + * + * @param context The current {@link Context}. + * @param resId The resource ID of the {@link BitmapDrawable} to get. + * + * @return A {@link BitmapDrawable} with the correct drawing bounds set. + * + */ + private static BitmapDrawable initializeOverlayDrawable(Context context, int resId) + { + // Resources handle for fetching the drawable, etc. + final Resources res = context.getResources(); + + // SharedPreference to retrieve the X and Y coordinates for the drawable. + final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); + + // Get the desired drawable. + BitmapDrawable drawable = (BitmapDrawable) res.getDrawable(resId); + + // String ID of the drawable. This is what is passed into SharedPreferences + // to check whether or not a value has been set. + String drawableId = res.getResourceEntryName(resId); + + // The X and Y coordinates of the drawable on the InputOverlay. + // These were set in the input overlay configuration menu. + int drawableX = (int) sPrefs.getFloat(drawableId+"-X", 0f); + int drawableY = (int) sPrefs.getFloat(drawableId+"-Y", 0f); + + // Intrinsic width and height of the drawable. + // For any who may not know, intrinsic width/height + // are the original unmodified width and height of the image. + int intrinWidth = drawable.getIntrinsicWidth(); + int intrinHeight = drawable.getIntrinsicHeight(); + + // Now set the bounds for the drawable. + // This will dictate where on the screen (and the what the size) of the drawable will be. + drawable.setBounds(drawableX, drawableY, drawableX+intrinWidth, drawableY+intrinHeight); + + return drawable; + } + } diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayItem.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayItem.java deleted file mode 100644 index a45578b263f4..000000000000 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayItem.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.dolphinemu.dolphinemu.emulation.overlay; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; - -/** - * Represents a drawable image for the {@link InputOverlay} - */ -public final class InputOverlayItem -{ - // The image as a BitmapDrawable - private BitmapDrawable drawable; - - // Width and height of the underlying image. - private int width; - private int height; - - // X and Y coordinates to display this item at. - private int x; - private int y; - - // Image scale factor. - private float scaleFactor = 1.0f; - - // Rectangle that we draw this item to. - private Rect drawRect; - - /** - * Constructor. - * - * @param res Reference to the app resources for fetching display metrics. - * @param resId Resource ID of the {@link BitmapDrawable} to encapsulate. - */ - public InputOverlayItem(Resources res, int resId) - { - // Idiot-proof the constructor. - if (res == null) - throw new IllegalArgumentException("res cannot be null"); - - // Everything is valid, decode the filename as a bitmap. - drawable = (BitmapDrawable) res.getDrawable(resId); - Bitmap image = drawable.getBitmap(); - - // Set width/height - width = image.getWidth(); - height = image.getHeight(); - - // Initialize rectangle to zero width, height, x, and y. - drawRect = new Rect(); - } - - /** - * Constructor - * - * @param res Reference to the app resources for fetching display metrics. - * @param resId Resource ID of the {@link BitmapDrawable} to encapsulate. - * @param x X coordinate on the screen to place the control. - * @param y Y coordinate on the screen to place the control. - */ - public InputOverlayItem(Resources res, int resId, int x, int y) - { - this(res, resId); - - setPosition(x, y); - } - - /** - * Sets the position of this item on the screen. - * - * @param x New x-coordinate for this image. - * @param y New y-coordinate for this image. - */ - public void setPosition(int x, int y) - { - this.x = x; - this.y = y; - - drawRect.set(x, y, x + (int)(width * scaleFactor), y + (int)(height * scaleFactor)); - drawable.setBounds(drawRect); - } - - /** - * Sets a new scaling factor for the current image. - * - * @param scaleFactor The new scaling factor. Note that 1.0 is normal size. - */ - public void setScaleFactor(float scaleFactor) - { - this.scaleFactor = scaleFactor; - - // Adjust for the new scale factor. - setPosition(x, y); - } - - /** - * Draws this item to a given canvas. - * - * @param canvas The canvas to draw this item to. - */ - public void draw(Canvas canvas) - { - drawable.draw(canvas); - } -} diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigButton.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigButton.java index cca596e85cd1..39ac957234ee 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigButton.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigButton.java @@ -7,6 +7,8 @@ package org.dolphinemu.dolphinemu.settings.input; import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -19,6 +21,9 @@ */ public final class InputOverlayConfigButton extends Button implements OnTouchListener { + // SharedPreferences instance that the button positions are cached to. + private final SharedPreferences sharedPrefs; + /** * Constructor * @@ -31,6 +36,9 @@ public InputOverlayConfigButton(Context context, AttributeSet attribs) // Set the button as its own OnTouchListener. setOnTouchListener(this); + + // Get the SharedPreferences instance. + sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); } @Override @@ -45,6 +53,20 @@ public boolean onTouch(View v, MotionEvent event) setY(getY() + event.getY()); return true; } + + // Whenever the press event has ended + // is when we save all of the information. + case MotionEvent.ACTION_UP: + { + // String ID of this button. + String buttonId = getResources().getResourceEntryName(getId()); + + // Add the current X and Y positions of this button into SharedPreferences. + SharedPreferences.Editor editor = sharedPrefs.edit(); + editor.putFloat(buttonId+"-X", getX()); + editor.putFloat(buttonId+"-Y", getY()); + editor.commit(); + } } return false; From c1ed54832c69d2ab4fdc877b9cba74e2a07d673b Mon Sep 17 00:00:00 2001 From: Lioncash Date: Fri, 25 Oct 2013 21:05:42 -0400 Subject: [PATCH 047/202] [Android] Maintain the chosen button layout when returning to the input overlay configuration menu. - Also make the overlay configuration screen fullscreen for the app. - Also force the overlay activity to be landscape, since this is the only orientation the EmulationActivity supports. --- Source/Android/AndroidManifest.xml | 12 ++++++++---- .../input/InputOverlayConfigButton.java | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Source/Android/AndroidManifest.xml b/Source/Android/AndroidManifest.xml index 5b2a3cfee9b7..5ecd07f5de0c 100644 --- a/Source/Android/AndroidManifest.xml +++ b/Source/Android/AndroidManifest.xml @@ -33,12 +33,16 @@ - + - + - + Date: Fri, 25 Oct 2013 21:14:11 -0400 Subject: [PATCH 048/202] [Android] Prevent automatic alignments within the overlay configuration settings. Let the user do this on their own. --- .../res/layout/input_overlay_config_layout.xml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Source/Android/res/layout/input_overlay_config_layout.xml b/Source/Android/res/layout/input_overlay_config_layout.xml index fc4b1bcf158d..29b7e578a65a 100644 --- a/Source/Android/res/layout/input_overlay_config_layout.xml +++ b/Source/Android/res/layout/input_overlay_config_layout.xml @@ -8,15 +8,20 @@ android:id="@+id/button_a" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:layout_alignParentTop="true" + android:layout_alignParentLeft="false" + android:layout_alignParentTop="false" + android:layout_centerHorizontal="false" + android:layout_centerVertical="false" android:background="@drawable/button_a" /> @@ -24,8 +29,10 @@ android:id="@+id/button_start" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentTop="true" - android:layout_centerHorizontal="true" + android:layout_alignParentLeft="false" + android:layout_alignParentTop="false" + android:layout_centerHorizontal="false" + android:layout_centerVertical="false" android:layout_toRightOf="@+id/button_b" android:background="@drawable/button_start" /> From d9be95ed9eb894216d2483fa220992fc7cdf01da Mon Sep 17 00:00:00 2001 From: Lioncash Date: Fri, 25 Oct 2013 21:29:06 -0400 Subject: [PATCH 049/202] [Android] Fix alignment issues with the overlay config settings. --- .../layout/input_overlay_config_layout.xml | 27 +++++++++++++++---- .../input/InputOverlayConfigButton.java | 4 +-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Source/Android/res/layout/input_overlay_config_layout.xml b/Source/Android/res/layout/input_overlay_config_layout.xml index 29b7e578a65a..f1261d3c3523 100644 --- a/Source/Android/res/layout/input_overlay_config_layout.xml +++ b/Source/Android/res/layout/input_overlay_config_layout.xml @@ -1,16 +1,23 @@ - + android:layout_height="fill_parent" + android:clipChildren="false" + android:clipToPadding="false" > @@ -18,22 +25,32 @@ android:id="@+id/button_b" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_alignParentBottom="false" + android:layout_alignParentEnd="false" android:layout_alignParentLeft="false" + android:layout_alignParentRight="false" + android:layout_alignParentStart="false" android:layout_alignParentTop="false" + android:layout_alignWithParentIfMissing="false" android:layout_centerHorizontal="false" + android:layout_centerInParent="false" android:layout_centerVertical="false" - android:layout_toRightOf="@+id/button_a" android:background="@drawable/button_b" /> \ No newline at end of file diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigButton.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigButton.java index 8d95800c620e..28f3bb10fc45 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigButton.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigButton.java @@ -39,7 +39,7 @@ public InputOverlayConfigButton(Context context, AttributeSet attribs) // Get the SharedPreferences instance. sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); - + // String ID of this button. final String buttonId = getResources().getResourceEntryName(getId()); @@ -49,8 +49,6 @@ public InputOverlayConfigButton(Context context, AttributeSet attribs) // If they are not -1, then they have a previous value set. // Thus, we set those coordinate values. - // TODO: This is not always correct placement. Fix this. - // Likely something to do with the backing layout being a relative layout. if (x != -1f && y != -1f) { setX(x); From 77eb9ce725dc9a3f17394318356bcde39562169e Mon Sep 17 00:00:00 2001 From: Lioncash Date: Fri, 25 Oct 2013 23:10:17 -0400 Subject: [PATCH 050/202] [Android] Add the capability to dynamically enable and disable the input overlay during emulation. --- Source/Android/res/menu/emuwindow_overlay.xml | 11 +++- Source/Android/res/values-ja/strings.xml | 2 + Source/Android/res/values/strings.xml | 2 + .../emulation/EmulationActivity.java | 65 ++++++++++++++++--- 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/Source/Android/res/menu/emuwindow_overlay.xml b/Source/Android/res/menu/emuwindow_overlay.xml index d1c480c2842f..88b50f907d9b 100644 --- a/Source/Android/res/menu/emuwindow_overlay.xml +++ b/Source/Android/res/menu/emuwindow_overlay.xml @@ -1,4 +1,12 @@

+ + + + + + diff --git a/Source/Android/res/values-ja/strings.xml b/Source/Android/res/values-ja/strings.xml index 4ecd85fd6239..bab2d74b0a99 100644 --- a/Source/Android/res/values-ja/strings.xml +++ b/Source/Android/res/values-ja/strings.xml @@ -29,6 +29,8 @@ クリックされたファイル: %1$s + 入力オーバーレイを有効 + 入力オーバーレイを無効 ステートセーブ ステートロード 終了 diff --git a/Source/Android/res/values/strings.xml b/Source/Android/res/values/strings.xml index b4943e86321d..6c26d8828c1c 100644 --- a/Source/Android/res/values/strings.xml +++ b/Source/Android/res/values/strings.xml @@ -29,6 +29,8 @@ File clicked: %1$s + Enable Input Overlay + Disable Input Overlay Save State Load State Exit diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/EmulationActivity.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/EmulationActivity.java index 8b8f4e05517e..1d8b2afe1779 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/EmulationActivity.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/EmulationActivity.java @@ -36,6 +36,7 @@ public final class EmulationActivity extends Activity private boolean IsActionBarHidden = false; private float screenWidth; private float screenHeight; + private SharedPreferences sharedPrefs; @Override public void onCreate(Bundle savedInstanceState) @@ -60,29 +61,36 @@ public void onCreate(Bundle savedInstanceState) getActionBar().setBackgroundDrawable(actionBarBackground); // Set the native rendering screen width/height. - // Also get the intent passed from the GameList when the game - // was selected. This is so the path of the game can be retrieved - // and set on the native side of the code so the emulator can actually - // load the game. - Intent gameToEmulate = getIntent(); - + // // Due to a bug in Adreno, it renders the screen rotated 90 degrees when using OpenGL // Flip the width and height when on Adreno to work around this. // Mali isn't affected by this bug. - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - if (prefs.getString("gpuPref", "Software Rendering").equals("OGL") + sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + if (sharedPrefs.getString("gpuPref", "Software Rendering").equals("OGL") && VideoSettingsFragment.SupportsGLES3() && VideoSettingsFragment.m_GLVendor != null && VideoSettingsFragment.m_GLVendor.equals("Qualcomm")) NativeLibrary.SetDimensions((int)screenHeight, (int)screenWidth); else NativeLibrary.SetDimensions((int)screenWidth, (int)screenHeight); + + // Get the intent passed from the GameList when the game + // was selected. This is so the path of the game can be retrieved + // and set on the native side of the code so the emulator can actually + // load the game. + Intent gameToEmulate = getIntent(); NativeLibrary.SetFilename(gameToEmulate.getStringExtra("SelectedGame")); Running = true; // Set the emulation window. setContentView(R.layout.emulation_view); + // If the input overlay was previously disabled, then don't show it. + if (!sharedPrefs.getBoolean("showInputOverlay", true)) + { + findViewById(R.id.emulationControlOverlay).setVisibility(View.INVISIBLE); + } + // Hide the action bar by default so it doesn't get in the way. getActionBar().hide(); IsActionBarHidden = true; @@ -168,11 +176,50 @@ public boolean onCreateOptionsMenu(Menu menu) return true; } + @Override + public boolean onPrepareOptionsMenu(Menu menu) + { + // Determine which string the "Enable Input Overlay" menu item should have + // depending on its visibility at the time of preparing the options menu. + if (!sharedPrefs.getBoolean("showInputOverlay", true)) + { + menu.findItem(R.id.enableInputOverlay).setTitle(R.string.enable_input_overlay); + } + else + { + menu.findItem(R.id.enableInputOverlay).setTitle(R.string.disable_input_overlay); + } + + return true; + } + @Override public boolean onMenuItemSelected(int itemId, MenuItem item) { switch(item.getItemId()) { + // Enable/Disable input overlay. + case R.id.enableInputOverlay: + { + View overlay = findViewById(R.id.emulationControlOverlay); + + // Show the overlay + if (item.getTitle().equals(getString(R.string.enable_input_overlay))) + { + overlay.setVisibility(View.VISIBLE); + item.setTitle(R.string.disable_input_overlay); + sharedPrefs.edit().putBoolean("showInputOverlay", true).commit(); + } + else // Hide the overlay + { + overlay.setVisibility(View.INVISIBLE); + item.setTitle(R.string.enable_input_overlay); + sharedPrefs.edit().putBoolean("showInputOverlay", false).commit(); + } + + return true; + } + // Save state slots case R.id.saveSlot1: NativeLibrary.SaveState(0); @@ -194,7 +241,7 @@ public boolean onMenuItemSelected(int itemId, MenuItem item) NativeLibrary.SaveState(4); return true; - // Load state slot + // Load state slots case R.id.loadSlot1: NativeLibrary.LoadState(0); return true; From 8d71a83ae5cecc8474f97ccf240a13868af0d5a1 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Sat, 26 Oct 2013 00:55:07 -0400 Subject: [PATCH 051/202] [Android] Fix the super-mature string that was a placeholder for the title for the overlay configuration setting. Also fix a NullPointerException possibility. Fixed it by giving the PreferenceScreen a title. --- Source/Android/res/values-ja/strings.xml | 2 ++ Source/Android/res/values/strings.xml | 2 ++ Source/Android/res/xml/input_prefs.xml | 7 +++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Source/Android/res/values-ja/strings.xml b/Source/Android/res/values-ja/strings.xml index bab2d74b0a99..ee3bb8d3e757 100644 --- a/Source/Android/res/values-ja/strings.xml +++ b/Source/Android/res/values-ja/strings.xml @@ -43,6 +43,8 @@ 入力 + 入力オーバーレイレイアウト + 入力オーバーレイのためのボタンのレイアウト。 ゲームキューブの入力バインディング 入力バインディング %1$sにバインドするための入力を移動または押してください。 diff --git a/Source/Android/res/values/strings.xml b/Source/Android/res/values/strings.xml index 6c26d8828c1c..29e5a67b30e0 100644 --- a/Source/Android/res/values/strings.xml +++ b/Source/Android/res/values/strings.xml @@ -43,6 +43,8 @@ Input + Input Overlay Layout + Button layout for the input overlay. Gamecube Input Bindings Input Binding Press or move an input to bind it to %1$s. diff --git a/Source/Android/res/xml/input_prefs.xml b/Source/Android/res/xml/input_prefs.xml index 5dc15ac75ec4..afd32b476bec 100644 --- a/Source/Android/res/xml/input_prefs.xml +++ b/Source/Android/res/xml/input_prefs.xml @@ -5,9 +5,12 @@ + android:summary="@string/input_overlay_layout_desc" + android:title="@string/input_overlay_layout"/> - + From c78d5a97517bcd49e369410a28f2a894916ad1c5 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Sat, 26 Oct 2013 00:59:53 -0400 Subject: [PATCH 052/202] [Android] Improve an if statement check in InputConfigFragment.java. Also clarify it. --- .../dolphinemu/settings/input/InputConfigFragment.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java index 2ada44ccff2f..657c5f081731 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java @@ -119,7 +119,8 @@ public void onClick(DialogInterface dialog, int which) return true; } - if (pref.getKey().equals("inputOverlayConfigPref")) + // If the user has clicked the option to configure the input overlay. + if (pref.getTitle().equals(getString(R.string.input_overlay_layout))) { Intent inputOverlayConfig = new Intent(getActivity(), InputOverlayConfigActivity.class); startActivity(inputOverlayConfig); From c3e4aa1f3586b041c11306fc3b274a0f2ddb1e20 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Sat, 26 Oct 2013 01:06:00 -0400 Subject: [PATCH 053/202] [Android] Add the copyright header to some Java source files that were lacking them. --- .../dolphinemu/emulation/overlay/InputOverlay.java | 6 ++++++ .../dolphinemu/settings/custom/UpdatingListPreference.java | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java index d681a3c85cc0..5053a03edc50 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java @@ -1,3 +1,9 @@ +/** + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2 + * Refer to the license.txt file included. + */ + package org.dolphinemu.dolphinemu.emulation.overlay; import java.util.HashSet; diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/custom/UpdatingListPreference.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/custom/UpdatingListPreference.java index 7b6fa734ca37..273b5fe5479b 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/custom/UpdatingListPreference.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/custom/UpdatingListPreference.java @@ -1,3 +1,9 @@ +/** + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2 + * Refer to the license.txt file included. + */ + package org.dolphinemu.dolphinemu.settings.custom; import android.content.Context; From c24dfe559bf78c42acb11b22685c9da8c9416117 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Sat, 26 Oct 2013 05:36:20 -0500 Subject: [PATCH 054/202] [Android] Change how the onTouchEvent native method works. Just pass in the correct Button ID and Action and it'll be pressed or not. Not actually rigged up to the Java code yet. Doesn't support anything with an Axis yet so no C stick, main stick, L and R triggers --- .../dolphinemu/dolphinemu/NativeLibrary.java | 7 ++- .../emulation/EmulationActivity.java | 16 ------ .../DolphinWX/Src/Android/ButtonManager.cpp | 53 +++++++------------ .../DolphinWX/Src/Android/ButtonManager.h | 25 +-------- Source/Core/DolphinWX/Src/MainAndroid.cpp | 4 +- 5 files changed, 25 insertions(+), 80 deletions(-) diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java index 80fcf64ff6fe..dd4693b347e5 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java @@ -17,12 +17,11 @@ public final class NativeLibrary { /** * Handles touch events. - * + * + * @param Button Key code identifying which button was pressed. * @param Action Mask for the action being performed. - * @param X Location on the screen's X-axis that the touch event occurred. - * @param Y Location on the screen's Y-axis that the touch event occurred. */ - public static native void onTouchEvent(int Action, float X, float Y); + public static native void onTouchEvent(int Button, int Action); /** * Handles button press events for a gamepad. diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/EmulationActivity.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/EmulationActivity.java index 1d8b2afe1779..dff9d09b22ac 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/EmulationActivity.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/EmulationActivity.java @@ -135,22 +135,6 @@ public void onDestroy() } } - @Override - public boolean onTouchEvent(MotionEvent event) - { - float X = event.getX(); - float Y = event.getY(); - int Action = event.getActionMasked(); - - // Converts button locations 0 - 1 to OGL screen coords -1.0 - 1.0 - float ScreenX = ((X / screenWidth) * 2.0f) - 1.0f; - float ScreenY = ((Y / screenHeight) * -2.0f) + 1.0f; - - NativeLibrary.onTouchEvent(Action, ScreenX, ScreenY); - - return false; - } - @Override public void onBackPressed() { diff --git a/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp b/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp index 1aa7187075c0..a80e28f3734a 100644 --- a/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp +++ b/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp @@ -15,7 +15,7 @@ // Official SVN repository and contact information can be found at // http://code.google.com/p/dolphin-emu/ -#include +#include #include "GLInterface.h" #include "Android/TextureLoader.h" #include "Android/ButtonManager.h" @@ -24,16 +24,8 @@ extern void DrawButton(GLuint tex, float *coords); namespace ButtonManager { - std::vector m_buttons; - std::map m_controllers; - // XXX: This needs to not be here so we can load the locations from file - // This will allow customizable button locations in the future - // These are the OpenGL on screen coordinates - float m_coords[][8] = { // X, Y, X, EY, EX, EY, EX, Y - {0.75f, -1.0f, 0.75f, -0.75f, 1.0f, -0.75f, 1.0f, -1.0f}, // A - {0.50f, -1.0f, 0.50f, -0.75f, 0.75f, -0.75f, 0.75f, -1.0f}, // B - {-0.10f, -1.0f, -0.10f, -0.80f, 0.10f, -0.80f, 0.10f, -1.0f}, // Start - }; + std::unordered_map m_buttons; + std::unordered_map m_controllers; const char *configStrings[] = { "InputA", "InputB", "InputStart", @@ -71,9 +63,17 @@ namespace ButtonManager void Init() { // Initialize our touchscreen buttons - m_buttons.push_back(new Button("ButtonA.png", BUTTON_A, m_coords[0])); - m_buttons.push_back(new Button("ButtonB.png", BUTTON_B, m_coords[1])); - m_buttons.push_back(new Button("ButtonStart.png", BUTTON_START, m_coords[2])); + m_buttons[BUTTON_A] = new Button(); + m_buttons[BUTTON_B] = new Button(); + m_buttons[BUTTON_START] = new Button(); + m_buttons[BUTTON_X] = new Button(); + m_buttons[BUTTON_Y] = new Button(); + m_buttons[BUTTON_Z] = new Button(); + m_buttons[BUTTON_UP] = new Button(); + m_buttons[BUTTON_DOWN] = new Button(); + m_buttons[BUTTON_LEFT] = new Button(); + m_buttons[BUTTON_RIGHT] = new Button(); + // Init our controller bindings IniFile ini; @@ -109,9 +109,7 @@ namespace ButtonManager bool GetButtonPressed(ButtonType button) { bool pressed = false; - for (auto it = m_buttons.begin(); it != m_buttons.end(); ++it) - if ((*it)->GetButtonType() == button) - pressed = (*it)->Pressed(); + pressed = m_buttons[button]->Pressed(); for (auto it = m_controllers.begin(); it != m_controllers.end(); ++it) pressed |= it->second->ButtonValue(button); @@ -125,28 +123,13 @@ namespace ButtonManager return 0.0f; return it->second->AxisValue(axis); } - void TouchEvent(int action, float x, float y) + void TouchEvent(int button, int action) { // Actions // 0 is press // 1 is let go // 2 is move - for (auto it = m_buttons.begin(); it != m_buttons.end(); ++it) - { - float *coords = (*it)->GetCoords(); - if ( x >= coords[0] && - x <= coords[4] && - y >= coords[1] && - y <= coords[3]) - { - if (action == 0) - (*it)->SetState(BUTTON_PRESSED); - if (action == 1) - (*it)->SetState(BUTTON_RELEASED); - if (action == 2) - ; // XXX: Be used later for analog stick - } - } + m_buttons[button]->SetState(action ? BUTTON_RELEASED : BUTTON_PRESSED); } void GamepadEvent(std::string dev, int button, int action) @@ -174,7 +157,7 @@ namespace ButtonManager void Shutdown() { for(auto it = m_buttons.begin(); it != m_buttons.end(); ++it) - delete *it; + delete it->second; for (auto it = m_controllers.begin(); it != m_controllers.end(); ++it) delete it->second; m_controllers.clear(); diff --git a/Source/Core/DolphinWX/Src/Android/ButtonManager.h b/Source/Core/DolphinWX/Src/Android/ButtonManager.h index 39b828efb75e..4b284f83d0c5 100644 --- a/Source/Core/DolphinWX/Src/Android/ButtonManager.h +++ b/Source/Core/DolphinWX/Src/Android/ButtonManager.h @@ -61,32 +61,11 @@ namespace ButtonManager class Button { private: - int m_tex; - ButtonType m_button; ButtonState m_state; - float m_coords[8]; public: - Button(std::string filename, ButtonType button, float *coords) - { - u32 width, height; - char *image; - // image = LoadPNG((std::string(DOLPHIN_DATA_DIR "/") + filename).c_str(), width, height); - // XXX: Make platform specific drawing - - m_button = button; - memcpy(m_coords, coords, sizeof(float) * 8); - m_state = BUTTON_RELEASED; - } - Button(ButtonType button) - { - m_button = button; - m_state = BUTTON_RELEASED; - } + Button() : m_state(BUTTON_RELEASED) {} void SetState(ButtonState state) { m_state = state; } bool Pressed() { return m_state == BUTTON_PRESSED; } - ButtonType GetButtonType() { return m_button; } - GLuint GetTexture() { return m_tex; } - float *GetCoords() { return m_coords; } ~Button() { } }; @@ -131,7 +110,7 @@ namespace ButtonManager void DrawButtons(); bool GetButtonPressed(ButtonType button); float GetAxisValue(ButtonType axis); - void TouchEvent(int action, float x, float y); + void TouchEvent(int button, int action); void GamepadEvent(std::string dev, int button, int action); void GamepadAxisEvent(std::string dev, int axis, float value); void Shutdown(); diff --git a/Source/Core/DolphinWX/Src/MainAndroid.cpp b/Source/Core/DolphinWX/Src/MainAndroid.cpp index 5b8699bb0ded..3c0c80871ab1 100644 --- a/Source/Core/DolphinWX/Src/MainAndroid.cpp +++ b/Source/Core/DolphinWX/Src/MainAndroid.cpp @@ -237,9 +237,9 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_StopEmulatio Core::Stop(); updateMainFrameEvent.Set(); // Kick the waiting event } -JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onTouchEvent(JNIEnv *env, jobject obj, jint Action, jfloat X, jfloat Y) +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onTouchEvent(JNIEnv *env, jobject obj, jint Button, jint Action) { - ButtonManager::TouchEvent(Action, X, Y); + ButtonManager::TouchEvent(Button, Action); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onGamePadEvent(JNIEnv *env, jobject obj, jstring jDevice, jint Button, jint Action) { From d8c119e5f5f648dda1d60606481cc56a517e2615 Mon Sep 17 00:00:00 2001 From: comex Date: Mon, 21 Oct 2013 17:41:21 -0400 Subject: [PATCH 055/202] Initial implementation of IOSync, a replacement for the ad-hoc broken movie/netplay syncing crap. Breaks netplay for now. Improved version that actually works. --- Source/Core/Core/CMakeLists.txt | 2 + Source/Core/Core/Core.vcxproj | 4 + Source/Core/Core/Core.vcxproj.filters | 6 +- Source/Core/Core/Src/HW/EXI_DeviceIPL.cpp | 17 +- Source/Core/Core/Src/HW/EXI_DeviceIPL.h | 1 - Source/Core/Core/Src/HW/HW.cpp | 5 + Source/Core/Core/Src/HW/SI.cpp | 131 ++++++++------ Source/Core/Core/Src/HW/SI_Device.cpp | 2 + Source/Core/Core/Src/HW/SI_Device.h | 27 ++- .../Core/Core/Src/HW/SI_DeviceAMBaseboard.cpp | 26 ++- .../Core/Core/Src/HW/SI_DeviceAMBaseboard.h | 18 ++ Source/Core/Core/Src/HW/SI_DeviceGBA.cpp | 4 + .../Core/Src/HW/SI_DeviceGCController.cpp | 50 ++---- .../Core/Core/Src/HW/SI_DeviceGCController.h | 12 +- .../Core/Src/HW/SI_DeviceGCSteeringWheel.cpp | 7 +- .../Core/Src/HW/WiimoteEmu/WiimoteEmu.cpp | 10 -- Source/Core/Core/Src/IOSync.cpp | 68 ++++++++ Source/Core/Core/Src/IOSync.h | 165 ++++++++++++++++++ Source/Core/Core/Src/IOSyncBackends.cpp | 61 +++++++ Source/Core/Core/Src/IOSyncBackends.h | 28 +++ Source/Core/Core/Src/NetPlayClient.cpp | 60 +++---- Source/Core/Core/Src/NetPlayClient.h | 22 +-- Source/Core/Core/Src/NetPlayProto.h | 6 +- Source/Core/Core/Src/NetPlayServer.cpp | 14 ++ Source/Core/Core/Src/NetPlayServer.h | 6 + Source/Core/DolphinWX/Src/NetWindow.cpp | 2 + 26 files changed, 556 insertions(+), 198 deletions(-) create mode 100644 Source/Core/Core/Src/IOSync.cpp create mode 100644 Source/Core/Core/Src/IOSync.h create mode 100644 Source/Core/Core/Src/IOSyncBackends.cpp create mode 100644 Source/Core/Core/Src/IOSyncBackends.h diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 07a6fc2aca5e..c8d3002d30b0 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -10,6 +10,8 @@ set(SRCS Src/ActionReplay.cpp Src/ec_wii.cpp Src/GeckoCodeConfig.cpp Src/GeckoCode.cpp + Src/IOSync.cpp + Src/IOSyncBackends.cpp Src/Movie.cpp Src/NetPlayClient.cpp Src/NetPlayServer.cpp diff --git a/Source/Core/Core/Core.vcxproj b/Source/Core/Core/Core.vcxproj index 70fd1932e7ae..b759b0da3069 100644 --- a/Source/Core/Core/Core.vcxproj +++ b/Source/Core/Core/Core.vcxproj @@ -321,6 +321,8 @@ + + @@ -527,6 +529,8 @@ + + diff --git a/Source/Core/Core/Core.vcxproj.filters b/Source/Core/Core/Core.vcxproj.filters index 148b88fa918b..1bbc24d51d71 100644 --- a/Source/Core/Core/Core.vcxproj.filters +++ b/Source/Core/Core/Core.vcxproj.filters @@ -295,6 +295,8 @@ HW %28Flipper/Hollywood%29\VI - Video Interface + + PowerPC\Interpreter @@ -837,6 +839,8 @@ HW %28Flipper/Hollywood%29\VI - Video Interface + + PowerPC\Interpreter @@ -1215,4 +1219,4 @@ {658f0a72-5b08-4241-9c01-31cdc4ab7057} - \ No newline at end of file + diff --git a/Source/Core/Core/Src/HW/EXI_DeviceIPL.cpp b/Source/Core/Core/Src/HW/EXI_DeviceIPL.cpp index 9402b7bbaba5..0a4a6be2983a 100644 --- a/Source/Core/Core/Src/HW/EXI_DeviceIPL.cpp +++ b/Source/Core/Core/Src/HW/EXI_DeviceIPL.cpp @@ -349,24 +349,9 @@ void CEXIIPL::TransferByte(u8& _uByte) u32 CEXIIPL::GetGCTime() { - u64 ltime = 0; static const u32 cJanuary2000 = 0x386D4380; // Seconds between 1.1.1970 and 1.1.2000 - if (Movie::IsRecordingInput() || Movie::IsPlayingInput()) - { - ltime = Movie::GetRecordingStartTime(); - - // let's keep time moving forward, regardless of what it starts at - ltime += CoreTiming::GetTicks() / SystemTimers::GetTicksPerSecond(); - } - else - { - // hack in some netplay stuff - ltime = NetPlay_GetGCTime(); - - if (0 == ltime) - ltime = Common::Timer::GetLocalTimeSinceJan1970(); - } + u32 ltime = IOSync::g_Backend->GetTime(); return ((u32)ltime - cJanuary2000); diff --git a/Source/Core/Core/Src/HW/EXI_DeviceIPL.h b/Source/Core/Core/Src/HW/EXI_DeviceIPL.h index 3198d0b1bd55..a485a6c6bb18 100644 --- a/Source/Core/Core/Src/HW/EXI_DeviceIPL.h +++ b/Source/Core/Core/Src/HW/EXI_DeviceIPL.h @@ -19,7 +19,6 @@ class CEXIIPL : public IEXIDevice void DoState(PointerWrap &p); static u32 GetGCTime(); - static u32 NetPlay_GetGCTime(); static void Descrambler(u8* data, u32 size); diff --git a/Source/Core/Core/Src/HW/HW.cpp b/Source/Core/Core/Src/HW/HW.cpp index 586344ecc604..e23588eddfb9 100644 --- a/Source/Core/Core/Src/HW/HW.cpp +++ b/Source/Core/Core/Src/HW/HW.cpp @@ -32,6 +32,7 @@ namespace HW SystemTimers::PreInit(); State::Init(); + IOSync::Init(); // Init the whole Hardware AudioInterface::Init(); @@ -103,6 +104,10 @@ namespace HW p.DoMarker("WII_IPC_HLE_Interface"); } + // must be after SI + p.DoMarker("IOSync"); + IOSync::DoState(p); + p.DoMarker("WIIHW"); } } diff --git a/Source/Core/Core/Src/HW/SI.cpp b/Source/Core/Core/Src/HW/SI.cpp index 76810b7f6d5c..2eed984068ed 100644 --- a/Source/Core/Core/Src/HW/SI.cpp +++ b/Source/Core/Core/Src/HW/SI.cpp @@ -6,8 +6,6 @@ #include "ChunkFile.h" #include "../ConfigManager.h" #include "../CoreTiming.h" -#include "../Movie.h" -#include "../NetPlayProto.h" #include "SystemTimers.h" #include "ProcessorInterface.h" @@ -18,12 +16,6 @@ namespace SerialInterface { - -static int changeDevice; - -void RunSIBuffer(); -void UpdateInterrupts(); - // SI Interrupt Types enum SIInterruptType { @@ -106,7 +98,7 @@ struct SSIChannel USIChannelOut m_Out; USIChannelIn_Hi m_InHi; USIChannelIn_Lo m_InLo; - ISIDevice* m_pDevice; + std::unique_ptr m_pDevice; }; // SI Poll: Controls how often a device is polled @@ -215,6 +207,32 @@ static USIStatusReg g_StatusReg; static USIEXIClockCount g_EXIClockCount; static u8 g_SIBuffer[128]; +static int changeDevice[4]; +} + +void SIDeviceSyncerBase::OnConnected(int channel, SIDevices& subtype) +{ + CoreTiming::RemoveAllEvents(SerialInterface::changeDevice[channel]); + auto& sidev = SerialInterface::g_Channel[channel].m_pDevice; + if (!sidev || subtype != sidev->GetDeviceType()) + { + CoreTiming::ScheduleEvent(0, SerialInterface::changeDevice[channel], ((u64)channel << 32) | SIDEVICE_NONE); + CoreTiming::ScheduleEvent(50000000, SerialInterface::changeDevice[channel], ((u64)channel << 32) | subtype); + } +} + +void SIDeviceSyncerBase::OnDisconnected(int channel) +{ + CoreTiming::RemoveAllEvents(SerialInterface::changeDevice[channel]); + CoreTiming::ScheduleEvent(0, SerialInterface::changeDevice[channel], ((u64)channel << 32) | SIDEVICE_NONE); +} + +namespace SerialInterface +{ + +void RunSIBuffer(); +void UpdateInterrupts(); + void DoState(PointerWrap &p) { for(int i = 0; i < NUMBER_OF_CHANNELS; i++) @@ -222,27 +240,16 @@ void DoState(PointerWrap &p) p.Do(g_Channel[i].m_InHi.Hex); p.Do(g_Channel[i].m_InLo.Hex); p.Do(g_Channel[i].m_Out.Hex); - - ISIDevice* pDevice = g_Channel[i].m_pDevice; + + ISIDevice* pDevice = g_Channel[i].m_pDevice.get(); SIDevices type = pDevice->GetDeviceType(); p.Do(type); - ISIDevice* pSaveDevice = (type == pDevice->GetDeviceType()) ? pDevice : SIDevice_Create(type, i); - pSaveDevice->DoState(p); - if(pSaveDevice != pDevice) + if (p.GetMode() == PointerWrap::MODE_READ) { - // if we had to create a temporary device, discard it if we're not loading. - // also, if no movie is active, we'll assume the user wants to keep their current devices - // instead of the ones they had when the savestate was created. - if(p.GetMode() != PointerWrap::MODE_READ || - (!Movie::IsRecordingInput() && !Movie::IsPlayingInput())) - { - delete pSaveDevice; - } - else - { - AddDevice(pSaveDevice); - } + AddDevice(type, i); + pDevice = g_Channel[i].m_pDevice.get(); } + pDevice->DoState(p); } p.Do(g_Poll); p.DoPOD(g_ComCSR); @@ -254,16 +261,21 @@ void DoState(PointerWrap &p) void Init() { + for (int i = 0; i < 4; i++) + { + char buf[64]; + sprintf(buf, "ChangeSIDevice%d", i); + changeDevice[i] = CoreTiming::RegisterEvent(strdup(buf), ChangeDeviceCallback); + } + for (int i = 0; i < NUMBER_OF_CHANNELS; i++) { g_Channel[i].m_Out.Hex = 0; g_Channel[i].m_InHi.Hex = 0; g_Channel[i].m_InLo.Hex = 0; - if (Movie::IsRecordingInput() || Movie::IsPlayingInput()) - AddDevice(Movie::IsUsingPad(i) ? (Movie::IsUsingBongo(i) ? SIDEVICE_GC_TARUKONGA : SIDEVICE_GC_CONTROLLER) : SIDEVICE_NONE, i); - else if (!NetPlay::IsNetPlayRunning()) - AddDevice(SConfig::GetInstance().m_SIDevice[i], i); + AddDevice(SIDEVICE_NONE, i); + ChangeDevice(SConfig::GetInstance().m_SIDevice[i], i); } g_Poll.Hex = 0; @@ -276,8 +288,6 @@ void Init() g_EXIClockCount.Hex = 0; //g_EXIClockCount.LOCK = 1; // Supposedly set on reset, but logs from real wii don't look like it is... memset(g_SIBuffer, 0, 128); - - changeDevice = CoreTiming::RegisterEvent("ChangeSIDevice", ChangeDeviceCallback); } void Shutdown() @@ -484,10 +494,11 @@ void Write32(const u32 _iValue, const u32 _iAddress) // send command to devices if (tmpStatus.WR) { - g_Channel[0].m_pDevice->SendCommand(g_Channel[0].m_Out.Hex, g_Poll.EN0); - g_Channel[1].m_pDevice->SendCommand(g_Channel[1].m_Out.Hex, g_Poll.EN1); - g_Channel[2].m_pDevice->SendCommand(g_Channel[2].m_Out.Hex, g_Poll.EN2); - g_Channel[3].m_pDevice->SendCommand(g_Channel[3].m_Out.Hex, g_Poll.EN3); + u32 poll[] = { g_Poll.EN0, g_Poll.EN1, g_Poll.EN2, g_Poll.EN3 }; + for (int i = 0; i < 4; i++) + { + g_Channel[i].m_pDevice->SendCommand(g_Channel[i].m_Out.Hex, poll[i]); + } g_StatusReg.WR = 0; g_StatusReg.WRST0 = 0; @@ -546,8 +557,9 @@ void GenerateSIInterrupt(SIInterruptType _SIInterrupt) void RemoveDevice(int _iDeviceNumber) { - delete g_Channel[_iDeviceNumber].m_pDevice; - g_Channel[_iDeviceNumber].m_pDevice = NULL; + auto& pDevice = g_Channel[_iDeviceNumber].m_pDevice; + if (pDevice) + pDevice.reset(); } void AddDevice(ISIDevice* pDevice) @@ -560,7 +572,7 @@ void AddDevice(ISIDevice* pDevice) RemoveDevice(_iDeviceNumber); // create the new one - g_Channel[_iDeviceNumber].m_pDevice = pDevice; + g_Channel[_iDeviceNumber].m_pDevice.reset(pDevice); } void AddDevice(const SIDevices _device, int _iDeviceNumber) @@ -597,15 +609,21 @@ void ChangeDeviceCallback(u64 userdata, int cyclesLate) void ChangeDevice(SIDevices device, int channel) { - // Called from GUI, so we need to make it thread safe. - // Let the hardware see no device for .5b cycles - CoreTiming::ScheduleEvent_Threadsafe(0, changeDevice, ((u64)channel << 32) | SIDEVICE_NONE); - CoreTiming::ScheduleEvent_Threadsafe(500000000, changeDevice, ((u64)channel << 32) | device); + g_SISyncClass.DisconnectLocalDevice(channel); + if (device != SIDEVICE_NONE) + g_SISyncClass.ConnectLocalDevice(channel, std::move(device)); } void UpdateDevices() { // Update channels and set the status bit if there's new data + for (int i = 0; i < 4; i++) + { + auto& dev = g_Channel[i].m_pDevice; + if (dev->GetLocalIndex() != -1) + dev->EnqueueLocalData(); + } + g_StatusReg.RDST0 = !!g_Channel[0].m_pDevice->GetData(g_Channel[0].m_InHi.Hex, g_Channel[0].m_InLo.Hex); g_StatusReg.RDST1 = !!g_Channel[1].m_pDevice->GetData(g_Channel[1].m_InHi.Hex, g_Channel[1].m_InLo.Hex); g_StatusReg.RDST2 = !!g_Channel[2].m_pDevice->GetData(g_Channel[2].m_InHi.Hex, g_Channel[2].m_InLo.Hex); @@ -641,23 +659,24 @@ void RunSIBuffer() int GetTicksToNextSIPoll() { - // Poll for input at regular intervals (once per frame) when playing or recording a movie - if (Movie::IsPlayingInput() || Movie::IsRecordingInput()) + // I don't understand the point of the previous code in here + // that had different rates for normal, movie, and netplay. + // None of this is nondeterministic, and the usual rate seems + // to be 120fps anyway, which was the netplay rate. + // Or why this is polling in the first place, but... -comex + + if (g_Poll.Y) { - if (Movie::IsNetPlayRecording()) - return SystemTimers::GetTicksPerSecond() / VideoInterface::TargetRefreshRate / 2; + return min(VideoInterface::GetTicksPerFrame() / g_Poll.Y, VideoInterface::GetTicksPerLine() * g_Poll.X); + } + else + { + if (g_Poll.X) + return VideoInterface::GetTicksPerLine() * g_Poll.X; else - return SystemTimers::GetTicksPerSecond() / VideoInterface::TargetRefreshRate; + return SystemTimers::GetTicksPerSecond() / 60; } - if (NetPlay::IsNetPlayRunning()) - return SystemTimers::GetTicksPerSecond() / VideoInterface::TargetRefreshRate / 2; - - if (!g_Poll.Y && g_Poll.X) - return VideoInterface::GetTicksPerLine() * g_Poll.X; - else if (!g_Poll.Y) - return SystemTimers::GetTicksPerSecond() / 60; - return min(VideoInterface::GetTicksPerFrame() / g_Poll.Y, VideoInterface::GetTicksPerLine() * g_Poll.X); } } // end of namespace SerialInterface diff --git a/Source/Core/Core/Src/HW/SI_Device.cpp b/Source/Core/Core/Src/HW/SI_Device.cpp index e9674bcab60e..1817c9ffb51e 100644 --- a/Source/Core/Core/Src/HW/SI_Device.cpp +++ b/Source/Core/Core/Src/HW/SI_Device.cpp @@ -91,3 +91,5 @@ ISIDevice* SIDevice_Create(const SIDevices device, const int port_number) break; } } + +IOSync::Class g_SISyncClass; diff --git a/Source/Core/Core/Src/HW/SI_Device.h b/Source/Core/Core/Src/HW/SI_Device.h index 62614656c773..ba74536494c6 100644 --- a/Source/Core/Core/Src/HW/SI_Device.h +++ b/Source/Core/Core/Src/HW/SI_Device.h @@ -6,6 +6,7 @@ #define _SIDEVICE_H #include "Common.h" +#include "IOSync.h" class PointerWrap; @@ -55,6 +56,16 @@ enum SIDevices SIDEVICE_AM_BASEBOARD }; +class SIDeviceSyncerBase +{ +public: + enum { ClassId = IOSync::ClassBase::ClassSI }; + typedef SIDevices SubtypeData; + void OnConnected(int index, SIDevices& subtype); + void OnDisconnected(int index); + void DoSubtypeData(SIDevices* subtype, PointerWrap& p) { p.Do(*subtype); } +}; +extern IOSync::Class g_SISyncClass; class ISIDevice { @@ -72,15 +83,18 @@ class ISIDevice // Destructor virtual ~ISIDevice() {} - // Run the SI Buffer - virtual int RunBuffer(u8* _pBuffer, int _iLength); + // Enqueue the data for a local device. Must not block. + virtual void EnqueueLocalData() {} - // Return true on new data + // Return true on new data. Might block on dequeue. virtual bool GetData(u32& _Hi, u32& _Low) = 0; - // Send a command directly (no detour per buffer) + // Send a command directly (no detour per buffer). virtual void SendCommand(u32 _Cmd, u8 _Poll) = 0; + // Run the SI Buffer. Might block on dequeue. + virtual int RunBuffer(u8* _pBuffer, int _iLength); + // Savestate support virtual void DoState(PointerWrap& p) {} @@ -94,6 +108,11 @@ class ISIDevice { return m_deviceType; } + + int GetLocalIndex() + { + return g_SISyncClass.GetLocalIndex(m_iDeviceNumber); + } }; extern ISIDevice* SIDevice_Create(const SIDevices device, const int port_number); diff --git a/Source/Core/Core/Src/HW/SI_DeviceAMBaseboard.cpp b/Source/Core/Core/Src/HW/SI_DeviceAMBaseboard.cpp index 83af23b7a72f..4cbc42d9eaaf 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceAMBaseboard.cpp +++ b/Source/Core/Core/Src/HW/SI_DeviceAMBaseboard.cpp @@ -7,7 +7,6 @@ #include "SI_Device.h" #include "SI_DeviceAMBaseboard.h" -#include "GCPadStatus.h" #include "GCPad.h" // where to put baseboard debug @@ -135,9 +134,7 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* _pBuffer, int _iLength) case 0x10: { DEBUG_LOG(AMBASEBOARDDEBUG, "GC-AM: Command 10, %02x (READ STATUS&SWITCHES)", ptr(1)); - SPADStatus PadStatus; - memset(&PadStatus, 0 ,sizeof(PadStatus)); - Pad::GetStatus(ISIDevice::m_iDeviceNumber, &PadStatus); + auto PadStatus = m_CurrentPadStatus; res[resp++] = 0x10; res[resp++] = 0x2; int d10_0 = 0xdf; @@ -300,8 +297,7 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* _pBuffer, int _iLength) msg.addData(0); // tilt for (i=0; i(ISIDevice::m_iDeviceNumber); _Low = 0; _Hi = 0x00800000; return true; } +void CSIDevice_AMBaseboard::DoState(PointerWrap& p) +{ + p.Do(m_CurrentPadStatus); +} + void CSIDevice_AMBaseboard::SendCommand(u32 _Cmd, u8 _Poll) { ERROR_LOG(SERIALINTERFACE, "Unknown direct command (0x%x)", _Cmd); diff --git a/Source/Core/Core/Src/HW/SI_DeviceAMBaseboard.h b/Source/Core/Core/Src/HW/SI_DeviceAMBaseboard.h index 8c135ca9809c..ca5ee926c455 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceAMBaseboard.h +++ b/Source/Core/Core/Src/HW/SI_DeviceAMBaseboard.h @@ -5,6 +5,10 @@ #ifndef _SIDEVICE_AMBASEBOARD_H #define _SIDEVICE_AMBASEBOARD_H +#include "SI.h" +#include "SI_Device.h" +#include "GCPadStatus.h" + // triforce (GC-AM) baseboard class CSIDevice_AMBaseboard : public ISIDevice { @@ -15,6 +19,16 @@ class CSIDevice_AMBaseboard : public ISIDevice CMD_GCAM = 0x70, }; + struct SReport : public SPADStatus + { + void DoReport(PointerWrap& p) + { + p.Do(*this); + } + }; + + SPADStatus m_CurrentPadStatus; + public: // constructor CSIDevice_AMBaseboard(SIDevices device, int _iDeviceNumber); @@ -22,11 +36,15 @@ class CSIDevice_AMBaseboard : public ISIDevice // run the SI Buffer virtual int RunBuffer(u8* _pBuffer, int _iLength); + virtual void EnqueueLocalData(); + // return true on new data virtual bool GetData(u32& _Hi, u32& _Low); // send a command directly virtual void SendCommand(u32 _Cmd, u8 _Poll); + + virtual void DoState(PointerWrap& p); }; #endif // _SIDEVICE_AMBASEBOARD_H diff --git a/Source/Core/Core/Src/HW/SI_DeviceGBA.cpp b/Source/Core/Core/Src/HW/SI_DeviceGBA.cpp index 937bf45036b1..613b6c2e93cf 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceGBA.cpp +++ b/Source/Core/Core/Src/HW/SI_DeviceGBA.cpp @@ -126,6 +126,10 @@ CSIDevice_GBA::CSIDevice_GBA(SIDevices _device, int _iDeviceNumber) : ISIDevice(_device, _iDeviceNumber) , GBASockServer() { + if (GetLocalIndex() == -1) + { + PanicAlert("GBA code is too insane for netplay. This won't work."); + } } int CSIDevice_GBA::RunBuffer(u8* _pBuffer, int _iLength) diff --git a/Source/Core/Core/Src/HW/SI_DeviceGCController.cpp b/Source/Core/Core/Src/HW/SI_DeviceGCController.cpp index c78760d175c5..ec7c1c17739a 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceGCController.cpp +++ b/Source/Core/Core/Src/HW/SI_DeviceGCController.cpp @@ -11,8 +11,6 @@ #include "GCPad.h" -#include "../Movie.h" - #include "../CoreTiming.h" #include "SystemTimers.h" #include "ProcessorInterface.h" @@ -102,6 +100,14 @@ int CSIDevice_GCController::RunBuffer(u8* _pBuffer, int _iLength) } +void CSIDevice_GCController::EnqueueLocalData() +{ + SReport PadStatus; + memset(&PadStatus, 0, sizeof(PadStatus)); + Pad::GetStatus(ISIDevice::m_iDeviceNumber, &PadStatus); + g_SISyncClass.EnqueueLocalReport(GetLocalIndex(), PadStatus); +} + // GetData // Return true on new data (max 7 Bytes and 6 bits ;) @@ -110,37 +116,7 @@ int CSIDevice_GCController::RunBuffer(u8* _pBuffer, int _iLength) // |_ ERR_STATUS (error on last GetData or SendCmd?) bool CSIDevice_GCController::GetData(u32& _Hi, u32& _Low) { - SPADStatus PadStatus; - memset(&PadStatus, 0, sizeof(PadStatus)); - - Pad::GetStatus(ISIDevice::m_iDeviceNumber, &PadStatus); - Movie::CallInputManip(&PadStatus, ISIDevice::m_iDeviceNumber); - - u32 netValues[2]; - if (NetPlay_GetInput(ISIDevice::m_iDeviceNumber, PadStatus, netValues)) - { - _Hi = netValues[0]; // first 4 bytes - _Low = netValues[1]; // last 4 bytes - return true; - } - - Movie::SetPolledDevice(); - - if(Movie::IsPlayingInput()) - { - Movie::PlayController(&PadStatus, ISIDevice::m_iDeviceNumber); - Movie::InputUpdate(); - } - else if(Movie::IsRecordingInput()) - { - Movie::RecordInput(&PadStatus, ISIDevice::m_iDeviceNumber); - Movie::InputUpdate(); - } - else - { - Movie::CheckPadStatus(&PadStatus, ISIDevice::m_iDeviceNumber); - } - + auto PadStatus = g_SISyncClass.DequeueReport(ISIDevice::m_iDeviceNumber); _Hi = MapPadStatus(PadStatus); // Low bits are packed differently per mode @@ -255,11 +231,9 @@ void CSIDevice_GCController::SendCommand(u32 _Cmd, u8 _Poll) unsigned int uType = command.Parameter1; // 0 = stop, 1 = rumble, 2 = stop hard unsigned int uStrength = command.Parameter2; - // get the correct pad number that should rumble locally when using netplay - const u8 numPAD = NetPlay_InGamePadToLocalPad(ISIDevice::m_iDeviceNumber); - - if (numPAD < 4) - Pad::Rumble(numPAD, uType, uStrength); + int li = GetLocalIndex(); + if (li != -1) + Pad::Rumble(li, uType, uStrength); if (!_Poll) { diff --git a/Source/Core/Core/Src/HW/SI_DeviceGCController.h b/Source/Core/Core/Src/HW/SI_DeviceGCController.h index 8f5d08b61054..db577ec2d57a 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceGCController.h +++ b/Source/Core/Core/Src/HW/SI_DeviceGCController.h @@ -65,6 +65,14 @@ class CSIDevice_GCController : public ISIDevice COMBO_RESET }; + struct SReport : public SPADStatus + { + void DoReport(PointerWrap& p) + { + p.Do(*this); + } + }; + // struct to compare input against // Set on connection and (standard pad only) on button combo SOrigin m_Origin; @@ -88,9 +96,7 @@ class CSIDevice_GCController : public ISIDevice // Run the SI Buffer virtual int RunBuffer(u8* _pBuffer, int _iLength); - // Send and Receive pad input from network - static bool NetPlay_GetInput(u8 numPAD, SPADStatus status, u32 *PADStatus); - static u8 NetPlay_InGamePadToLocalPad(u8 numPAD); + virtual void EnqueueLocalData(); // Return true on new data virtual bool GetData(u32& _Hi, u32& _Low); diff --git a/Source/Core/Core/Src/HW/SI_DeviceGCSteeringWheel.cpp b/Source/Core/Core/Src/HW/SI_DeviceGCSteeringWheel.cpp index c4850ad805d5..597ee98bba4f 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceGCSteeringWheel.cpp +++ b/Source/Core/Core/Src/HW/SI_DeviceGCSteeringWheel.cpp @@ -35,10 +35,9 @@ void CSIDevice_GCSteeringWheel::SendCommand(u32 _Cmd, u8 _Poll) unsigned int uType = command.Parameter2; // 06 = motor on, 04 = motor off // get the correct pad number that should rumble locally when using netplay - const u8 numPAD = NetPlay_InGamePadToLocalPad(ISIDevice::m_iDeviceNumber); - - if (numPAD < 4) - Pad::Motor(numPAD, uType, uStrength); + int li = GetLocalIndex(); + if (li != -1) + Pad::Motor(li, uType, uStrength); if (!_Poll) { diff --git a/Source/Core/Core/Src/HW/WiimoteEmu/WiimoteEmu.cpp b/Source/Core/Core/Src/HW/WiimoteEmu/WiimoteEmu.cpp index 94e7d88cd601..14b1c3a1ae01 100644 --- a/Source/Core/Core/Src/HW/WiimoteEmu/WiimoteEmu.cpp +++ b/Source/Core/Core/Src/HW/WiimoteEmu/WiimoteEmu.cpp @@ -771,16 +771,6 @@ void Wiimote::Update() } } } - if (NetPlay::IsNetPlayRunning()) - { - NetPlay_GetWiimoteData(m_index, data, rptf.size); - if (rptf.core) - m_status.buttons = *(wm_core*)(data + rptf.core); - } - if (!Movie::IsPlayingInput()) - { - Movie::CheckWiimoteStatus(m_index, data, rptf, m_reg_ir.mode); - } // don't send a data report if auto reporting is off if (false == m_reporting_auto && data[2] >= WM_REPORT_CORE) diff --git a/Source/Core/Core/Src/IOSync.cpp b/Source/Core/Core/Src/IOSync.cpp new file mode 100644 index 000000000000..c5703e1eb6d7 --- /dev/null +++ b/Source/Core/Core/Src/IOSync.cpp @@ -0,0 +1,68 @@ +#include "IOSync.h" +#include "IOSyncBackends.h" + +namespace IOSync +{ + +// This sticks around even if the backend is replaced. +static PWBuffer g_LocalSubtypes[ClassBase::NumClasses][ClassBase::MaxDeviceIndex]; +static bool g_LocalIsConnected[ClassBase::NumClasses][ClassBase::MaxDeviceIndex]; + +void Backend::ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf) +{ + g_LocalSubtypes[classId][localIndex] = std::move(buf); + g_LocalIsConnected[classId][localIndex] = true; +} + +void Backend::DisconnectLocalDevice(int classId, int localIndex) +{ + g_LocalIsConnected[classId][localIndex] = false; +} + +PWBuffer* Backend::GetLocalSubtype(int classId, int localIndex) +{ + return g_LocalIsConnected[classId][localIndex] ? + &g_LocalSubtypes[classId][localIndex] : + NULL; + +} + +ClassBase::ClassBase() +{ + for (int d = 0; d < MaxDeviceIndex; d++) + { + m_LocalToRemote[d] = -1; + m_RemoteToLocal[d] = -1; + m_IsConnected[d] = 0; + } +} + +void ClassBase::SetIndex(int localIndex, int index) +{ + int oldRemote = m_LocalToRemote[localIndex]; + if (oldRemote != -1) + m_RemoteToLocal[oldRemote] = -1; + m_LocalToRemote[localIndex] = index; + if (index != -1) + m_RemoteToLocal[index] = localIndex; +} + +void Init() +{ + g_Backend.reset(new BackendLocal()); +} + +void DoState(PointerWrap& p) +{ + for (int c = 0; c < ClassBase::NumClasses; c++) + { + g_Classes[c]->DoState(p); + } + + g_Backend->DoState(p); +} + +std::unique_ptr g_Backend; +ClassBase* g_Classes[ClassBase::NumClasses]; + +} diff --git a/Source/Core/Core/Src/IOSync.h b/Source/Core/Core/Src/IOSync.h new file mode 100644 index 000000000000..cab1630fb8f4 --- /dev/null +++ b/Source/Core/Core/Src/IOSync.h @@ -0,0 +1,165 @@ +// Copyright 2013 Dolphin Emulator Project +// Licensed under GPLv2 +// Refer to the license.txt file included. + +#pragma once + +#include "Common.h" +#include "ChunkFile.h" +#include + +namespace IOSync +{ + +class Backend +{ +public: + virtual void ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf); + virtual void DisconnectLocalDevice(int classId, int localIndex); + virtual void EnqueueLocalReport(int classId, int localIndex, PWBuffer&& buf) = 0; + virtual PWBuffer DequeueReport(int classId, int index) = 0; + virtual void OnPacketError() = 0; + virtual u32 GetTime() = 0; + virtual void DoState(PointerWrap& p) = 0; + PWBuffer* GetLocalSubtype(int classId, int localIndex); +}; + +class ClassBase +{ +public: + enum Class + { + // These are part of the binary format and should not be changed. + ClassSI, + NumClasses + }; + + enum + { + MaxDeviceIndex = 4 + }; + + ClassBase(); + + // Are reports needed for this local device? + bool IsInUse(int localIndex) + { + return m_LocalToRemote[localIndex] != -1; + } + + // Gets the local index, if any, corresponding to this remote device, or -1 + // if there is none. Used for output such as rumble. + int GetLocalIndex(int index) + { + return m_RemoteToLocal[index]; + } + + void SetIndex(int localIndex, int index); + + // These should be called on thread. + virtual void OnConnectedInternal(int index, const PWBuffer* subtype) = 0; + virtual void OnDisconnectedInternal(int index) = 0; + virtual void DoState(PointerWrap& p) = 0; + + s8 m_IsConnected[MaxDeviceIndex]; +private: + s8 m_LocalToRemote[MaxDeviceIndex]; + s8 m_RemoteToLocal[MaxDeviceIndex]; +}; + +extern std::unique_ptr g_Backend; +extern ClassBase* g_Classes[ClassBase::NumClasses]; + +template +class Class : public Base, public ClassBase +{ +public: + Class() + { + g_Classes[Base::ClassId] = this; + } + // Make a local device available. + // subtypeData is data that does not change during the life of the device, + // and is sent along with the connection notice. + void ConnectLocalDevice(int localIndex, typename Base::SubtypeData&& subtypeData) + { + Packet p; + Base::DoSubtypeData(&subtypeData, p); + g_Backend->ConnectLocalDevice(Base::ClassId, localIndex, std::move(*p.vec)); + } + + void DisconnectLocalDevice(int localIndex) + { + g_Backend->DisconnectLocalDevice(Base::ClassId, localIndex); + } + + void DisconnectAllLocalDevices() + { + for (int idx = 0; idx < MaxDeviceIndex; idx++) + DisconnectLocalDevice(idx); + } + + template + void EnqueueLocalReport(int localIndex, Report&& reportData) + { + Packet p; + reportData.DoReport(p); + g_Backend->EnqueueLocalReport(Base::ClassId, localIndex, std::move(*p.vec)); + } + + typename Base::SubtypeData& GetSubtype(int index) + { + return m_Subtypes[index]; + } + + template + Report DequeueReport(int index) + { + while (1) + { + Packet p(g_Backend->DequeueReport(Base::ClassId, index)); + Report reportData; + reportData.DoReport(p); + if (p.failure) + { + g_Backend->OnPacketError(); + continue; + } + return reportData; + } + } + + virtual void OnConnectedInternal(int index, const PWBuffer* subtype) override + { + PointerWrap p(const_cast(subtype), PointerWrap::MODE_READ); + Base::DoSubtypeData(&m_Subtypes[index], p); + m_IsConnected[index] = true; + if (p.failure) + { + m_Subtypes[index] = typename Base::SubtypeData(); + g_Backend->OnPacketError(); + } + Base::OnConnected(index, m_Subtypes[index]); + } + + virtual void OnDisconnectedInternal(int index) override + { + m_Subtypes[index] = typename Base::SubtypeData(); + m_IsConnected[index] = false; + Base::OnDisconnected(index); + } + + virtual void DoState(PointerWrap& p) + { + p.Do(m_IsConnected); + p.Do(m_Subtypes); + } + +private: + typename Base::SubtypeData m_Subtypes[MaxDeviceIndex]; +}; + +void Init(); +void DoState(PointerWrap& p); + +} diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp new file mode 100644 index 000000000000..ba1ee221f5b2 --- /dev/null +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -0,0 +1,61 @@ +#include "IOSyncBackends.h" +#include "Timer.h" + +namespace IOSync +{ + +void BackendLocal::ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf) +{ + Backend::ConnectLocalDevice(classId, localIndex, std::move(buf)); + g_Classes[classId]->SetIndex(localIndex, localIndex); + g_Classes[classId]->OnConnectedInternal(localIndex, GetLocalSubtype(classId, localIndex)); +} + +void BackendLocal::DisconnectLocalDevice(int classId, int localIndex) +{ + Backend::DisconnectLocalDevice(classId, localIndex); + g_Classes[classId]->OnDisconnectedInternal(localIndex); +} + +void BackendLocal::EnqueueLocalReport(int classId, int localIndex, PWBuffer&& buf) +{ + m_Reports[classId][localIndex] = std::move(buf); +} + +PWBuffer BackendLocal::DequeueReport(int classId, int index) +{ + return std::move(m_Reports[classId][index]); +} + +void BackendLocal::OnPacketError() +{ + PanicAlert("Packet error in BackendLocal - input serialization code is messed up."); +} + +u32 BackendLocal::GetTime() +{ + return Common::Timer::GetLocalTimeSinceJan1970(); +} + +void BackendLocal::DoState(PointerWrap& p) +{ + if (p.GetMode() != PointerWrap::MODE_READ) + return; + // Disregard existing devices. + for (int c = 0; c < ClassBase::NumClasses; c++) + { + ClassBase* cls = g_Classes[c]; + for (int d = 0; d < ClassBase::MaxDeviceIndex; d++) + { + if (cls->m_IsConnected[d]) + cls->OnDisconnectedInternal(d); + if (PWBuffer* subtype = GetLocalSubtype(c, d)) + { + g_Classes[c]->SetIndex(d, d); + g_Classes[c]->OnConnectedInternal(d, subtype); + } + } + } +} + +} diff --git a/Source/Core/Core/Src/IOSyncBackends.h b/Source/Core/Core/Src/IOSyncBackends.h new file mode 100644 index 000000000000..c254a8731777 --- /dev/null +++ b/Source/Core/Core/Src/IOSyncBackends.h @@ -0,0 +1,28 @@ +// Copyright 2013 Dolphin Emulator Project +// Licensed under GPLv2 +// Refer to the license.txt file included. + +#pragma once + +#include "IOSync.h" + +namespace IOSync +{ + +// The trivial local backend, with optional movie recording. +class BackendLocal : public Backend +{ +public: + virtual void ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf) override; + virtual void DisconnectLocalDevice(int classId, int localIndex) override; + virtual void EnqueueLocalReport(int classId, int localIndex, PWBuffer&& buf) override; + virtual PWBuffer DequeueReport(int classId, int index) override; + virtual void OnPacketError() override; + virtual u32 GetTime() override; + virtual void DoState(PointerWrap& p) override; +private: + PWBuffer m_Reports[ClassBase::NumClasses][ClassBase::MaxDeviceIndex]; +}; + + +} diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 0092781d6cea..13e3767215fb 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -16,7 +16,6 @@ // for wiimote/ OSD messages #include "Core.h" #include "ConfigManager.h" -#include "Movie.h" #include "HW/WiimoteEmu/WiimoteEmu.h" std::mutex crit_netplay_client; @@ -24,25 +23,6 @@ static NetPlayClient * netplay_client = NULL; NetSettings g_NetPlaySettings; #define RPT_SIZE_HACK (1 << 16) - -NetPad::NetPad() -{ - nHi = 0x00808080; - nLo = 0x80800000; -} - -NetPad::NetPad(const SPADStatus* const pad_status) -{ - nHi = (u32)((u8)pad_status->stickY); - nHi |= (u32)((u8)pad_status->stickX << 8); - nHi |= (u32)((u16)pad_status->button << 16); - nHi |= 0x00800000; - nLo = (u8)pad_status->triggerRight; - nLo |= (u32)((u8)pad_status->triggerLeft << 8); - nLo |= (u32)((u8)pad_status->substickY << 16); - nLo |= (u32)((u8)pad_status->substickX << 24); -} - NetPlayClient::~NetPlayClient() { // not perfect @@ -63,7 +43,9 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam m_state_callback = stateCallback; m_local_name = name; +#if 0 ClearBuffers(); +#endif size_t pos = hostSpec.find(':'); if (pos != std::string::npos) @@ -239,6 +221,7 @@ void NetPlayClient::OnData(Packet&& packet) } break; +#if 0 case NP_MSG_PAD_MAPPING : { packet.DoArray(m_pad_map, 4); @@ -291,7 +274,7 @@ void NetPlayClient::OnData(Packet&& packet) m_wiimote_buffer[(unsigned)map].Push(std::move(nw)); } break; - +#endif case NP_MSG_PAD_BUFFER : { @@ -488,6 +471,7 @@ void NetPlayClient::GetPlayerList(std::string& list, std::vector& pid_list) { const Player *player = &(i->second); ss << player->name << "[" << (int)player->pid << "] : " << player->revision << "\n | "; + #if 0 for (unsigned int j = 0; j < 4; j++) { if (m_pad_map[j] == player->pid) @@ -502,6 +486,7 @@ void NetPlayClient::GetPlayerList(std::string& list, std::vector& pid_list) else ss << '-'; } + #endif ss << " | "; if (player->ping != -1u) ss << player->ping; @@ -553,6 +538,7 @@ void NetPlayClient::ChangeName(const std::string& name) }); } +#if 0 void NetPlayClient::SendPadState(const PadMapping in_game_pad, const NetPad& np) { // send to server @@ -575,6 +561,7 @@ void NetPlayClient::SendWiimoteState(const PadMapping in_game_pad, const NetWiim SendPacket(packet); } +#endif bool NetPlayClient::StartGame(const std::string &path) { @@ -600,6 +587,7 @@ bool NetPlayClient::StartGame(const std::string &path) NetPlay_Enable(this); +#if 0 ClearBuffers(); if (m_dialog->IsRecording()) @@ -618,30 +606,15 @@ bool NetPlayClient::StartGame(const std::string &path) } Movie::BeginRecordingInput(controllers_mask); } +#endif // boot game m_dialog->BootGame(path); +#if 0 UpdateDevices(); - - if (SConfig::GetInstance().m_LocalCoreStartupParameter.bWii) - { - for (unsigned int i = 0; i < 4; ++i) - WiimoteReal::ChangeWiimoteSource(i, m_wiimote_map[i] != -1 ? WIIMOTE_SRC_EMU : WIIMOTE_SRC_NONE); - - // Needed to prevent locking up at boot if (when) the wiimotes connect out of order. - NetWiimote nw; - nw.resize(4, 0); - - for (unsigned int w = 0; w < 4; ++w) - { - if (m_wiimote_map[w] != -1) - // probably overkill, but whatever - for (unsigned int i = 0; i < 7; ++i) - m_wiimote_buffer[w].Push(nw); - } - } +#endif return true; } @@ -651,6 +624,7 @@ bool NetPlayClient::ChangeGame(const std::string&) return true; } +#if 0 void NetPlayClient::UpdateDevices() { for (PadMapping i = 0; i < 4; i++) @@ -742,6 +716,8 @@ bool NetPlayClient::GetNetPads(const u8 pad_nb, const SPADStatus* const pad_stat tmp.substickY = ((u8*)&netvalues->nLo)[2]; tmp.triggerLeft = ((u8*)&netvalues->nLo)[1]; tmp.triggerRight = ((u8*)&netvalues->nLo)[0]; + +#if 0 if (Movie::IsRecordingInput()) { Movie::RecordInput(&tmp, pad_nb); @@ -751,6 +727,7 @@ bool NetPlayClient::GetNetPads(const u8 pad_nb, const SPADStatus* const pad_stat { Movie::CheckPadStatus(&tmp, pad_nb); } +#endif return true; } @@ -849,6 +826,7 @@ bool NetPlayClient::WiimoteUpdate(int _number, u8* data, const u8 size) memcpy(data, nw.data(), size); return true; } +#endif bool NetPlayClient::StopGame() { @@ -875,6 +853,7 @@ void NetPlayClient::Stop() { if (m_is_running == false) return; +#if 0 g_TraversalClient->RunOnThread([=]() mutable { bool isPadMapped = false; ASSUME_ON(NET); @@ -896,8 +875,10 @@ void NetPlayClient::Stop() ENetUtil::BroadcastPacket(m_host, packet); } }); +#endif } +#if 0 u8 NetPlayClient::InGamePadToLocalPad(u8 ingame_pad) { // not our pad @@ -1001,6 +982,7 @@ u8 CSIDevice_GCController::NetPlay_InGamePadToLocalPad(u8 numPAD) else return numPAD; } +#endif bool NetPlay::IsNetPlayRunning() { diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 0223e950173b..7d714024df48 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -13,7 +13,6 @@ #include "enet/enet.h" #include "NetPlayProto.h" -#include "GCPadStatus.h" #include #include @@ -23,16 +22,6 @@ #include "FifoQueue.h" #include "TraversalClient.h" -class NetPad -{ -public: - NetPad(); - NetPad(const SPADStatus* const); - - u32 nHi; - u32 nLo; -}; - class NetPlayUI { public: @@ -94,6 +83,7 @@ class NetPlayClient : public TraversalClientClient void SendChatMessage(const std::string& msg) /* ON(GUI) */; void ChangeName(const std::string& name) /* ON(GUI) */; + #if 0 // Send and receive pads values bool WiimoteUpdate(int _number, u8* data, const u8 size) /* ON(CPU) */; bool GetNetPads(const u8 pad_nb, const SPADStatus* const, NetPad* const netvalues) /* ON(CPU) */; @@ -102,6 +92,7 @@ class NetPlayClient : public TraversalClientClient u8 InGamePadToLocalPad(u8 localPad); u8 LocalWiimoteToInGameWiimote(u8 local_pad); + #endif void SetDialog(NetPlayUI* dialog); @@ -112,13 +103,12 @@ class NetPlayClient : public TraversalClientClient std::function m_state_callback; protected: + #if 0 void ClearBuffers() /* on multiple */; + #endif std::recursive_mutex m_crit; - Common::FifoQueue m_pad_buffer[4]; - Common::FifoQueue m_wiimote_buffer[4]; - NetPlayUI* m_dialog; ENetHost* m_host; std::string m_host_spec; @@ -135,15 +125,19 @@ class NetPlayClient : public TraversalClientClient u32 m_current_game; + #if 0 PadMapping m_pad_map[4]; PadMapping m_wiimote_map[4]; + #endif bool m_is_recording; private: + #if 0 void UpdateDevices() /* on multiple, this sucks */; void SendPadState(const PadMapping in_game_pad, const NetPad& np) /* ON(CPU) */; void SendWiimoteState(const PadMapping in_game_pad, const NetWiimote& nw) /* ON(CPU) */; + #endif void OnData(Packet&& packet) ON(NET); void OnDisconnect(int reason) ON(NET); void SendPacket(Packet& packet); diff --git a/Source/Core/Core/Src/NetPlayProto.h b/Source/Core/Core/Src/NetPlayProto.h index 16181150b57b..a09bc5bc25e9 100644 --- a/Source/Core/Core/Src/NetPlayProto.h +++ b/Source/Core/Core/Src/NetPlayProto.h @@ -25,8 +25,6 @@ struct Rpt : public std::vector u16 channel; }; -typedef std::vector NetWiimote; - #define NETPLAY_VERSION "Dolphin NetPlay 2013-10-03" const int NETPLAY_INITIAL_GCTIME = 1272737767; @@ -41,12 +39,14 @@ enum NP_MSG_CHAT_MESSAGE = 0x30, NP_MSG_CHANGE_NAME = 0x31, + NP_MSG_PAD_BUFFER = 0x60, + /* NP_MSG_PAD_DATA = 0x60, NP_MSG_PAD_MAPPING = 0x61, - NP_MSG_PAD_BUFFER = 0x62, NP_MSG_WIIMOTE_DATA = 0x70, NP_MSG_WIIMOTE_MAPPING = 0x71, + */ NP_MSG_START_GAME = 0xA0, NP_MSG_CHANGE_GAME = 0xA1, diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 17be0487a43a..b81a09321d0e 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -36,8 +36,10 @@ NetPlayServer::NetPlayServer() m_is_running = false; m_num_players = 0; m_dialog = NULL; +#if 0 memset(m_pad_map, -1, sizeof(m_pad_map)); memset(m_wiimote_map, -1, sizeof(m_wiimote_map)); +#endif m_target_buffer_size = 20; #ifdef __APPLE__ m_dynamic_store = SCDynamicStoreCreate(NULL, CFSTR("NetPlayServer"), NULL, NULL); @@ -218,6 +220,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) UpdatePings(); +#if 0 // try to automatically assign new user a pad for (unsigned int m = 0; m < 4; ++m) { @@ -227,6 +230,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) break; } } +#endif // send join message to already connected clients { @@ -281,8 +285,10 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) } } +#if 0 UpdatePadMapping(); // sync pad mappings with everyone UpdateWiimoteMapping(); +#endif return 0; } @@ -296,6 +302,7 @@ void NetPlayServer::OnDisconnect(PlayerId pid) player.connected = false; +#if 0 if (m_is_running) { for (int i = 0; i < 4; i++) @@ -312,6 +319,7 @@ void NetPlayServer::OnDisconnect(PlayerId pid) } } } +#endif Packet opacket; opacket.W((MessageId)NP_MSG_PLAYER_LEAVE); @@ -320,6 +328,7 @@ void NetPlayServer::OnDisconnect(PlayerId pid) // alert other players of disconnect SendToClientsOnThread(opacket); +#if 0 for (int i = 0; i < 4; i++) if (m_pad_map[i] == pid) m_pad_map[i] = -1; @@ -329,8 +338,10 @@ void NetPlayServer::OnDisconnect(PlayerId pid) if (m_wiimote_map[i] == pid) m_wiimote_map[i] = -1; UpdateWiimoteMapping(); +#endif } +#if 0 void NetPlayServer::GetPadMapping(PadMapping map[4]) { for (int i = 0; i < 4; i++) @@ -372,6 +383,7 @@ void NetPlayServer::UpdateWiimoteMapping() opacket.DoArray(m_wiimote_map, 4); SendToClients(opacket); } +#endif void NetPlayServer::AdjustPadBufferSize(unsigned int size) { @@ -452,6 +464,7 @@ void NetPlayServer::OnData(PlayerId pid, Packet&& packet) } break; +#if 0 case NP_MSG_PAD_DATA : { // if this is pad data from the last game still being received, ignore it @@ -495,6 +508,7 @@ void NetPlayServer::OnData(PlayerId pid, Packet&& packet) SendToClients(packet, pid); } break; +#endif case NP_MSG_PONG : { diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 63ebc0211319..f727c7a2529f 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -32,11 +32,13 @@ class NetPlayServer : public TraversalClientClient bool StartGame(const std::string &path) /* ON(GUI) */; +#if 0 void GetPadMapping(PadMapping map[]) /* ON(GUI) */; void SetPadMapping(const PadMapping map[]) /* ON(GUI) */; void GetWiimoteMapping(PadMapping map[]) /* ON(GUI) */; void SetWiimoteMapping(const PadMapping map[]) /* ON(GUI) */; +#endif void AdjustPadBufferSize(unsigned int size) /* multiple threads */; @@ -67,8 +69,10 @@ class NetPlayServer : public TraversalClientClient MessageId OnConnect(PlayerId pid, Packet& hello) ON(NET); void OnDisconnect(PlayerId pid) ON(NET); void OnData(PlayerId pid, Packet&& packet) ON(NET); +#if 0 void UpdatePadMapping() /* multiple threads */; void UpdateWiimoteMapping() /* multiple threads */; +#endif void UpdatePings() ON(NET); std::vector> GetInterfaceListInternal(); @@ -80,8 +84,10 @@ class NetPlayServer : public TraversalClientClient bool m_update_pings; u32 m_current_game; u32 m_target_buffer_size; +#if 0 PadMapping m_pad_map[4]; PadMapping m_wiimote_map[4]; +#endif std::vector m_players; unsigned m_num_players; diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index a316a5baf5e6..8d6d4231ce67 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -478,6 +478,7 @@ void NetPlayDiag::OnThread(wxCommandEvent& event) void NetPlayDiag::OnConfigPads(wxCommandEvent&) { +#if 0 PadMapping mapping[4]; PadMapping wiimotemapping[4]; std::vector player_list; @@ -488,6 +489,7 @@ void NetPlayDiag::OnConfigPads(wxCommandEvent&) pmd.ShowModal(); netplay_server->SetPadMapping(mapping); netplay_server->SetWiimoteMapping(wiimotemapping); +#endif } void NetPlayDiag::OnDefocusName(wxFocusEvent&) From 6aba5a9b1b606557962230f61d3eb6a77766758e Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 22 Oct 2013 01:38:39 -0400 Subject: [PATCH 056/202] Avoid copying packets for no good reason because they're tiny. --- Source/Core/Common/Src/ChunkFile.h | 11 ++++++ Source/Core/Common/Src/TraversalClient.cpp | 20 ++++++++-- Source/Core/Common/Src/TraversalClient.h | 5 ++- Source/Core/Core/Src/NetPlayClient.cpp | 14 +++---- Source/Core/Core/Src/NetPlayClient.h | 2 +- Source/Core/Core/Src/NetPlayServer.cpp | 44 +++++++++++----------- Source/Core/Core/Src/NetPlayServer.h | 4 +- 7 files changed, 62 insertions(+), 38 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index 329518ececb7..3958376b9b46 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -69,6 +69,10 @@ class PWBuffer : public NonCopyable { free(m_Data); } + PWBuffer copy() + { + return PWBuffer(m_Data, m_Size); + } void swap(PWBuffer& other) { std::swap(m_Data, other.m_Data); @@ -98,6 +102,13 @@ class PWBuffer : public NonCopyable resize(old + _size); memcpy(&m_Data[old], inData, _size); } + u8* release_data() + { + u8* data = m_Data; + m_Data = NULL; + m_Size = m_Capacity = 0; + return data; + } u8* data() { return m_Data; } const u8* data() const { return m_Data; } u8& operator[](size_t i) { return m_Data[i]; } diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index 3f51b3cbc3b1..406bd97660a9 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -3,14 +3,26 @@ #include "TraversalClient.h" #include "enet/enet.h" -void ENetUtil::BroadcastPacket(ENetHost* host, const Packet& pac) +// derp +inline ENetPacket* ENetUtil::MakeENetPacket(Packet&& pac, enet_uint32 flags) { - enet_host_broadcast(host, 0, enet_packet_create((u8*) pac.vec->data(), pac.vec->size(), ENET_PACKET_FLAG_RELIABLE)); + ENetPacket* packet = (ENetPacket*) enet_malloc (sizeof (ENetPacket)); + packet->dataLength = pac.vec->size(); + packet->data = pac.vec->release_data(); + packet->referenceCount = 0; + packet->flags = flags; + packet->freeCallback = NULL; + return packet; } -void ENetUtil::SendPacket(ENetPeer* peer, const Packet& pac) +void ENetUtil::BroadcastPacket(ENetHost* host, Packet&& pac) { - enet_peer_send(peer, 0, enet_packet_create((u8*) pac.vec->data(), pac.vec->size(), ENET_PACKET_FLAG_RELIABLE)); + enet_host_broadcast(host, 0, MakeENetPacket(std::move(pac), ENET_PACKET_FLAG_RELIABLE)); +} + +void ENetUtil::SendPacket(ENetPeer* peer, Packet&& pac) +{ + enet_peer_send(peer, 0, MakeENetPacket(std::move(pac), ENET_PACKET_FLAG_RELIABLE)); } Packet ENetUtil::MakePacket(ENetPacket* epacket) diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index 58e052d791f0..32191041b45c 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -17,8 +17,9 @@ DEFINE_THREAD_HAT(NET); #include "ChunkFile.h" namespace ENetUtil { - void BroadcastPacket(ENetHost* host, const Packet& pac) ON(NET); - void SendPacket(ENetPeer* peer, const Packet& pac) ON(NET); + ENetPacket* MakeENetPacket(Packet&& pac, enet_uint32 flags); + void BroadcastPacket(ENetHost* host, Packet&& pac) ON(NET); + void SendPacket(ENetPeer* peer, Packet&& pac) ON(NET); Packet MakePacket(ENetPacket* epacket); void Wakeup(ENetHost* host); int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) /* ON(NET) */; diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 13e3767215fb..7a187a7e1c72 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -96,12 +96,12 @@ void NetPlayClient::SetDialog(NetPlayUI* dialog) m_have_dialog_event.Set(); } -void NetPlayClient::SendPacket(Packet& packet) +void NetPlayClient::SendPacket(Packet&& packet) { CopyAsMove tmp(std::move(packet)); g_TraversalClient->RunOnThread([=]() mutable { ASSUME_ON(NET); - ENetUtil::BroadcastPacket(m_host, *tmp); + ENetUtil::BroadcastPacket(m_host, std::move(*tmp)); }); } @@ -345,7 +345,7 @@ void NetPlayClient::OnData(Packet&& packet) pong.W(ping_key); std::lock_guard lk(m_crit); - ENetUtil::BroadcastPacket(m_host, pong); + ENetUtil::BroadcastPacket(m_host, std::move(pong)); } break; @@ -406,7 +406,7 @@ void NetPlayClient::OnENetEvent(ENetEvent* event) hello.W(std::string(NETPLAY_VERSION)); hello.W(std::string(netplay_dolphin_ver)); hello.W(m_local_name); - ENetUtil::BroadcastPacket(m_host, hello); + ENetUtil::BroadcastPacket(m_host, std::move(hello)); m_state = WaitingForHelloResponse; if (m_state_callback) m_state_callback(this); @@ -519,7 +519,7 @@ void NetPlayClient::SendChatMessage(const std::string& msg) packet.W((MessageId)NP_MSG_CHAT_MESSAGE); packet.W(msg); - SendPacket(packet); + SendPacket(std::move(packet)); } void NetPlayClient::ChangeName(const std::string& name) @@ -534,7 +534,7 @@ void NetPlayClient::ChangeName(const std::string& name) packet.W((MessageId)NP_MSG_CHANGE_NAME); packet.W(name); - SendPacket(packet); + SendPacket(std::move(packet)); }); } @@ -579,7 +579,7 @@ bool NetPlayClient::StartGame(const std::string &path) packet.W(m_current_game); packet.W(g_NetPlaySettings); - SendPacket(packet); + SendPacket(std::move(packet)); std::lock_guard lk(m_crit); diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 7d714024df48..4f7a6ba17ea8 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -140,7 +140,7 @@ class NetPlayClient : public TraversalClientClient #endif void OnData(Packet&& packet) ON(NET); void OnDisconnect(int reason) ON(NET); - void SendPacket(Packet& packet); + void SendPacket(Packet&& packet); void DoDirectConnect(const ENetAddress& addr); PlayerId m_pid; diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index b81a09321d0e..83f040f00a05 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -68,7 +68,7 @@ void NetPlayServer::UpdatePings() ping.W(m_ping_key); m_ping_timer.Start(); - SendToClientsOnThread(ping); + SendToClientsOnThread(std::move(ping)); m_update_pings = false; } @@ -239,7 +239,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) opacket.W(pid); opacket.W(player.name); opacket.W(player.revision); - SendToClientsOnThread(opacket); + SendToClientsOnThread(std::move(opacket)); } // send new client success message with their id @@ -247,7 +247,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) Packet opacket; opacket.W((MessageId)0); opacket.W(pid); - ENetUtil::SendPacket(peer, opacket); + ENetUtil::SendPacket(peer, std::move(opacket)); } // send new client the selected game @@ -258,7 +258,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) Packet opacket; opacket.W((MessageId)NP_MSG_CHANGE_GAME); opacket.W(m_selected_game); - ENetUtil::SendPacket(peer, opacket); + ENetUtil::SendPacket(peer, std::move(opacket)); } } @@ -267,7 +267,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) Packet opacket; opacket.W((MessageId)NP_MSG_PAD_BUFFER); opacket.W((u32)m_target_buffer_size); - ENetUtil::SendPacket(peer, opacket); + ENetUtil::SendPacket(peer, std::move(opacket)); } // send players @@ -281,7 +281,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) opacket.W((PlayerId)opid); opacket.W(oplayer.name); opacket.W(oplayer.revision); - ENetUtil::SendPacket(peer, opacket); + ENetUtil::SendPacket(peer, std::move(opacket)); } } @@ -314,7 +314,7 @@ void NetPlayServer::OnDisconnect(PlayerId pid) Packet opacket; opacket.W((MessageId)NP_MSG_DISABLE_GAME); - SendToClientsOnThread(opacket); + SendToClientsOnThread(std::move(opacket)); break; } } @@ -326,7 +326,7 @@ void NetPlayServer::OnDisconnect(PlayerId pid) opacket.W(pid); // alert other players of disconnect - SendToClientsOnThread(opacket); + SendToClientsOnThread(std::move(opacket)); #if 0 for (int i = 0; i < 4; i++) @@ -373,7 +373,7 @@ void NetPlayServer::UpdatePadMapping() Packet opacket; opacket.W((MessageId)NP_MSG_PAD_MAPPING); opacket.DoArray(m_pad_map, 4); - SendToClients(opacket); + SendToClients(std::move(opacket)); } void NetPlayServer::UpdateWiimoteMapping() @@ -381,7 +381,7 @@ void NetPlayServer::UpdateWiimoteMapping() Packet opacket; opacket.W((MessageId)NP_MSG_WIIMOTE_MAPPING); opacket.DoArray(m_wiimote_map, 4); - SendToClients(opacket); + SendToClients(std::move(opacket)); } #endif @@ -393,7 +393,7 @@ void NetPlayServer::AdjustPadBufferSize(unsigned int size) Packet opacket; opacket.W((MessageId)NP_MSG_PAD_BUFFER); opacket.W(m_target_buffer_size); - SendToClients(opacket); + SendToClients(std::move(opacket)); } void NetPlayServer::OnData(PlayerId pid, Packet&& packet) @@ -411,7 +411,7 @@ void NetPlayServer::OnData(PlayerId pid, Packet&& packet) Packet opacket; opacket.W(error); opacket.W((PlayerId)0); - ENetUtil::SendPacket(peer, opacket); + ENetUtil::SendPacket(peer, std::move(opacket)); enet_peer_disconnect_later(peer, 0); } else @@ -442,7 +442,7 @@ void NetPlayServer::OnData(PlayerId pid, Packet&& packet) opacket.W(pid); opacket.W(msg); - SendToClientsOnThread(opacket, pid); + SendToClientsOnThread(std::move(opacket), pid); } break; @@ -460,7 +460,7 @@ void NetPlayServer::OnData(PlayerId pid, Packet&& packet) opacket.W(pid); opacket.W(name); - SendToClientsOnThread(opacket, pid); + SendToClientsOnThread(std::move(opacket), pid); } break; @@ -526,7 +526,7 @@ void NetPlayServer::OnData(PlayerId pid, Packet&& packet) opacket.W(pid); opacket.W(player.ping); - SendToClientsOnThread(opacket); + SendToClientsOnThread(std::move(opacket)); } break; @@ -544,7 +544,7 @@ void NetPlayServer::OnData(PlayerId pid, Packet&& packet) Packet opacket; opacket.W((MessageId)NP_MSG_STOP_GAME); - SendToClientsOnThread(opacket); + SendToClientsOnThread(std::move(opacket)); m_is_running = false; } @@ -567,7 +567,7 @@ bool NetPlayServer::ChangeGame(const std::string &game) Packet opacket; opacket.W((MessageId)NP_MSG_CHANGE_GAME); opacket.W(game); - SendToClients(opacket); + SendToClients(std::move(opacket)); return true; } @@ -595,24 +595,24 @@ bool NetPlayServer::StartGame(const std::string &path) opacket.W((int) m_settings.m_EXIDevice[0]); opacket.W((int) m_settings.m_EXIDevice[1]); - SendToClients(opacket); + SendToClients(std::move(opacket)); m_is_running = true; return true; } -void NetPlayServer::SendToClients(Packet& packet, const PlayerId skip_pid) +void NetPlayServer::SendToClients(Packet&& packet, const PlayerId skip_pid) { CopyAsMove tmp(std::move(packet)); g_TraversalClient->RunOnThread([=]() mutable { ASSUME_ON(NET); - SendToClientsOnThread(*tmp, skip_pid); + SendToClientsOnThread(std::move(*tmp), skip_pid); }); } -void NetPlayServer::SendToClientsOnThread(const Packet& packet, const PlayerId skip_pid) +void NetPlayServer::SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid) { ENetPacket* epacket = NULL; for (size_t pid = 0; pid < m_players.size(); pid++) @@ -621,7 +621,7 @@ void NetPlayServer::SendToClientsOnThread(const Packet& packet, const PlayerId s m_players[pid].connected) { if (!epacket) - epacket = enet_packet_create(packet.vec->data(), packet.vec->size(), ENET_PACKET_FLAG_RELIABLE); + epacket = ENetUtil::MakeENetPacket(std::move(packet), ENET_PACKET_FLAG_RELIABLE); enet_peer_send(&m_host->peers[pid], 0, epacket); } } diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index f727c7a2529f..7624a5a705d5 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -64,8 +64,8 @@ class NetPlayServer : public TraversalClientClient bool connected; }; - void SendToClients(Packet& packet, const PlayerId skip_pid = -1); - void SendToClientsOnThread(const Packet& packet, const PlayerId skip_pid = -1) ON(NET); + void SendToClients(Packet&& packet, const PlayerId skip_pid = -1); + void SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid = -1) ON(NET); MessageId OnConnect(PlayerId pid, Packet& hello) ON(NET); void OnDisconnect(PlayerId pid) ON(NET); void OnData(PlayerId pid, Packet&& packet) ON(NET); From f5b29ebee59e2f0e01336a1351451428bc8adf53 Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 24 Oct 2013 17:40:52 -0400 Subject: [PATCH 057/202] Add NOT_ON to help check that thread queued functions are not unnecessarily being called while already on the thread. --- Source/Core/Common/Src/Common.h | 2 ++ Source/Core/Common/Src/TraversalClient.h | 2 +- Source/Core/Core/Src/NetPlayServer.h | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/Core/Common/Src/Common.h b/Source/Core/Common/Src/Common.h index d7e067ae1996..6f113c30828a 100644 --- a/Source/Core/Common/Src/Common.h +++ b/Source/Core/Common/Src/Common.h @@ -235,6 +235,8 @@ DEFINE_THREAD_HAT(GUI); _TS_MACRO(__attribute__((pt_guarded_by(name##ThreadHat)))) #define ON(name) \ _TS_MACRO(__attribute__((exclusive_locks_required(name##ThreadHat)))) +#define NOT_ON(name) \ + _TS_MACRO(__attribute__((locks_excluded(name##ThreadHat)))) #define IGNORE_THREAD_SAFETY \ _TS_MACRO(__attribute__((no_thread_safety_analysis))) #define ASSUME_ON(name) \ diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index 32191041b45c..aa377e8e7d11 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -53,7 +53,7 @@ class ENetHostClient public: ENetHostClient(size_t peerCount, u16 port, bool isTraversalClient = false); ~ENetHostClient(); - void RunOnThread(std::function func); + void RunOnThread(std::function func) NOT_ON(NET); void CreateThread(); void Reset(); diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 7624a5a705d5..0a4bb7eb8583 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -64,7 +64,7 @@ class NetPlayServer : public TraversalClientClient bool connected; }; - void SendToClients(Packet&& packet, const PlayerId skip_pid = -1); + void SendToClients(Packet&& packet, const PlayerId skip_pid = -1) NOT_ON(NET); void SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid = -1) ON(NET); MessageId OnConnect(PlayerId pid, Packet& hello) ON(NET); void OnDisconnect(PlayerId pid) ON(NET); From ef3e6e1fde67aa42c1d5f06f7353cdcef69daeeb Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 24 Oct 2013 22:30:10 -0400 Subject: [PATCH 058/202] Poll more or less frequently depending on whether anyone's connected. In what is arguably a flaw in enet, packet timeouts can't happen while enet_host_service is sitting in poll(), and for now traversal resends also rely on going through the loop. However, if traversal is the only client, 300ms is fine. --- Source/Core/Common/Src/TraversalClient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index 406bd97660a9..2edcd01883f5 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -152,7 +152,7 @@ void ENetHostClient::ThreadFunc() PanicAlert("enet_socket_get_address failed."); continue; } - int count = enet_host_service(m_Host, &event, 500); + int count = enet_host_service(m_Host, &event, m_Host->connectedPeers > 0 ? 10 : 300); if (count < 0) { PanicAlert("enet_host_service failed... do something about this."); From ab1ec753599adf431fc2b74638da0c72fa84e85e Mon Sep 17 00:00:00 2001 From: comex Date: Sat, 26 Oct 2013 15:18:07 -0400 Subject: [PATCH 059/202] Simplify the IOSync interface. --- Source/Core/Common/Src/ChunkFile.h | 11 ++- Source/Core/Core/Src/HW/SI.cpp | 14 ++- Source/Core/Core/Src/HW/SI_Device.cpp | 2 +- Source/Core/Core/Src/HW/SI_Device.h | 12 +-- Source/Core/Core/Src/IOSync.cpp | 62 ++++++------ Source/Core/Core/Src/IOSync.h | 122 +++++++++++++----------- Source/Core/Core/Src/IOSyncBackends.cpp | 22 ++--- Source/Core/Core/Src/IOSyncBackends.h | 2 +- 8 files changed, 133 insertions(+), 114 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index 3958376b9b46..b002ead71a00 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -69,7 +69,7 @@ class PWBuffer : public NonCopyable { free(m_Data); } - PWBuffer copy() + PWBuffer copy() const { return PWBuffer(m_Data, m_Size); } @@ -256,6 +256,15 @@ class PointerWrap } } + void Do(PWBuffer& x) + { + u32 size = (u32)x.size(); + Do(size); + if (mode == MODE_READ) + x.resize(size); + DoArray(x.data(), size); + } + template void Do(std::list& x) { diff --git a/Source/Core/Core/Src/HW/SI.cpp b/Source/Core/Core/Src/HW/SI.cpp index 2eed984068ed..9030afa448f1 100644 --- a/Source/Core/Core/Src/HW/SI.cpp +++ b/Source/Core/Core/Src/HW/SI.cpp @@ -210,19 +210,23 @@ static u8 g_SIBuffer[128]; static int changeDevice[4]; } -void SIDeviceSyncerBase::OnConnected(int channel, SIDevices& subtype) +void SISyncClass::OnConnected(int channel, PWBuffer&& subtype) { + IOSync::Class::OnConnected(channel, std::move(subtype)); + SIDevices type = GrabSubtype(GetSubtype(channel)); + CoreTiming::RemoveAllEvents(SerialInterface::changeDevice[channel]); auto& sidev = SerialInterface::g_Channel[channel].m_pDevice; - if (!sidev || subtype != sidev->GetDeviceType()) + if (!sidev || type != sidev->GetDeviceType()) { CoreTiming::ScheduleEvent(0, SerialInterface::changeDevice[channel], ((u64)channel << 32) | SIDEVICE_NONE); - CoreTiming::ScheduleEvent(50000000, SerialInterface::changeDevice[channel], ((u64)channel << 32) | subtype); + CoreTiming::ScheduleEvent(50000000, SerialInterface::changeDevice[channel], ((u64)channel << 32) | type); } } -void SIDeviceSyncerBase::OnDisconnected(int channel) +void SISyncClass::OnDisconnected(int channel) { + IOSync::Class::OnDisconnected(channel); CoreTiming::RemoveAllEvents(SerialInterface::changeDevice[channel]); CoreTiming::ScheduleEvent(0, SerialInterface::changeDevice[channel], ((u64)channel << 32) | SIDEVICE_NONE); } @@ -611,7 +615,7 @@ void ChangeDevice(SIDevices device, int channel) { g_SISyncClass.DisconnectLocalDevice(channel); if (device != SIDEVICE_NONE) - g_SISyncClass.ConnectLocalDevice(channel, std::move(device)); + g_SISyncClass.ConnectLocalDevice(channel, g_SISyncClass.PushSubtype(device)); } void UpdateDevices() diff --git a/Source/Core/Core/Src/HW/SI_Device.cpp b/Source/Core/Core/Src/HW/SI_Device.cpp index 1817c9ffb51e..7bd156e07ea9 100644 --- a/Source/Core/Core/Src/HW/SI_Device.cpp +++ b/Source/Core/Core/Src/HW/SI_Device.cpp @@ -92,4 +92,4 @@ ISIDevice* SIDevice_Create(const SIDevices device, const int port_number) } } -IOSync::Class g_SISyncClass; +SISyncClass g_SISyncClass; diff --git a/Source/Core/Core/Src/HW/SI_Device.h b/Source/Core/Core/Src/HW/SI_Device.h index ba74536494c6..99319572072e 100644 --- a/Source/Core/Core/Src/HW/SI_Device.h +++ b/Source/Core/Core/Src/HW/SI_Device.h @@ -56,16 +56,14 @@ enum SIDevices SIDEVICE_AM_BASEBOARD }; -class SIDeviceSyncerBase +class SISyncClass : public IOSync::Class { public: - enum { ClassId = IOSync::ClassBase::ClassSI }; - typedef SIDevices SubtypeData; - void OnConnected(int index, SIDevices& subtype); - void OnDisconnected(int index); - void DoSubtypeData(SIDevices* subtype, PointerWrap& p) { p.Do(*subtype); } + SISyncClass() : IOSync::Class(ClassSI) {} + virtual void OnConnected(int index, PWBuffer&& subtype) override; + virtual void OnDisconnected(int index) override; }; -extern IOSync::Class g_SISyncClass; +extern SISyncClass g_SISyncClass; class ISIDevice { diff --git a/Source/Core/Core/Src/IOSync.cpp b/Source/Core/Core/Src/IOSync.cpp index c5703e1eb6d7..609c02eccd01 100644 --- a/Source/Core/Core/Src/IOSync.cpp +++ b/Source/Core/Core/Src/IOSync.cpp @@ -4,47 +4,49 @@ namespace IOSync { -// This sticks around even if the backend is replaced. -static PWBuffer g_LocalSubtypes[ClassBase::NumClasses][ClassBase::MaxDeviceIndex]; -static bool g_LocalIsConnected[ClassBase::NumClasses][ClassBase::MaxDeviceIndex]; - -void Backend::ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf) +void Class::SetIndex(int localIndex, int index) { - g_LocalSubtypes[classId][localIndex] = std::move(buf); - g_LocalIsConnected[classId][localIndex] = true; + if (localIndex != -1) + { + int oldRemote = m_Local[localIndex].m_OtherIndex; + if (oldRemote != -1) + m_Remote[oldRemote].m_OtherIndex = -1; + m_Local[localIndex].m_OtherIndex = index; + } + if (index != -1) + { + int oldLocal = m_Remote[index].m_OtherIndex; + if (oldLocal != -1) + m_Local[oldLocal].m_OtherIndex = -1; + m_Remote[index].m_OtherIndex = localIndex; + } } -void Backend::DisconnectLocalDevice(int classId, int localIndex) +void Class::OnConnected(int index, PWBuffer&& subtype) { - g_LocalIsConnected[classId][localIndex] = false; + m_Remote[index].m_Subtype = std::move(subtype); + m_Remote[index].m_IsConnected = true; } -PWBuffer* Backend::GetLocalSubtype(int classId, int localIndex) +void Class::OnDisconnected(int index) { - return g_LocalIsConnected[classId][localIndex] ? - &g_LocalSubtypes[classId][localIndex] : - NULL; - + m_Remote[index] = DeviceInfo(); } -ClassBase::ClassBase() +void Class::DeviceInfo::DoState(PointerWrap& p) { - for (int d = 0; d < MaxDeviceIndex; d++) - { - m_LocalToRemote[d] = -1; - m_RemoteToLocal[d] = -1; - m_IsConnected[d] = 0; - } + p.Do(m_OtherIndex); + p.Do(m_IsConnected); + p.Do(m_Subtype); } -void ClassBase::SetIndex(int localIndex, int index) +void Class::DoState(PointerWrap& p) { - int oldRemote = m_LocalToRemote[localIndex]; - if (oldRemote != -1) - m_RemoteToLocal[oldRemote] = -1; - m_LocalToRemote[localIndex] = index; - if (index != -1) - m_RemoteToLocal[index] = localIndex; + for (int i = 0; i< MaxDeviceIndex; i++) + { + m_Local[i].DoState(p); + m_Remote[i].DoState(p); + } } void Init() @@ -54,7 +56,7 @@ void Init() void DoState(PointerWrap& p) { - for (int c = 0; c < ClassBase::NumClasses; c++) + for (int c = 0; c < Class::NumClasses; c++) { g_Classes[c]->DoState(p); } @@ -63,6 +65,6 @@ void DoState(PointerWrap& p) } std::unique_ptr g_Backend; -ClassBase* g_Classes[ClassBase::NumClasses]; +Class* g_Classes[Class::NumClasses]; } diff --git a/Source/Core/Core/Src/IOSync.h b/Source/Core/Core/Src/IOSync.h index cab1630fb8f4..0f9b16fdbfde 100644 --- a/Source/Core/Core/Src/IOSync.h +++ b/Source/Core/Core/Src/IOSync.h @@ -11,23 +11,29 @@ namespace IOSync { + +class Class; +class Backend; + +extern std::unique_ptr g_Backend; +extern Class* g_Classes[]; + class Backend { public: - virtual void ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf); - virtual void DisconnectLocalDevice(int classId, int localIndex); + virtual void ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf) = 0; + virtual void DisconnectLocalDevice(int classId, int localIndex) = 0; virtual void EnqueueLocalReport(int classId, int localIndex, PWBuffer&& buf) = 0; virtual PWBuffer DequeueReport(int classId, int index) = 0; virtual void OnPacketError() = 0; virtual u32 GetTime() = 0; virtual void DoState(PointerWrap& p) = 0; - PWBuffer* GetLocalSubtype(int classId, int localIndex); }; -class ClassBase +class Class { public: - enum Class + enum ClassID { // These are part of the binary format and should not be changed. ClassSI, @@ -39,58 +45,38 @@ class ClassBase MaxDeviceIndex = 4 }; - ClassBase(); + Class(int classId) + : m_ClassId(classId) + { + g_Classes[classId] = this; + } // Are reports needed for this local device? bool IsInUse(int localIndex) { - return m_LocalToRemote[localIndex] != -1; + return m_Local[localIndex].m_OtherIndex != -1; } // Gets the local index, if any, corresponding to this remote device, or -1 // if there is none. Used for output such as rumble. int GetLocalIndex(int index) { - return m_RemoteToLocal[index]; + return m_Remote[index].m_OtherIndex; } void SetIndex(int localIndex, int index); - // These should be called on thread. - virtual void OnConnectedInternal(int index, const PWBuffer* subtype) = 0; - virtual void OnDisconnectedInternal(int index) = 0; - virtual void DoState(PointerWrap& p) = 0; - - s8 m_IsConnected[MaxDeviceIndex]; -private: - s8 m_LocalToRemote[MaxDeviceIndex]; - s8 m_RemoteToLocal[MaxDeviceIndex]; -}; - -extern std::unique_ptr g_Backend; -extern ClassBase* g_Classes[ClassBase::NumClasses]; - -template -class Class : public Base, public ClassBase -{ -public: - Class() - { - g_Classes[Base::ClassId] = this; - } // Make a local device available. // subtypeData is data that does not change during the life of the device, // and is sent along with the connection notice. - void ConnectLocalDevice(int localIndex, typename Base::SubtypeData&& subtypeData) + void ConnectLocalDevice(int localIndex, PWBuffer&& subtypeData) { - Packet p; - Base::DoSubtypeData(&subtypeData, p); - g_Backend->ConnectLocalDevice(Base::ClassId, localIndex, std::move(*p.vec)); + g_Backend->ConnectLocalDevice(m_ClassId, localIndex, std::move(subtypeData)); } void DisconnectLocalDevice(int localIndex) { - g_Backend->DisconnectLocalDevice(Base::ClassId, localIndex); + g_Backend->DisconnectLocalDevice(m_ClassId, localIndex); } void DisconnectAllLocalDevices() @@ -104,12 +90,22 @@ class Class : public Base, public ClassBase { Packet p; reportData.DoReport(p); - g_Backend->EnqueueLocalReport(Base::ClassId, localIndex, std::move(*p.vec)); + g_Backend->EnqueueLocalReport(m_ClassId, localIndex, std::move(*p.vec)); } - typename Base::SubtypeData& GetSubtype(int index) + const PWBuffer* GetSubtype(int index) { - return m_Subtypes[index]; + return &m_Remote[index].m_Subtype; + } + + const PWBuffer* GetLocalSubtype(int index) + { + return &m_Local[index].m_Subtype; + } + + bool IsConnected(int index) + { + return m_Remote[index].m_IsConnected; } template @@ -117,7 +113,7 @@ class Class : public Base, public ClassBase { while (1) { - Packet p(g_Backend->DequeueReport(Base::ClassId, index)); + Packet p(g_Backend->DequeueReport(m_ClassId, index)); Report reportData; reportData.DoReport(p); if (p.failure) @@ -129,36 +125,48 @@ class Class : public Base, public ClassBase } } - virtual void OnConnectedInternal(int index, const PWBuffer* subtype) override + template + static T GrabSubtype(const PWBuffer* buf) { - PointerWrap p(const_cast(subtype), PointerWrap::MODE_READ); - Base::DoSubtypeData(&m_Subtypes[index], p); - m_IsConnected[index] = true; + PointerWrap p(const_cast(buf), PointerWrap::MODE_READ); + T t = T(); + p.Do(t); if (p.failure) - { - m_Subtypes[index] = typename Base::SubtypeData(); g_Backend->OnPacketError(); - } - Base::OnConnected(index, m_Subtypes[index]); + return t; } - virtual void OnDisconnectedInternal(int index) override + template + static PWBuffer PushSubtype(T subtype) { - m_Subtypes[index] = typename Base::SubtypeData(); - m_IsConnected[index] = false; - Base::OnDisconnected(index); + Packet p; + p.W(subtype); + return std::move(*p.vec); } - virtual void DoState(PointerWrap& p) - { - p.Do(m_IsConnected); - p.Do(m_Subtypes); - } + // These should be called on thread. + virtual void OnConnected(int index, PWBuffer&& subtype); + virtual void OnDisconnected(int index); + virtual void DoState(PointerWrap& p); private: - typename Base::SubtypeData m_Subtypes[MaxDeviceIndex]; + struct DeviceInfo // local or remote + { + DeviceInfo() + : m_OtherIndex(-1), m_IsConnected(false) {} + void DoState(PointerWrap& p); + + s8 m_OtherIndex; // remote<->local + bool m_IsConnected; + PWBuffer m_Subtype; + }; + + DeviceInfo m_Local[MaxDeviceIndex]; + DeviceInfo m_Remote[MaxDeviceIndex]; + int m_ClassId; }; + void Init(); void DoState(PointerWrap& p); diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index ba1ee221f5b2..b32c762bb5e5 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -6,15 +6,13 @@ namespace IOSync void BackendLocal::ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf) { - Backend::ConnectLocalDevice(classId, localIndex, std::move(buf)); g_Classes[classId]->SetIndex(localIndex, localIndex); - g_Classes[classId]->OnConnectedInternal(localIndex, GetLocalSubtype(classId, localIndex)); + g_Classes[classId]->OnConnected(localIndex, std::move(buf)); } void BackendLocal::DisconnectLocalDevice(int classId, int localIndex) { - Backend::DisconnectLocalDevice(classId, localIndex); - g_Classes[classId]->OnDisconnectedInternal(localIndex); + g_Classes[classId]->OnDisconnected(localIndex); } void BackendLocal::EnqueueLocalReport(int classId, int localIndex, PWBuffer&& buf) @@ -42,17 +40,17 @@ void BackendLocal::DoState(PointerWrap& p) if (p.GetMode() != PointerWrap::MODE_READ) return; // Disregard existing devices. - for (int c = 0; c < ClassBase::NumClasses; c++) + for (int c = 0; c < Class::NumClasses; c++) { - ClassBase* cls = g_Classes[c]; - for (int d = 0; d < ClassBase::MaxDeviceIndex; d++) + Class* cls = g_Classes[c]; + for (int d = 0; d < Class::MaxDeviceIndex; d++) { - if (cls->m_IsConnected[d]) - cls->OnDisconnectedInternal(d); - if (PWBuffer* subtype = GetLocalSubtype(c, d)) + if (cls->IsConnected(d)) + cls->OnDisconnected(d); + if (const PWBuffer* subtype = cls->GetLocalSubtype(d)) { - g_Classes[c]->SetIndex(d, d); - g_Classes[c]->OnConnectedInternal(d, subtype); + cls->SetIndex(d, d); + cls->OnConnected(d, subtype->copy()); } } } diff --git a/Source/Core/Core/Src/IOSyncBackends.h b/Source/Core/Core/Src/IOSyncBackends.h index c254a8731777..02511595de93 100644 --- a/Source/Core/Core/Src/IOSyncBackends.h +++ b/Source/Core/Core/Src/IOSyncBackends.h @@ -21,7 +21,7 @@ class BackendLocal : public Backend virtual u32 GetTime() override; virtual void DoState(PointerWrap& p) override; private: - PWBuffer m_Reports[ClassBase::NumClasses][ClassBase::MaxDeviceIndex]; + PWBuffer m_Reports[Class::NumClasses][Class::MaxDeviceIndex]; }; From 6f24c1ab42e3c5556105f391c221ab3ad8c3b481 Mon Sep 17 00:00:00 2001 From: comex Date: Sat, 26 Oct 2013 18:54:47 -0400 Subject: [PATCH 060/202] NetPlay for IOSync! --- Source/Core/Common/Src/ChunkFile.h | 4 + Source/Core/Core/Src/HW/SI.cpp | 8 +- Source/Core/Core/Src/HW/SI_Device.h | 1 + .../Core/Core/Src/HW/SI_DeviceAMBaseboard.cpp | 6 +- .../Core/Src/HW/SI_DeviceGCController.cpp | 99 ++-- Source/Core/Core/Src/IOSync.cpp | 10 +- Source/Core/Core/Src/IOSync.h | 34 +- Source/Core/Core/Src/IOSyncBackends.cpp | 215 +++++++- Source/Core/Core/Src/IOSyncBackends.h | 51 +- Source/Core/Core/Src/NetPlayClient.cpp | 500 ++---------------- Source/Core/Core/Src/NetPlayClient.h | 42 +- Source/Core/Core/Src/NetPlayProto.h | 9 +- Source/Core/Core/Src/NetPlayServer.cpp | 199 ++++--- Source/Core/Core/Src/NetPlayServer.h | 30 +- 14 files changed, 528 insertions(+), 680 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index b002ead71a00..7f9383b26d99 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -102,6 +102,10 @@ class PWBuffer : public NonCopyable resize(old + _size); memcpy(&m_Data[old], inData, _size); } + void append(const PWBuffer& other) + { + append(other.m_Data, other.m_Size); + } u8* release_data() { u8* data = m_Data; diff --git a/Source/Core/Core/Src/HW/SI.cpp b/Source/Core/Core/Src/HW/SI.cpp index 9030afa448f1..9e922c12d6b2 100644 --- a/Source/Core/Core/Src/HW/SI.cpp +++ b/Source/Core/Core/Src/HW/SI.cpp @@ -613,7 +613,8 @@ void ChangeDeviceCallback(u64 userdata, int cyclesLate) void ChangeDevice(SIDevices device, int channel) { - g_SISyncClass.DisconnectLocalDevice(channel); + if (g_SISyncClass.LocalIsConnected(channel)) + g_SISyncClass.DisconnectLocalDevice(channel); if (device != SIDEVICE_NONE) g_SISyncClass.ConnectLocalDevice(channel, g_SISyncClass.PushSubtype(device)); } @@ -634,6 +635,11 @@ void UpdateDevices() g_StatusReg.RDST3 = !!g_Channel[3].m_pDevice->GetData(g_Channel[3].m_InHi.Hex, g_Channel[3].m_InLo.Hex); UpdateInterrupts(); + + // This doesn't really have to be synced with SI, but we do it here to + // minimize the time before packets are sent out + IOSync::g_Backend->NewLocalSubframe(); + } void RunSIBuffer() diff --git a/Source/Core/Core/Src/HW/SI_Device.h b/Source/Core/Core/Src/HW/SI_Device.h index 99319572072e..212d1d6bc971 100644 --- a/Source/Core/Core/Src/HW/SI_Device.h +++ b/Source/Core/Core/Src/HW/SI_Device.h @@ -62,6 +62,7 @@ class SISyncClass : public IOSync::Class SISyncClass() : IOSync::Class(ClassSI) {} virtual void OnConnected(int index, PWBuffer&& subtype) override; virtual void OnDisconnected(int index) override; + virtual int GetMaxDeviceIndex() override { return 4; } }; extern SISyncClass g_SISyncClass; diff --git a/Source/Core/Core/Src/HW/SI_DeviceAMBaseboard.cpp b/Source/Core/Core/Src/HW/SI_DeviceAMBaseboard.cpp index 4cbc42d9eaaf..df063519d9bd 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceAMBaseboard.cpp +++ b/Source/Core/Core/Src/HW/SI_DeviceAMBaseboard.cpp @@ -438,14 +438,16 @@ void CSIDevice_AMBaseboard::EnqueueLocalData() { SReport PadStatus; memset(&PadStatus, 0, sizeof(PadStatus)); - Pad::GetStatus(ISIDevice::m_iDeviceNumber, &PadStatus); + Pad::GetStatus(GetLocalIndex(), &PadStatus); g_SISyncClass.EnqueueLocalReport(GetLocalIndex(), PadStatus); } // Not really used on GC-AM bool CSIDevice_AMBaseboard::GetData(u32& _Hi, u32& _Low) { - m_CurrentPadStatus = g_SISyncClass.DequeueReport(ISIDevice::m_iDeviceNumber); + g_SISyncClass.DequeueReport(ISIDevice::m_iDeviceNumber, [=](SReport&& report) { + m_CurrentPadStatus = report; + }); _Low = 0; _Hi = 0x00800000; diff --git a/Source/Core/Core/Src/HW/SI_DeviceGCController.cpp b/Source/Core/Core/Src/HW/SI_DeviceGCController.cpp index ec7c1c17739a..5dbfb5165b09 100644 --- a/Source/Core/Core/Src/HW/SI_DeviceGCController.cpp +++ b/Source/Core/Core/Src/HW/SI_DeviceGCController.cpp @@ -104,7 +104,7 @@ void CSIDevice_GCController::EnqueueLocalData() { SReport PadStatus; memset(&PadStatus, 0, sizeof(PadStatus)); - Pad::GetStatus(ISIDevice::m_iDeviceNumber, &PadStatus); + Pad::GetStatus(GetLocalIndex(), &PadStatus); g_SISyncClass.EnqueueLocalReport(GetLocalIndex(), PadStatus); } @@ -116,55 +116,58 @@ void CSIDevice_GCController::EnqueueLocalData() // |_ ERR_STATUS (error on last GetData or SendCmd?) bool CSIDevice_GCController::GetData(u32& _Hi, u32& _Low) { - auto PadStatus = g_SISyncClass.DequeueReport(ISIDevice::m_iDeviceNumber); - _Hi = MapPadStatus(PadStatus); + _Hi = 0x80000000; + _Low = 0; + g_SISyncClass.DequeueReport(ISIDevice::m_iDeviceNumber, [&](SReport&& PadStatus) { + _Hi = MapPadStatus(PadStatus); - // Low bits are packed differently per mode - if (m_Mode == 0 || m_Mode == 5 || m_Mode == 6 || m_Mode == 7) - { - _Low = (u8)(PadStatus.analogB >> 4); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.analogA >> 4) << 4); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.triggerRight >> 4) << 8); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.triggerLeft >> 4) << 12); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.substickY) << 16); // All 8 bits - _Low |= (u32)((u8)(PadStatus.substickX) << 24); // All 8 bits - } - else if (m_Mode == 1) - { - _Low = (u8)(PadStatus.analogB >> 4); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.analogA >> 4) << 4); // Top 4 bits - _Low |= (u32)((u8)PadStatus.triggerRight << 8); // All 8 bits - _Low |= (u32)((u8)PadStatus.triggerLeft << 16); // All 8 bits - _Low |= (u32)((u8)PadStatus.substickY << 24); // Top 4 bits - _Low |= (u32)((u8)PadStatus.substickX << 28); // Top 4 bits - } - else if (m_Mode == 2) - { - _Low = (u8)(PadStatus.analogB); // All 8 bits - _Low |= (u32)((u8)(PadStatus.analogA) << 8); // All 8 bits - _Low |= (u32)((u8)(PadStatus.triggerRight >> 4) << 16); // Top 4 bits - _Low |= (u32)((u8)(PadStatus.triggerLeft >> 4) << 20); // Top 4 bits - _Low |= (u32)((u8)PadStatus.substickY << 24); // Top 4 bits - _Low |= (u32)((u8)PadStatus.substickX << 28); // Top 4 bits - } - else if (m_Mode == 3) - { - // Analog A/B are always 0 - _Low = (u8)PadStatus.triggerRight; // All 8 bits - _Low |= (u32)((u8)PadStatus.triggerLeft << 8); // All 8 bits - _Low |= (u32)((u8)PadStatus.substickY << 16); // All 8 bits - _Low |= (u32)((u8)PadStatus.substickX << 24); // All 8 bits - } - else if (m_Mode == 4) - { - _Low = (u8)(PadStatus.analogB); // All 8 bits - _Low |= (u32)((u8)(PadStatus.analogA) << 8); // All 8 bits - // triggerLeft/Right are always 0 - _Low |= (u32)((u8)PadStatus.substickY << 16); // All 8 bits - _Low |= (u32)((u8)PadStatus.substickX << 24); // All 8 bits - } + // Low bits are packed differently per mode + if (m_Mode == 0 || m_Mode == 5 || m_Mode == 6 || m_Mode == 7) + { + _Low = (u8)(PadStatus.analogB >> 4); // Top 4 bits + _Low |= (u32)((u8)(PadStatus.analogA >> 4) << 4); // Top 4 bits + _Low |= (u32)((u8)(PadStatus.triggerRight >> 4) << 8); // Top 4 bits + _Low |= (u32)((u8)(PadStatus.triggerLeft >> 4) << 12); // Top 4 bits + _Low |= (u32)((u8)(PadStatus.substickY) << 16); // All 8 bits + _Low |= (u32)((u8)(PadStatus.substickX) << 24); // All 8 bits + } + else if (m_Mode == 1) + { + _Low = (u8)(PadStatus.analogB >> 4); // Top 4 bits + _Low |= (u32)((u8)(PadStatus.analogA >> 4) << 4); // Top 4 bits + _Low |= (u32)((u8)PadStatus.triggerRight << 8); // All 8 bits + _Low |= (u32)((u8)PadStatus.triggerLeft << 16); // All 8 bits + _Low |= (u32)((u8)PadStatus.substickY << 24); // Top 4 bits + _Low |= (u32)((u8)PadStatus.substickX << 28); // Top 4 bits + } + else if (m_Mode == 2) + { + _Low = (u8)(PadStatus.analogB); // All 8 bits + _Low |= (u32)((u8)(PadStatus.analogA) << 8); // All 8 bits + _Low |= (u32)((u8)(PadStatus.triggerRight >> 4) << 16); // Top 4 bits + _Low |= (u32)((u8)(PadStatus.triggerLeft >> 4) << 20); // Top 4 bits + _Low |= (u32)((u8)PadStatus.substickY << 24); // Top 4 bits + _Low |= (u32)((u8)PadStatus.substickX << 28); // Top 4 bits + } + else if (m_Mode == 3) + { + // Analog A/B are always 0 + _Low = (u8)PadStatus.triggerRight; // All 8 bits + _Low |= (u32)((u8)PadStatus.triggerLeft << 8); // All 8 bits + _Low |= (u32)((u8)PadStatus.substickY << 16); // All 8 bits + _Low |= (u32)((u8)PadStatus.substickX << 24); // All 8 bits + } + else if (m_Mode == 4) + { + _Low = (u8)(PadStatus.analogB); // All 8 bits + _Low |= (u32)((u8)(PadStatus.analogA) << 8); // All 8 bits + // triggerLeft/Right are always 0 + _Low |= (u32)((u8)PadStatus.substickY << 16); // All 8 bits + _Low |= (u32)((u8)PadStatus.substickX << 24); // All 8 bits + } - HandleButtonCombos(PadStatus); + HandleButtonCombos(PadStatus); + }); return true; } diff --git a/Source/Core/Core/Src/IOSync.cpp b/Source/Core/Core/Src/IOSync.cpp index 609c02eccd01..4611e27d2769 100644 --- a/Source/Core/Core/Src/IOSync.cpp +++ b/Source/Core/Core/Src/IOSync.cpp @@ -4,7 +4,7 @@ namespace IOSync { -void Class::SetIndex(int localIndex, int index) +void Class::SetIndex(int index, int localIndex) { if (localIndex != -1) { @@ -42,7 +42,7 @@ void Class::DeviceInfo::DoState(PointerWrap& p) void Class::DoState(PointerWrap& p) { - for (int i = 0; i< MaxDeviceIndex; i++) + for (int i = 0; i < MaxDeviceIndex; i++) { m_Local[i].DoState(p); m_Remote[i].DoState(p); @@ -50,6 +50,12 @@ void Class::DoState(PointerWrap& p) } void Init() +{ + if (!g_Backend) + ResetBackend(); +} + +void ResetBackend() { g_Backend.reset(new BackendLocal()); } diff --git a/Source/Core/Core/Src/IOSync.h b/Source/Core/Core/Src/IOSync.h index 0f9b16fdbfde..04b3bf9ef4e7 100644 --- a/Source/Core/Core/Src/IOSync.h +++ b/Source/Core/Core/Src/IOSync.h @@ -24,10 +24,11 @@ class Backend virtual void ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf) = 0; virtual void DisconnectLocalDevice(int classId, int localIndex) = 0; virtual void EnqueueLocalReport(int classId, int localIndex, PWBuffer&& buf) = 0; - virtual PWBuffer DequeueReport(int classId, int index) = 0; + virtual Packet DequeueReport(int classId, int index, bool* keepGoing) = 0; virtual void OnPacketError() = 0; virtual u32 GetTime() = 0; virtual void DoState(PointerWrap& p) = 0; + virtual void NewLocalSubframe() {} }; class Class @@ -51,10 +52,11 @@ class Class g_Classes[classId] = this; } - // Are reports needed for this local device? - bool IsInUse(int localIndex) + // Emulation code should only use this to test if a device is + // in use as an optimization, if necessary. + int GetRemoteIndex(int localIndex) { - return m_Local[localIndex].m_OtherIndex != -1; + return m_Local[localIndex].m_OtherIndex; } // Gets the local index, if any, corresponding to this remote device, or -1 @@ -64,7 +66,7 @@ class Class return m_Remote[index].m_OtherIndex; } - void SetIndex(int localIndex, int index); + void SetIndex(int index, int localIndex); // Make a local device available. // subtypeData is data that does not change during the life of the device, @@ -103,25 +105,33 @@ class Class return &m_Local[index].m_Subtype; } - bool IsConnected(int index) + const bool& LocalIsConnected(int index) + { + return m_Local[index].m_IsConnected; + } + + const bool& IsConnected(int index) { return m_Remote[index].m_IsConnected; } - template - Report DequeueReport(int index) + template + void DequeueReport(int index, Callback cb) { - while (1) + bool keepGoing = true; + while (keepGoing) { - Packet p(g_Backend->DequeueReport(m_ClassId, index)); + Packet p = g_Backend->DequeueReport(m_ClassId, index, &keepGoing); Report reportData; + if (p.vec->empty()) + break; reportData.DoReport(p); if (p.failure) { g_Backend->OnPacketError(); continue; } - return reportData; + cb(std::move(reportData)); } } @@ -148,6 +158,7 @@ class Class virtual void OnConnected(int index, PWBuffer&& subtype); virtual void OnDisconnected(int index); virtual void DoState(PointerWrap& p); + virtual int GetMaxDeviceIndex() = 0; private: struct DeviceInfo // local or remote @@ -168,6 +179,7 @@ class Class void Init(); +void ResetBackend(); void DoState(PointerWrap& p); } diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index b32c762bb5e5..7bf3b6c79d9a 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -1,5 +1,7 @@ #include "IOSyncBackends.h" #include "Timer.h" +#include "CoreTiming.h" +#include "HW/SystemTimers.h" namespace IOSync { @@ -12,17 +14,22 @@ void BackendLocal::ConnectLocalDevice(int classId, int localIndex, PWBuffer&& bu void BackendLocal::DisconnectLocalDevice(int classId, int localIndex) { + g_Classes[classId]->SetIndex(-1, localIndex); g_Classes[classId]->OnDisconnected(localIndex); } void BackendLocal::EnqueueLocalReport(int classId, int localIndex, PWBuffer&& buf) { - m_Reports[classId][localIndex] = std::move(buf); + m_ReportQueue[classId][localIndex].push_back(std::move(buf)); } -PWBuffer BackendLocal::DequeueReport(int classId, int index) +Packet BackendLocal::DequeueReport(int classId, int index, bool* keepGoing) { - return std::move(m_Reports[classId][index]); + *keepGoing = false; + auto& rq = m_ReportQueue[classId][index]; + PWBuffer result = std::move(rq.front()); + rq.pop_front(); + return Packet(std::move(result)); } void BackendLocal::OnPacketError() @@ -56,4 +63,206 @@ void BackendLocal::DoState(PointerWrap& p) } } +BackendNetPlay::BackendNetPlay(NetPlayClient* client, u32 delay) +{ + m_Client = client; + m_SubframeId = -1; + m_Delay = delay; + NewLocalSubframe(); +} + +void BackendNetPlay::ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf) +{ + WARN_LOG(NETPLAY, "Local connection class %d device %d", classId, localIndex); + //Packet* pac = m_Client->GetPacketToSendLater(); + Packet pac; + pac.W((MessageId) NP_MSG_CONNECT_DEVICE); + pac.W((u8) classId); + pac.W((u8) localIndex); + pac.W((u8) 0); // flags + pac.W((PlayerId) 0); // dummy + pac.W((u8) 0); // dummy + pac.vec->append(buf); + m_Client->SendPacket(std::move(pac)); +} + +void BackendNetPlay::DisconnectLocalDevice(int classId, int localIndex) +{ + WARN_LOG(NETPLAY, "Local disconnection class %d device %d", classId, localIndex); + g_Classes[classId]->SetIndex(-1, localIndex); + + //Packet* pac = m_Client->GetPacketToSendLater(); + Packet pac; + pac.W((MessageId) NP_MSG_DISCONNECT_DEVICE); + pac.W((u8) classId); + pac.W((u8) localIndex); + pac.W((u8) 0); // flags + m_Client->SendPacket(std::move(pac)); +} + +void BackendNetPlay::EnqueueLocalReport(int classId, int localIndex, PWBuffer&& buf) +{ + /* + printf("sending ..."); + for (int i = 0; i < buf.size(); i++) + printf("%02x ", *(u8 *) (buf.data() + i)); + printf("\n"); + */ + //Packet* pac = m_Client->GetPacketToSendLater(); + int ri = g_Classes[classId]->GetRemoteIndex(localIndex); + if (ri == -1) + return; + Packet pac; + pac.W((MessageId) NP_MSG_REPORT); + pac.W((u8) classId); + pac.W((u8) ri); + auto& last = m_DeviceInfo[classId][localIndex].m_LastSentSubframeId; + u8 skippedFrames = m_SubframeId - last; + last = m_SubframeId; + pac.W(skippedFrames); + pac.vec->append(buf); + // server won't send our own reports back to us + ProcessPacket(pac.vec->copy()); + m_Client->SendPacket(std::move(pac)); +} + +Packet BackendNetPlay::DequeueReport(int classId, int index, bool* keepGoing) +{ + auto& deviceInfo = m_DeviceInfo[classId][index]; + const bool& isConnected = g_Classes[classId]->IsConnected(index); + while (1) + { + if (!isConnected || deviceInfo.m_SubframeId > m_PastSubframeId) + { + *keepGoing = false; + return PWBuffer(); + } + auto& queue = deviceInfo.m_IncomingQueue; + if (!queue.empty()) + { + Packet& p = queue.front(); + u8 skippedFrames; + p.Do(skippedFrames); + if (deviceInfo.m_SubframeId + skippedFrames > m_PastSubframeId) + { + p.readOff--; + *keepGoing = false; + return PWBuffer(); + } + deviceInfo.m_SubframeId += skippedFrames; + *keepGoing = deviceInfo.m_SubframeId < m_PastSubframeId; + Packet q = std::move(p); + queue.pop_front(); + return q; + } + ProcessIncomingPackets(); + } +} + +void BackendNetPlay::OnPacketError() +{ + WARN_LOG(NETPLAY, "NetPlay packet error"); + m_Client->OnPacketErrorFromIOSync(); +} + +u32 BackendNetPlay::GetTime() +{ + return NETPLAY_INITIAL_GCTIME + CoreTiming::GetTicks() / SystemTimers::GetTicksPerSecond(); +} + +void BackendNetPlay::DoState(PointerWrap& p) +{ + if (p.GetMode() == PointerWrap::MODE_READ) + PanicAlert("No state loading in Netplay yet..."); +} + +void BackendNetPlay::OnPacketReceived(Packet&& packet) +{ + m_PacketsPendingProcessing.Push(std::move(packet)); +} + +void BackendNetPlay::ProcessIncomingPackets() +{ + Packet p; + while (m_PacketsPendingProcessing.Pop(p)) + { + ProcessPacket(std::move(p)); + } +} + +void BackendNetPlay::ProcessPacket(Packet&& p) +{ + MessageId packetType; + u8 classId, index, flags; + p.Do(packetType); + if (packetType != NP_MSG_PAD_BUFFER) + { + p.Do(classId); + p.Do(index); + p.Do(flags); + if (p.failure || + classId >= Class::NumClasses || + index >= g_Classes[classId]->GetMaxDeviceIndex()) + { + OnPacketError(); + return; + } + } + switch (packetType) + { + case NP_MSG_CONNECT_DEVICE: + { + PlayerId localPlayer; + u8 localIndex; + p.Do(localPlayer); + p.Do(localIndex); + if (localIndex >= g_Classes[classId]->GetMaxDeviceIndex()) + { + OnPacketError(); + return; + } + WARN_LOG(NETPLAY, "Connecting remote class %u device %u with local %u/pid%u", classId, index, localIndex, localPlayer); + auto& di = m_DeviceInfo[classId][index] = DeviceInfo(); + di.m_LastSentSubframeId = di.m_SubframeId = m_SubframeId; + g_Classes[classId]->SetIndex(index, localPlayer == m_Client->m_pid ? localIndex : -1); + g_Classes[classId]->OnConnected(index, PWBuffer(p.vec->data() + p.readOff, p.vec->size() - p.readOff)); + break; + } + case NP_MSG_DISCONNECT_DEVICE: + { + WARN_LOG(NETPLAY, "Disconnecting remote class %u device %u", classId, index); + m_DeviceInfo[classId][index] = DeviceInfo(); + g_Classes[classId]->SetIndex(index, -1); + if (g_Classes[classId]->IsConnected(index)) + g_Classes[classId]->OnDisconnected(index); + break; + } + case NP_MSG_REPORT: + { + p.readOff--; // go back to flags + m_DeviceInfo[classId][index].m_IncomingQueue.push_back(std::move(p)); + break; + } + case NP_MSG_PAD_BUFFER: + { + u32 delay; + p.Do(delay); + // XXX - it should be possible to have a half-frame delay. + m_Delay = delay * 2; + break; + } + default: + // can't happen + break; + } +} + +void BackendNetPlay::NewLocalSubframe() +{ + m_SubframeId++; + m_PastSubframeId = m_SubframeId - m_Delay; + // If we have nothing connected, we need to process the queue here. + ProcessIncomingPackets(); +} + } diff --git a/Source/Core/Core/Src/IOSyncBackends.h b/Source/Core/Core/Src/IOSyncBackends.h index 02511595de93..d0bff8f46b9f 100644 --- a/Source/Core/Core/Src/IOSyncBackends.h +++ b/Source/Core/Core/Src/IOSyncBackends.h @@ -5,6 +5,10 @@ #pragma once #include "IOSync.h" +#include "NetPlayClient.h" +#include + +// TODO: what happens to packets the server receives after disconnect? namespace IOSync { @@ -16,13 +20,56 @@ class BackendLocal : public Backend virtual void ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf) override; virtual void DisconnectLocalDevice(int classId, int localIndex) override; virtual void EnqueueLocalReport(int classId, int localIndex, PWBuffer&& buf) override; - virtual PWBuffer DequeueReport(int classId, int index) override; + virtual Packet DequeueReport(int classId, int index, bool* keepGoing) override; virtual void OnPacketError() override; virtual u32 GetTime() override; virtual void DoState(PointerWrap& p) override; private: - PWBuffer m_Reports[Class::NumClasses][Class::MaxDeviceIndex]; + std::deque m_ReportQueue[Class::NumClasses][Class::MaxDeviceIndex]; }; +class BackendNetPlay : public Backend +{ +public: + BackendNetPlay(NetPlayClient* client, u32 delay); + virtual void ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf) override; + virtual void DisconnectLocalDevice(int classId, int localIndex) override; + virtual void EnqueueLocalReport(int classId, int localIndex, PWBuffer&& buf) override; + virtual Packet DequeueReport(int classId, int index, bool* keepGoing) override; + virtual void OnPacketError() override; + virtual u32 GetTime() override; + virtual void DoState(PointerWrap& p) override; + + // from netplay + void OnPacketReceived(Packet&& packet) ON(NET); + // from (arbitrarily-ish) SI + virtual void NewLocalSubframe() override; +private: + struct DeviceInfo + { + DeviceInfo() + { + m_SubframeId = 0; + m_LastSentSubframeId = 0; + } + std::deque m_IncomingQueue; + s64 m_SubframeId; + s64 m_LastSentSubframeId; + }; + + void ProcessIncomingPackets(); + void ProcessPacket(Packet&& p); + void UpdateDelay(u32 delay); + + NetPlayClient* m_Client; + // this is split up to avoid unnecessary copying in The Future + Common::FifoQueue m_PacketsPendingProcessing; + s64 m_SubframeId; + // We accept packets sent before this frame. + s64 m_PastSubframeId; + u32 m_Delay; + // indexed by remote device + DeviceInfo m_DeviceInfo[Class::NumClasses][Class::MaxDeviceIndex]; +}; } diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 7a187a7e1c72..cdfd02d30429 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include "NetPlayClient.h" +#include "IOSyncBackends.h" // for wiimote #include "HW/WiimoteReal/WiimoteReal.h" @@ -36,16 +37,13 @@ NetPlayClient::~NetPlayClient() NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& name, std::function stateCallback) { m_is_running = false; - m_target_buffer_size = 20; + m_delay = 20; m_state = Failure; m_dialog = NULL; m_host = NULL; m_state_callback = stateCallback; m_local_name = name; - -#if 0 - ClearBuffers(); -#endif + m_backend = NULL; size_t pos = hostSpec.find(':'); if (pos != std::string::npos) @@ -105,6 +103,14 @@ void NetPlayClient::SendPacket(Packet&& packet) }); } +void NetPlayClient::OnPacketErrorFromIOSync() +{ + g_TraversalClient->RunOnThread([=]() { + ASSUME_ON(NET); + OnDisconnect(InvalidPacket); + }); +} + void NetPlayClient::OnData(Packet&& packet) { if (m_state == WaitingForHelloResponse) @@ -138,11 +144,14 @@ void NetPlayClient::OnData(Packet&& packet) else if(m_state != Connected) return; + size_t oldOff = packet.readOff; + MessageId mid; packet.Do(mid); if (packet.failure) return OnDisconnect(InvalidPacket); + size_t offset; switch (mid) { case NP_MSG_PLAYER_JOIN : @@ -221,71 +230,27 @@ void NetPlayClient::OnData(Packet&& packet) } break; -#if 0 - case NP_MSG_PAD_MAPPING : - { - packet.DoArray(m_pad_map, 4); - - if (packet.failure) - return OnDisconnect(InvalidPacket); - - UpdateDevices(); - - m_dialog->Update(); - } - break; - - case NP_MSG_WIIMOTE_MAPPING : + case NP_MSG_PAD_BUFFER: { - packet.DoArray(m_wiimote_map, 4); - + packet.Do(m_delay); if (packet.failure) return OnDisconnect(InvalidPacket); - - m_dialog->Update(); - } - break; - - case NP_MSG_PAD_DATA : - { - PadMapping map; - NetPad np; - packet.Do(map); - packet.Do(np.nHi); - packet.Do(np.nLo); - if (packet.failure || map < 0 || map >= 4) - return OnDisconnect(InvalidPacket); - - // add to pad buffer - m_pad_buffer[map].Push(np); - } - break; - - case NP_MSG_WIIMOTE_DATA : - { - PadMapping map; - NetWiimote nw; - packet.Do(map); - packet.Do(nw); - if (packet.failure || map < 0 || map >= 4) - return OnDisconnect(InvalidPacket); - - // add to wiimote buffer - m_wiimote_buffer[(unsigned)map].Push(std::move(nw)); + offset = 0; + if (!m_backend) + break; + /* fall through */ } - break; -#endif - case NP_MSG_PAD_BUFFER : + case NP_MSG_DISCONNECT_DEVICE: + case NP_MSG_CONNECT_DEVICE: + case NP_MSG_REPORT: { - u32 size = 0; - packet.Do(size); - if (packet.failure) + if (!m_backend) return OnDisconnect(InvalidPacket); - - m_target_buffer_size = size; + packet.readOff = oldOff; + m_backend->OnPacketReceived(std::move(packet)); + break; } - break; case NP_MSG_CHANGE_GAME : { @@ -471,22 +436,15 @@ void NetPlayClient::GetPlayerList(std::string& list, std::vector& pid_list) { const Player *player = &(i->second); ss << player->name << "[" << (int)player->pid << "] : " << player->revision << "\n | "; - #if 0 - for (unsigned int j = 0; j < 4; j++) - { - if (m_pad_map[j] == player->pid) - ss << j + 1; - else - ss << '-'; - } - for (unsigned int j = 0; j < 4; j++) + /* + for (int j = 0; j < 4; j++) { - if (m_wiimote_map[j] == player->pid) + if (m_backend->GetPidForDevice(IOSync::ClassBase::ClassSI, j) == player->pid) ss << j + 1; else ss << '-'; } - #endif + */ ss << " | "; if (player->ping != -1u) ss << player->ping; @@ -538,31 +496,6 @@ void NetPlayClient::ChangeName(const std::string& name) }); } -#if 0 -void NetPlayClient::SendPadState(const PadMapping in_game_pad, const NetPad& np) -{ - // send to server - Packet packet; - packet.W((MessageId)NP_MSG_PAD_DATA); - packet.W(in_game_pad); - packet.W(np.nHi); - packet.W(np.nLo); - - SendPacket(packet); -} - -void NetPlayClient::SendWiimoteState(const PadMapping in_game_pad, const NetWiimote& nw) -{ - // send to server - Packet packet; - packet.W((MessageId)NP_MSG_WIIMOTE_DATA); - packet.W(in_game_pad); - packet.W(nw); - - SendPacket(packet); -} -#endif - bool NetPlayClient::StartGame(const std::string &path) { if (m_is_running) @@ -587,35 +520,12 @@ bool NetPlayClient::StartGame(const std::string &path) NetPlay_Enable(this); -#if 0 - ClearBuffers(); - - if (m_dialog->IsRecording()) - { - - if (Movie::IsReadOnly()) - Movie::SetReadOnly(false); - - u8 controllers_mask = 0; - for (unsigned int i = 0; i < 4; ++i) - { - if (m_pad_map[i] != -1) - controllers_mask |= (1 << i); - if (m_wiimote_map[i] != -1) - controllers_mask |= (1 << (i + 4)); - } - Movie::BeginRecordingInput(controllers_mask); - } -#endif + IOSync::g_Backend.reset(m_backend = new IOSync::BackendNetPlay(this, m_delay)); // boot game m_dialog->BootGame(path); -#if 0 - UpdateDevices(); -#endif - return true; } @@ -624,212 +534,9 @@ bool NetPlayClient::ChangeGame(const std::string&) return true; } -#if 0 -void NetPlayClient::UpdateDevices() -{ - for (PadMapping i = 0; i < 4; i++) - { - // XXX: add support for other device types? does it matter? - SerialInterface::AddDevice(m_pad_map[i] != -1 ? SIDEVICE_GC_CONTROLLER : SIDEVICE_NONE, i); - } -} - -void NetPlayClient::ClearBuffers() -{ - // clear pad buffers, Clear method isn't thread safe - for (unsigned int i=0; i<4; ++i) - { - while (m_pad_buffer[i].Size()) - m_pad_buffer[i].Pop(); - - while (m_wiimote_buffer[i].Size()) - m_wiimote_buffer[i].Pop(); - } -} - -bool NetPlayClient::GetNetPads(const u8 pad_nb, const SPADStatus* const pad_status, NetPad* const netvalues) -{ - // The interface for this is extremely silly. - // - // Imagine a physical device that links three Gamecubes together - // and emulates NetPlay that way. Which Gamecube controls which - // in-game controllers can be configured on the device (m_pad_map) - // but which sockets on each individual Gamecube should be used - // to control which players? The solution that Dolphin uses is - // that we hardcode the knowledge that they go in order, so if - // you have a 3P game with three gamecubes, then every single - // controller should be plugged into slot 1. - // - // If you have a 4P game, then one of the Gamecubes will have - // a controller plugged into slot 1, and another in slot 2. - // - // The slot number is the "local" pad number, and what player - // it actually means is the "in-game" pad number. - // - // The interface here gives us the status of local pads, and - // expects to get back "in-game" pad numbers back in response. - // e.g. it asks "here's the input that slot 1 has, and by the - // way, what's the state of P1?" - // - // We should add this split between "in-game" pads and "local" - // pads higher up. - - int in_game_num = LocalPadToInGamePad(pad_nb); - - // If this in-game pad is one of ours, then update from the - // information given. - if (in_game_num < 4) - { - NetPad np(pad_status); - - // adjust the buffer either up or down - // inserting multiple padstates or dropping states - while (m_pad_buffer[in_game_num].Size() <= m_target_buffer_size) - { - // add to buffer - m_pad_buffer[in_game_num].Push(np); - - // send - SendPadState(in_game_num, np); - } - } - - // Now, we need to swap out the local value with the values - // retrieved from NetPlay. This could be the value we pushed - // above if we're configured as P1 and the code is trying - // to retrieve data for slot 1. - while (!m_pad_buffer[pad_nb].Pop(*netvalues)) - { - if (!m_is_running) - return false; - - // TODO: use a condition instead of sleeping - Common::SleepCurrentThread(1); - } - - SPADStatus tmp; - tmp.stickY = ((u8*)&netvalues->nHi)[0]; - tmp.stickX = ((u8*)&netvalues->nHi)[1]; - tmp.button = ((u16*)&netvalues->nHi)[1]; - - tmp.substickX = ((u8*)&netvalues->nLo)[3]; - tmp.substickY = ((u8*)&netvalues->nLo)[2]; - tmp.triggerLeft = ((u8*)&netvalues->nLo)[1]; - tmp.triggerRight = ((u8*)&netvalues->nLo)[0]; - -#if 0 - if (Movie::IsRecordingInput()) - { - Movie::RecordInput(&tmp, pad_nb); - Movie::InputUpdate(); - } - else - { - Movie::CheckPadStatus(&tmp, pad_nb); - } -#endif - - return true; -} - - -bool NetPlayClient::WiimoteUpdate(int _number, u8* data, const u8 size) -{ - NetWiimote nw; - static u8 previousSize[4] = {4,4,4,4}; - { - std::lock_guard lk(m_crit); - - // in game mapping for this local wiimote - unsigned int in_game_num = LocalWiimoteToInGameWiimote(_number); - // does this local wiimote map in game? - if (in_game_num < 4) - { - if (previousSize[in_game_num] == size) - { - nw.assign(data, data + size); - do - { - // add to buffer - m_wiimote_buffer[in_game_num].Push(nw); - - SendWiimoteState(in_game_num, nw); - } while (m_wiimote_buffer[in_game_num].Size() <= m_target_buffer_size * 200 / 120); // TODO: add a seperate setting for wiimote buffer? - } - else - { - while (m_wiimote_buffer[in_game_num].Size() > 0) - { - // Reporting mode changed, so previous buffer is no good. - m_wiimote_buffer[in_game_num].Pop(); - } - nw.resize(size, 0); - - m_wiimote_buffer[in_game_num].Push(nw); - m_wiimote_buffer[in_game_num].Push(nw); - m_wiimote_buffer[in_game_num].Push(nw); - m_wiimote_buffer[in_game_num].Push(nw); - m_wiimote_buffer[in_game_num].Push(nw); - m_wiimote_buffer[in_game_num].Push(nw); - previousSize[in_game_num] = size; - } - } - - } // unlock players - - while (previousSize[_number] == size && !m_wiimote_buffer[_number].Pop(nw)) - { - // wait for receiving thread to push some data - Common::SleepCurrentThread(1); - if (false == m_is_running) - return false; - } - - // Use a blank input, since we may not have any valid input. - if (previousSize[_number] != size) - { - nw.resize(size, 0); - m_wiimote_buffer[_number].Push(nw); - m_wiimote_buffer[_number].Push(nw); - m_wiimote_buffer[_number].Push(nw); - m_wiimote_buffer[_number].Push(nw); - m_wiimote_buffer[_number].Push(nw); - } - - // We should have used a blank input last time, so now we just need to pop through the old buffer, until we reach a good input - if (nw.size() != size) - { - u8 tries = 0; - // Clear the buffer and wait for new input, since we probably just changed reporting mode. - while (nw.size() != size) - { - while (!m_wiimote_buffer[_number].Pop(nw)) - { - Common::SleepCurrentThread(1); - if (false == m_is_running) - return false; - } - ++tries; - if (tries > m_target_buffer_size * 200 / 120) - break; - } - - // If it still mismatches, it surely desynced - if (size != nw.size()) - { - PanicAlert("Netplay has desynced. There is no way to recover from this."); - return false; - } - } - - previousSize[_number] = size; - memcpy(data, nw.data(), size); - return true; -} -#endif - bool NetPlayClient::StopGame() { + // XXX - this is weird std::lock_guard lk(m_crit); if (!m_is_running) @@ -838,6 +545,10 @@ bool NetPlayClient::StopGame() return false; } + // reset the IOSync backend + IOSync::ResetBackend(); + m_backend = NULL; + m_dialog->AppendChat(" -- STOPPING GAME -- "); m_is_running = false; @@ -849,140 +560,21 @@ bool NetPlayClient::StopGame() return true; } +/* +static bool Has + for (int c = 0; c < IOSync::ClassBase::NumClasses; c++) + for (int i = 0; i < IOSync::ClassBase::MaxDeviceIndex; i++) +*/ + void NetPlayClient::Stop() { if (m_is_running == false) return; -#if 0 - g_TraversalClient->RunOnThread([=]() mutable { - bool isPadMapped = false; - ASSUME_ON(NET); - for (unsigned int i = 0; i < 4; ++i) - { - if (m_pad_map[i] == m_pid) - isPadMapped = true; - } - for (unsigned int i = 0; i < 4; ++i) - { - if (m_wiimote_map[i] == m_pid) - isPadMapped = true; - } - // tell the server to stop if we have a pad mapped in game. - if (isPadMapped) - { - Packet packet; - packet.W((MessageId)NP_MSG_STOP_GAME); - ENetUtil::BroadcastPacket(m_host, packet); - } - }); -#endif + abort(); + // if we have a pad, then tell the server to stop (need a dialog about + // this); else just quit netplay } -#if 0 -u8 NetPlayClient::InGamePadToLocalPad(u8 ingame_pad) -{ - // not our pad - if (m_pad_map[ingame_pad] != m_pid) - return 4; - - int local_pad = 0; - int pad = 0; - - for (; pad < ingame_pad; pad++) - { - if (m_pad_map[pad] == m_pid) - local_pad++; - } - - return local_pad; -} - -u8 NetPlayClient::LocalPadToInGamePad(u8 local_pad) -{ - // Figure out which in-game pad maps to which local pad. - // The logic we have here is that the local slots always - // go in order. - int local_pad_count = -1; - int ingame_pad = 0; - for (; ingame_pad < 4; ingame_pad++) - { - if (m_pad_map[ingame_pad] == m_pid) - local_pad_count++; - - if (local_pad_count == local_pad) - break; - } - - return ingame_pad; -} - -u8 NetPlayClient::LocalWiimoteToInGameWiimote(u8 local_pad) -{ - // Figure out which in-game pad maps to which local pad. - // The logic we have here is that the local slots always - // go in order. - int local_pad_count = -1; - int ingame_pad = 0; - for (; ingame_pad < 4; ingame_pad++) - { - if (m_wiimote_map[ingame_pad] == m_pid) - local_pad_count++; - - if (local_pad_count == local_pad) - break; - } - - return ingame_pad; -} - -// stuff hacked into dolphin - -// called from ---CPU--- thread -// Actual Core function which is called on every frame -bool CSIDevice_GCController::NetPlay_GetInput(u8 numPAD, SPADStatus PadStatus, u32 *PADStatus) -{ - std::lock_guard lk(crit_netplay_client); - - if (netplay_client) - return netplay_client->GetNetPads(numPAD, &PadStatus, (NetPad*)PADStatus); - else - return false; -} - -bool WiimoteEmu::Wiimote::NetPlay_GetWiimoteData(int wiimote, u8* data, u8 size) -{ - std::lock_guard lk(crit_netplay_client); - - if (netplay_client) - return netplay_client->WiimoteUpdate(wiimote, data, size); - else - return false; -} - -// called from ---CPU--- thread -// so all players' games get the same time -u32 CEXIIPL::NetPlay_GetGCTime() -{ - std::lock_guard lk(crit_netplay_client); - - if (netplay_client) - return NETPLAY_INITIAL_GCTIME; // watev - else - return 0; -} - -// called from ---CPU--- thread -// return the local pad num that should rumble given a ingame pad num -u8 CSIDevice_GCController::NetPlay_InGamePadToLocalPad(u8 numPAD) -{ - std::lock_guard lk(crit_netplay_client); - - if (netplay_client) - return netplay_client->InGamePadToLocalPad(numPAD); - else - return numPAD; -} -#endif bool NetPlay::IsNetPlayRunning() { diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 4f7a6ba17ea8..785a5f500780 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -22,6 +22,11 @@ #include "FifoQueue.h" #include "TraversalClient.h" +namespace IOSync +{ + class BackendNetPlay; +} + class NetPlayUI { public: @@ -83,16 +88,7 @@ class NetPlayClient : public TraversalClientClient void SendChatMessage(const std::string& msg) /* ON(GUI) */; void ChangeName(const std::string& name) /* ON(GUI) */; - #if 0 - // Send and receive pads values - bool WiimoteUpdate(int _number, u8* data, const u8 size) /* ON(CPU) */; - bool GetNetPads(const u8 pad_nb, const SPADStatus* const, NetPad* const netvalues) /* ON(CPU) */; - - u8 LocalPadToInGamePad(u8 localPad); - u8 InGamePadToLocalPad(u8 localPad); - - u8 LocalWiimoteToInGameWiimote(u8 local_pad); - #endif + void SendPacketFromIOSync(Packet&& pac) /* ON(CPU) */; void SetDialog(NetPlayUI* dialog); @@ -101,12 +97,13 @@ class NetPlayClient : public TraversalClientClient virtual void OnConnectReady(ENetAddress addr) override ON(NET); virtual void OnConnectFailed(u8 reason) override ON(NET); + // temporarily public, to replace with GetPacketToSendLater + void SendPacket(Packet&& packet); + void OnPacketErrorFromIOSync(); + std::function m_state_callback; + PlayerId m_pid; protected: - #if 0 - void ClearBuffers() /* on multiple */; - #endif - std::recursive_mutex m_crit; NetPlayUI* m_dialog; @@ -118,32 +115,23 @@ class NetPlayClient : public TraversalClientClient std::string m_selected_game ACCESS_ON(NET); volatile bool m_is_running; - unsigned int m_target_buffer_size; + // frame delay + u32 m_delay; Player* m_local_player GUARDED_BY(m_crit); std::string m_local_name ACCESS_ON(NET); - u32 m_current_game; + IOSync::BackendNetPlay* m_backend; - #if 0 - PadMapping m_pad_map[4]; - PadMapping m_wiimote_map[4]; - #endif + u32 m_current_game; bool m_is_recording; private: - #if 0 - void UpdateDevices() /* on multiple, this sucks */; - void SendPadState(const PadMapping in_game_pad, const NetPad& np) /* ON(CPU) */; - void SendWiimoteState(const PadMapping in_game_pad, const NetWiimote& nw) /* ON(CPU) */; - #endif void OnData(Packet&& packet) ON(NET); void OnDisconnect(int reason) ON(NET); - void SendPacket(Packet&& packet); void DoDirectConnect(const ENetAddress& addr); - PlayerId m_pid; std::map m_players GUARDED_BY(m_crit); std::unique_ptr m_host_client; Common::Event m_have_dialog_event; diff --git a/Source/Core/Core/Src/NetPlayProto.h b/Source/Core/Core/Src/NetPlayProto.h index a09bc5bc25e9..44a4e20afa3e 100644 --- a/Source/Core/Core/Src/NetPlayProto.h +++ b/Source/Core/Core/Src/NetPlayProto.h @@ -40,13 +40,10 @@ enum NP_MSG_CHANGE_NAME = 0x31, NP_MSG_PAD_BUFFER = 0x60, - /* - NP_MSG_PAD_DATA = 0x60, - NP_MSG_PAD_MAPPING = 0x61, - NP_MSG_WIIMOTE_DATA = 0x70, - NP_MSG_WIIMOTE_MAPPING = 0x71, - */ + NP_MSG_CONNECT_DEVICE = 0x61, + NP_MSG_DISCONNECT_DEVICE = 0x62, + NP_MSG_REPORT = 0x63, NP_MSG_START_GAME = 0xA0, NP_MSG_CHANGE_GAME = 0xA1, diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 83f040f00a05..72cc33d2a3df 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -285,11 +285,6 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) } } -#if 0 - UpdatePadMapping(); // sync pad mappings with everyone - UpdateWiimoteMapping(); -#endif - return 0; } @@ -302,24 +297,20 @@ void NetPlayServer::OnDisconnect(PlayerId pid) player.connected = false; -#if 0 - if (m_is_running) + for (int c = 0; c < IOSync::Class::NumClasses; c++) { - for (int i = 0; i < 4; i++) + for (int i = 0; i < IOSync::Class::MaxDeviceIndex; i++) { - if (m_pad_map[i] == pid) + if (m_device_map[c][i].first == pid) { - PanicAlertT("Client disconnect while game is running!! NetPlay is disabled. You must manually stop the game."); - m_is_running = false; - Packet opacket; - opacket.W((MessageId)NP_MSG_DISABLE_GAME); + opacket.W((MessageId)NP_MSG_DISCONNECT_DEVICE); + opacket.W((u8)c); + opacket.W((u8)i); SendToClientsOnThread(std::move(opacket)); - break; } } } -#endif Packet opacket; opacket.W((MessageId)NP_MSG_PLAYER_LEAVE); @@ -327,63 +318,7 @@ void NetPlayServer::OnDisconnect(PlayerId pid) // alert other players of disconnect SendToClientsOnThread(std::move(opacket)); - -#if 0 - for (int i = 0; i < 4; i++) - if (m_pad_map[i] == pid) - m_pad_map[i] = -1; - UpdatePadMapping(); - - for (int i = 0; i < 4; i++) - if (m_wiimote_map[i] == pid) - m_wiimote_map[i] = -1; - UpdateWiimoteMapping(); -#endif -} - -#if 0 -void NetPlayServer::GetPadMapping(PadMapping map[4]) -{ - for (int i = 0; i < 4; i++) - map[i] = m_pad_map[i]; -} - -void NetPlayServer::GetWiimoteMapping(PadMapping map[4]) -{ - for (int i = 0; i < 4; i++) - map[i] = m_wiimote_map[i]; -} - -void NetPlayServer::SetPadMapping(const PadMapping map[4]) -{ - for (int i = 0; i < 4; i++) - m_pad_map[i] = map[i]; - UpdatePadMapping(); -} - -void NetPlayServer::SetWiimoteMapping(const PadMapping map[4]) -{ - for (int i = 0; i < 4; i++) - m_wiimote_map[i] = map[i]; - UpdateWiimoteMapping(); -} - -void NetPlayServer::UpdatePadMapping() -{ - Packet opacket; - opacket.W((MessageId)NP_MSG_PAD_MAPPING); - opacket.DoArray(m_pad_map, 4); - SendToClients(std::move(opacket)); -} - -void NetPlayServer::UpdateWiimoteMapping() -{ - Packet opacket; - opacket.W((MessageId)NP_MSG_WIIMOTE_MAPPING); - opacket.DoArray(m_wiimote_map, 4); - SendToClients(std::move(opacket)); } -#endif void NetPlayServer::AdjustPadBufferSize(unsigned int size) { @@ -416,6 +351,7 @@ void NetPlayServer::OnData(PlayerId pid, Packet&& packet) } else { + player.is_localhost = peer->address.host == 0x0100007f; player.connected = true; m_num_players++; } @@ -464,51 +400,96 @@ void NetPlayServer::OnData(PlayerId pid, Packet&& packet) } break; -#if 0 - case NP_MSG_PAD_DATA : + case NP_MSG_CONNECT_DEVICE: + case NP_MSG_DISCONNECT_DEVICE: { - // if this is pad data from the last game still being received, ignore it - if (player.current_game != m_current_game) - break; - - PadMapping map; - u32 hi, lo; - packet.Do(map); - packet.Do(hi); - packet.Do(lo); - if (packet.failure) - return OnDisconnect(pid); - - // If the data is not from the correct player, - // then disconnect them. - if (m_pad_map[map] != pid) + u8 classId, localIndex, flags; + packet.Do(classId); + u8* indexP = (u8*) packet.vec->data() + packet.readOff; + packet.Do(localIndex); + packet.Do(flags); + int limit; + if (packet.failure || + classId >= IOSync::Class::NumClasses || + localIndex >= (limit = IOSync::g_Classes[classId]->GetMaxDeviceIndex())) + { return OnDisconnect(pid); - - // Relay to clients - SendToClients(packet, pid); + } + // todo: bring customization back + std::pair* map = m_device_map[classId]; + if (mid == NP_MSG_CONNECT_DEVICE) + { + PlayerId dummy1; + u8 dummy2; + PlayerId* localPlayerP = (PlayerId*) packet.vec->data() + packet.readOff; + packet.Do(dummy1); + u8* localIndexP = (u8*) packet.vec->data() + packet.readOff; + packet.Do(dummy2); + if (packet.failure) + return OnDisconnect(pid); + + WARN_LOG(NETPLAY, "Server: received CONNECT_DEVICE (%u/%u) from client %u", classId, localIndex, pid); + int i; + for (i = 0; i < limit; i++) + { + if (map[i].second == -1) + { + map[i].first = pid; + map[i].second = localIndex; + *indexP = i; + *localPlayerP = pid; + *localIndexP = localIndex; + WARN_LOG(NETPLAY, " --> assigning %d", i); + SendToClientsOnThread(std::move(packet)); + break; + } + } + if (i == limit) + WARN_LOG(NETPLAY, " --> no assignment"); + // todo: keep track of connected local devices so they can be + // assigned back later + } + else // DISCONNECT + { + WARN_LOG(NETPLAY, "Server: received DISCONNECT_DEVICE (%u/%u) from client %u", classId, localIndex, pid); + for (int i = 0; i < IOSync::Class::MaxDeviceIndex; i++) + { + if (map[i].first == pid && map[i].second == localIndex) + { + map[i].first = map[i].second = -1; + *indexP = i; + SendToClientsOnThread(std::move(packet)); + break; + } + } + } + break; } - break; - case NP_MSG_WIIMOTE_DATA : + + case NP_MSG_REPORT: { - // if this is wiimote data from the last game still being received, ignore it - if (player.current_game != m_current_game) - break; - - PadMapping map; - NetWiimote nw; - packet.Do(map); - packet.Do(nw); - // If the data is not from the correct player, - // then disconnect them. - if (packet.failure || m_wiimote_map[map] != pid) + u8 classId, index, flags; + packet.Do(classId); + packet.Do(index); + packet.Do(flags); + if (packet.failure || + classId >= IOSync::Class::NumClasses || + index >= IOSync::g_Classes[classId]->GetMaxDeviceIndex()) + { return OnDisconnect(pid); + } - // relay to clients - SendToClients(packet, pid); + if (m_device_map[classId][index].first == pid) + { + SendToClientsOnThread(std::move(packet), pid); + } + else + { + WARN_LOG(NETPLAY, "Received spurious report for index %u from pid %u!", index, pid); + } + break; } - break; -#endif case NP_MSG_PONG : { @@ -599,6 +580,8 @@ bool NetPlayServer::StartGame(const std::string &path) m_is_running = true; + memset(m_device_map, 0xff, sizeof(m_device_map)); + return true; } diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 0a4bb7eb8583..a7495d77dc9a 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -14,6 +14,7 @@ #include "NetPlayProto.h" #include "FifoQueue.h" #include "TraversalClient.h" +#include "IOSync.h" #include #include @@ -32,14 +33,6 @@ class NetPlayServer : public TraversalClientClient bool StartGame(const std::string &path) /* ON(GUI) */; -#if 0 - void GetPadMapping(PadMapping map[]) /* ON(GUI) */; - void SetPadMapping(const PadMapping map[]) /* ON(GUI) */; - - void GetWiimoteMapping(PadMapping map[]) /* ON(GUI) */; - void SetWiimoteMapping(const PadMapping map[]) /* ON(GUI) */; -#endif - void AdjustPadBufferSize(unsigned int size) /* multiple threads */; void SetDialog(NetPlayUI* dialog); @@ -62,6 +55,8 @@ class NetPlayServer : public TraversalClientClient u32 ping; u32 current_game; bool connected; + // Should we not bother with unreliable packets? + bool is_localhost; }; void SendToClients(Packet&& packet, const PlayerId skip_pid = -1) NOT_ON(NET); @@ -69,10 +64,6 @@ class NetPlayServer : public TraversalClientClient MessageId OnConnect(PlayerId pid, Packet& hello) ON(NET); void OnDisconnect(PlayerId pid) ON(NET); void OnData(PlayerId pid, Packet&& packet) ON(NET); -#if 0 - void UpdatePadMapping() /* multiple threads */; - void UpdateWiimoteMapping() /* multiple threads */; -#endif void UpdatePings() ON(NET); std::vector> GetInterfaceListInternal(); @@ -84,10 +75,17 @@ class NetPlayServer : public TraversalClientClient bool m_update_pings; u32 m_current_game; u32 m_target_buffer_size; -#if 0 - PadMapping m_pad_map[4]; - PadMapping m_wiimote_map[4]; -#endif + + // Note about disconnects: Imagine a single player plus + // spectators. The client should not have to wait for the + // server for each frame. However, if the server decides to + // change the mapping, the client must not desync. + // Therefore, in lieu of more complicated solutions, + // disconnects that will be a surprise for the disconnected + // user (i.e. not a disconnect request or the user + // disconnecting) must be scheduled for the far future. + + std::pair m_device_map[IOSync::Class::NumClasses][IOSync::Class::MaxDeviceIndex]; std::vector m_players; unsigned m_num_players; From 2fc53cbf6643a9c2e6ba86f19f518f137f869a3a Mon Sep 17 00:00:00 2001 From: comex Date: Sat, 26 Oct 2013 22:51:21 -0400 Subject: [PATCH 061/202] Add packet spam layer. --- Source/Core/Common/Src/ChunkFile.h | 15 ++ Source/Core/Common/Src/Misc.cpp | 3 + Source/Core/Common/Src/TraversalClient.cpp | 232 +++++++++++++++++++-- Source/Core/Common/Src/TraversalClient.h | 67 +++++- Source/Core/Core/Src/IOSyncBackends.cpp | 17 +- Source/Core/Core/Src/NetPlayClient.cpp | 19 +- Source/Core/Core/Src/NetPlayClient.h | 10 +- Source/Core/Core/Src/NetPlayServer.cpp | 56 ++--- Source/Core/Core/Src/NetPlayServer.h | 9 +- 9 files changed, 329 insertions(+), 99 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index 7f9383b26d99..32160fa13f9b 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -42,10 +42,17 @@ struct LinkedListItem : public T class PWBuffer : public NonCopyable { public: + static struct _NoCopy {} NoCopy; + PWBuffer() { init(); } + PWBuffer(void* inData, size_t _size, _NoCopy&) + { + m_Data = (u8*) inData; + m_Size = m_Capacity = _size; + } PWBuffer(void* inData, size_t _size) { init(); @@ -134,6 +141,7 @@ class PWBuffer : public NonCopyable size_t m_Size; size_t m_Capacity; }; +class Packet; // ewww #if _LIBCPP_VERSION @@ -260,6 +268,13 @@ class PointerWrap } } + void Do(PointerWrap& x) + { + x.mode = mode; + x.readOff = 0; + Do(*x.vec); + } + void Do(PWBuffer& x) { u32 size = (u32)x.size(); diff --git a/Source/Core/Common/Src/Misc.cpp b/Source/Core/Common/Src/Misc.cpp index ce42deb4d2f0..fcfbc05701d8 100644 --- a/Source/Core/Common/Src/Misc.cpp +++ b/Source/Core/Common/Src/Misc.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include "Common.h" +#include "ChunkFile.h" // Neither Android nor OS X support TLS #if defined(__APPLE__) || (ANDROID && __clang__) @@ -31,3 +32,5 @@ const char* GetLastErrorMsg() return err_str; } + +PWBuffer::_NoCopy PWBuffer::NoCopy; diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index 2edcd01883f5..8744442b3b6e 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -15,19 +15,15 @@ inline ENetPacket* ENetUtil::MakeENetPacket(Packet&& pac, enet_uint32 flags) return packet; } -void ENetUtil::BroadcastPacket(ENetHost* host, Packet&& pac) +void ENetUtil::SendPacket(ENetPeer* peer, Packet&& pac, bool reliable) { - enet_host_broadcast(host, 0, MakeENetPacket(std::move(pac), ENET_PACKET_FLAG_RELIABLE)); -} - -void ENetUtil::SendPacket(ENetPeer* peer, Packet&& pac) -{ - enet_peer_send(peer, 0, MakeENetPacket(std::move(pac), ENET_PACKET_FLAG_RELIABLE)); + enet_peer_send(peer, 0, MakeENetPacket(std::move(pac), reliable ? ENET_PACKET_FLAG_RELIABLE : ENET_PACKET_FLAG_UNSEQUENCED)); } Packet ENetUtil::MakePacket(ENetPacket* epacket) { - Packet pac(PWBuffer(epacket->data, epacket->dataLength)); + Packet pac(PWBuffer(epacket->data, epacket->dataLength, PWBuffer::NoCopy)); + epacket->data = NULL; enet_packet_destroy(epacket); return pac; } @@ -74,6 +70,8 @@ static void GetRandomishBytes(u8* buf, size_t size) ENetHostClient::ENetHostClient(size_t peerCount, u16 port, bool isTraversalClient) { m_isTraversalClient = isTraversalClient; + m_GlobalSequenceNumber = 0; + ENetAddress addr = { ENET_HOST_ANY, port }; m_Host = enet_host_create( &addr, // address @@ -133,6 +131,113 @@ void ENetHostClient::Reset() m_Client = NULL; } +void ENetHostClient::BroadcastPacket(Packet&& packet, ENetPeer* except, bool queued) +{ + if (queued && packet.vec->size() < MaxShortPacketLength) + { + u16 seq = m_GlobalSequenceNumber++; + m_OutgoingPacketInfo.push_back(OutgoingPacketInfo(std::move(packet), except, seq)); + size_t peer = 0; + for (auto it = m_PeerInfo.begin(); it != m_PeerInfo.end(); ++it, ++peer) + { + if (&m_Host->peers[peer] == except) + continue; + (*it).m_GlobalSeqToSeq[seq] = (*it).m_OutgoingSequenceNumber++; + } + if (m_SendTimer.GetTimeDifference() > 6) + { + ProcessPacketQueue(); + m_SendTimer.Update(); + } + } + else + { + Packet container; + container.W((u16) 0); + container.W((PointerWrap&) packet); + + // avoid copying + ENetPacket* epacket = NULL; + + for (ENetPeer* peer = m_Host->peers, * end = &m_Host->peers[m_Host->peerCount]; peer != end; peer++) + { + if (peer->state != ENET_PEER_STATE_CONNECTED) + continue; + if (peer == except) + continue; + u16 seq = m_PeerInfo[peer - m_Host->peers].m_OutgoingSequenceNumber++; + if (!epacket) + { + epacket = ENetUtil::MakeENetPacket(std::move(container), ENET_PACKET_FLAG_RELIABLE); + } + else + { + u16* oseqp = (u16 *) epacket->data; + if (*oseqp != seq) + { + epacket = enet_packet_create(epacket->data, epacket->dataLength, ENET_PACKET_FLAG_RELIABLE); + } + } + u16* oseqp = (u16 *) epacket->data; + *oseqp = seq; + enet_peer_send(peer, 0, epacket); + } + if (epacket && epacket->referenceCount == 0) + enet_packet_destroy(epacket); + } +} + +void ENetHostClient::SendPacket(ENetPeer* peer, Packet&& packet) +{ + Packet container; + container.W((u16) m_PeerInfo[peer - m_Host->peers].m_OutgoingSequenceNumber++); + container.Do((PointerWrap&) packet); + ENetUtil::SendPacket(peer, std::move(container)); +} + +void ENetHostClient::ProcessPacketQueue() +{ + // The idea is that we send packets n-1 times unreliably and n times + // reliably. + bool needReliable = false; + int numToRemove = 0; + size_t totalSize = 0; + for (auto it = m_OutgoingPacketInfo.rbegin(); it != m_OutgoingPacketInfo.rend(); ++it) + { + OutgoingPacketInfo& info = *it; + totalSize += info.m_Packet.vec->size(); + if (++info.m_NumSends == MaxPacketSends || totalSize >= MaxShortPacketLength) + { + if (!info.m_DidSendReliably) + needReliable = true; + numToRemove++; + } + } + // this can occasionally cause packets to be sent unnecessarily + // reliably + for (ENetPeer* peer = m_Host->peers, * end = &m_Host->peers[m_Host->peerCount]; peer != end; peer++) + { + if (peer->state != ENET_PEER_STATE_CONNECTED) + continue; + Packet p; + auto& pi = m_PeerInfo[peer - m_Host->peers]; + for (auto it = m_OutgoingPacketInfo.begin(); it != m_OutgoingPacketInfo.end(); ++it) + { + OutgoingPacketInfo& info = *it; + if (info.m_Except == peer) + continue; + // XXX - fix situation where someone connects while this is queued + p.W(pi.m_GlobalSeqToSeq[info.m_GlobalSequenceNumber]); + p.Do((PointerWrap&) info.m_Packet); + info.m_DidSendReliably = info.m_DidSendReliably || needReliable; + } + if (p.vec->size()) + ENetUtil::SendPacket(peer, std::move(p), needReliable); + } + while (numToRemove--) + m_OutgoingPacketInfo.pop_front(); +} + void ENetHostClient::ThreadFunc() { ASSUME_ON(NET); @@ -163,7 +268,92 @@ void ENetHostClient::ThreadFunc() // Even if there was nothing, forward it as a wakeup. if (m_Client) - m_Client->OnENetEvent(&event); + { + switch (event.type) + { + case ENET_EVENT_TYPE_RECEIVE: + OnReceive(&event, ENetUtil::MakePacket(event.packet)); + break; + case ENET_EVENT_TYPE_CONNECT: + { + size_t pid = event.peer - m_Host->peers; + if (pid >= m_PeerInfo.size()) + m_PeerInfo.resize(pid + 1); + m_PeerInfo[pid].m_IncomingPackets.clear(); + m_PeerInfo[pid].m_IncomingSequenceNumber = 0; + m_PeerInfo[pid].m_OutgoingSequenceNumber = 0; + } + /* fall through */ + default: + m_Client->OnENetEvent(&event); + } + } + } +} + +void ENetHostClient::OnReceive(ENetEvent* event, Packet&& packet) +{ + auto& pi = m_PeerInfo[event->peer - m_Host->peers]; +#if 0 + printf("OnReceive isn=%x\n", pi.m_IncomingSequenceNumber); + DumpBuf(*packet.vec); +#endif + auto& incomingPackets = pi.m_IncomingPackets; + u16 seq; + + while (packet.vec->size() > packet.readOff) + { + { + packet.Do(seq); + if (packet.failure) + goto failure; + + s16 diff = (s16) (seq - pi.m_IncomingSequenceNumber); + if (diff < 0) + { + // assume a duplicate of something we already have + goto skip; + } + + while (incomingPackets.size() <= (size_t) diff) + incomingPackets.push_back(PWBuffer()); + + PWBuffer& buf = incomingPackets[diff]; + if (!buf.empty()) + { + // another type of duplicate + goto skip; + } + + Packet sub; + packet.Do(buf); + if (packet.failure) + goto failure; + continue; + } + + skip: + { + u32 size; + packet.Do(size); + if (packet.vec->size() - packet.readOff < size) + goto failure; + packet.readOff += size; + continue; + } + + failure: + { + // strange + WARN_LOG(NETPLAY, "Failure splitting packet - truncation?"); + } + } + + while (!incomingPackets.empty() && !incomingPackets[0].empty()) + { + m_Client->OnData(event, std::move(incomingPackets.front())); + incomingPackets.pop_front(); + pi.m_IncomingSequenceNumber++; } } @@ -198,7 +388,7 @@ void TraversalClient::ReconnectToServer() hello.helloFromClient.protoVersion = TraversalProtoVersion; RunOnThread([=]() { ASSUME_ON(NET); - SendPacket(hello); + SendTraversalPacket(hello); if (m_Client) m_Client->OnTraversalStateChanged(); }); @@ -234,7 +424,7 @@ void TraversalClient::ConnectToClient(const std::string& host) TraversalPacket packet = {0}; packet.type = TraversalPacketConnectPlease; memcpy(packet.connectPlease.hostId.data(), host.c_str(), host.size()); - m_ConnectRequestId = SendPacket(packet); + m_ConnectRequestId = SendTraversalPacket(packet); m_PendingConnect = true; } @@ -271,11 +461,11 @@ void TraversalClient::HandleServerPacket(TraversalPacket* packet) OnFailure(ServerForgotAboutUs); break; } - for (auto it = m_OutgoingPackets.begin(); it != m_OutgoingPackets.end(); ++it) + for (auto it = m_OutgoingTraversalPackets.begin(); it != m_OutgoingTraversalPackets.end(); ++it) { if (it->packet.requestId == packet->requestId) { - m_OutgoingPackets.erase(it); + m_OutgoingTraversalPackets.erase(it); break; } } @@ -359,7 +549,7 @@ void TraversalClient::OnFailure(int reason) m_Client->OnTraversalStateChanged(); } -void TraversalClient::ResendPacket(OutgoingPacketInfo* info) +void TraversalClient::ResendPacket(OutgoingTraversalPacketInfo* info) { info->sendTime = enet_time_get(); info->tries++; @@ -373,14 +563,14 @@ void TraversalClient::ResendPacket(OutgoingPacketInfo* info) void TraversalClient::HandleResends() { enet_uint32 now = enet_time_get(); - for (auto it = m_OutgoingPackets.begin(); it != m_OutgoingPackets.end(); ++it) + for (auto it = m_OutgoingTraversalPackets.begin(); it != m_OutgoingTraversalPackets.end(); ++it) { if (now - it->sendTime >= (u32) (300 * it->tries)) { if (it->tries >= 5) { OnFailure(ResendTimeout); - m_OutgoingPackets.clear(); + m_OutgoingTraversalPackets.clear(); break; } else @@ -400,19 +590,19 @@ void TraversalClient::HandlePing() TraversalPacket ping = {0}; ping.type = TraversalPacketPing; ping.ping.hostId = m_HostId; - SendPacket(ping); + SendTraversalPacket(ping); m_PingTime = now; } } -TraversalRequestId TraversalClient::SendPacket(const TraversalPacket& packet) +TraversalRequestId TraversalClient::SendTraversalPacket(const TraversalPacket& packet) { - OutgoingPacketInfo info; + OutgoingTraversalPacketInfo info; info.packet = packet; GetRandomishBytes((u8*) &info.packet.requestId, sizeof(info.packet.requestId)); info.tries = 0; - m_OutgoingPackets.push_back(info); - ResendPacket(&m_OutgoingPackets.back()); + m_OutgoingTraversalPackets.push_back(info); + ResendPacket(&m_OutgoingTraversalPackets.back()); return info.packet.requestId; } diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index aa377e8e7d11..378f19b1e422 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -9,17 +9,30 @@ #include "enet/enet.h" #include #include +#include "Timer.h" +#include "ChunkFile.h" DEFINE_THREAD_HAT(NET); #define MAX_CLIENTS 200 -#include "ChunkFile.h" +static inline void DumpBuf(PWBuffer& buf) +{ + printf("+00:"); + int c = 0; + for (size_t i = 0; i < buf.size(); i++) + { + printf(" %02x", buf.data()[i]); + if (++c % 16 == 0) + printf("\n+%02x:", c); + } + printf("\n"); +} + namespace ENetUtil { ENetPacket* MakeENetPacket(Packet&& pac, enet_uint32 flags); - void BroadcastPacket(ENetHost* host, Packet&& pac) ON(NET); - void SendPacket(ENetPeer* peer, Packet&& pac) ON(NET); + void SendPacket(ENetPeer* peer, Packet&& pac, bool reliable = true) ON(NET); Packet MakePacket(ENetPacket* epacket); void Wakeup(ENetHost* host); int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) /* ON(NET) */; @@ -42,7 +55,8 @@ class CopyAsMove class TraversalClientClient { public: - virtual void OnENetEvent(ENetEvent*) ON(NET) = 0; + virtual void OnENetEvent(ENetEvent* event) ON(NET) = 0; + virtual void OnData(ENetEvent* event, Packet&& packet) ON(NET) = 0; virtual void OnTraversalStateChanged() ON(NET) = 0; virtual void OnConnectReady(ENetAddress addr) ON(NET) = 0; virtual void OnConnectFailed(u8 reason) ON(NET) = 0; @@ -51,18 +65,50 @@ class TraversalClientClient class ENetHostClient { public: + enum + { + MaxPacketSends = 4, + MaxShortPacketLength = 128 + }; + ENetHostClient(size_t peerCount, u16 port, bool isTraversalClient = false); ~ENetHostClient(); void RunOnThread(std::function func) NOT_ON(NET); void CreateThread(); void Reset(); + void BroadcastPacket(Packet&& packet, ENetPeer* except = NULL, bool queued = false) ON(NET); + void SendPacket(ENetPeer* peer, Packet&& packet) ON(NET); + void ProcessPacketQueue() ON(NET); + TraversalClientClient* m_Client; ENetHost* m_Host; protected: virtual void HandleResends() ON(NET) {} private: + struct OutgoingPacketInfo + { + OutgoingPacketInfo(Packet&& packet, ENetPeer* except, u16 seq) + : m_Packet(std::move(packet)), m_Except(except), m_DidSendReliably(false), m_NumSends(0), m_GlobalSequenceNumber(seq) {} + + Packet m_Packet; + ENetPeer* m_Except; + bool m_DidSendReliably; + int m_NumSends; + u16 m_GlobalSequenceNumber; + }; + + struct PeerInfo + { + std::deque m_IncomingPackets; + // the sequence number of the first element of m_IncomingPackets + u16 m_IncomingSequenceNumber; + u16 m_OutgoingSequenceNumber; + u16 m_GlobalSeqToSeq[65536]; + }; + void ThreadFunc() /* ON(NET) */; + void OnReceive(ENetEvent* event, Packet&& packet) ON(NET); Common::FifoQueue, false> m_RunQueue; std::mutex m_RunQueueWriteLock; @@ -70,6 +116,11 @@ class ENetHostClient Common::Event m_ResetEvent; bool m_ShouldEndThread ACCESS_ON(NET); bool m_isTraversalClient; + + std::deque m_OutgoingPacketInfo ACCESS_ON(NET); + Common::Timer m_SendTimer ACCESS_ON(NET); + std::vector m_PeerInfo ACCESS_ON(NET); + u16 m_GlobalSequenceNumber ACCESS_ON(NET); }; class TraversalClient : public ENetHostClient @@ -104,7 +155,7 @@ class TraversalClient : public ENetHostClient protected: virtual void HandleResends() ON(NET); private: - struct OutgoingPacketInfo + struct OutgoingTraversalPacketInfo { TraversalPacket packet; int tries; @@ -112,15 +163,15 @@ class TraversalClient : public ENetHostClient }; void HandleServerPacket(TraversalPacket* packet) ON(NET); - void ResendPacket(OutgoingPacketInfo* info) ON(NET); - TraversalRequestId SendPacket(const TraversalPacket& packet) ON(NET); + void ResendPacket(OutgoingTraversalPacketInfo* info) ON(NET); + TraversalRequestId SendTraversalPacket(const TraversalPacket& packet) ON(NET); void OnFailure(int reason) ON(NET); static int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) /* ON(NET) */; void HandlePing() ON(NET); TraversalRequestId m_ConnectRequestId; bool m_PendingConnect; - std::list m_OutgoingPackets ACCESS_ON(NET); + std::list m_OutgoingTraversalPackets ACCESS_ON(NET); ENetAddress m_ServerAddress; enet_uint32 m_PingTime; std::string m_Server; diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index 7bf3b6c79d9a..0f707938d2ac 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -74,7 +74,6 @@ BackendNetPlay::BackendNetPlay(NetPlayClient* client, u32 delay) void BackendNetPlay::ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf) { WARN_LOG(NETPLAY, "Local connection class %d device %d", classId, localIndex); - //Packet* pac = m_Client->GetPacketToSendLater(); Packet pac; pac.W((MessageId) NP_MSG_CONNECT_DEVICE); pac.W((u8) classId); @@ -83,7 +82,7 @@ void BackendNetPlay::ConnectLocalDevice(int classId, int localIndex, PWBuffer&& pac.W((PlayerId) 0); // dummy pac.W((u8) 0); // dummy pac.vec->append(buf); - m_Client->SendPacket(std::move(pac)); + m_Client->SendPacket(std::move(pac), /*queued=*/true); } void BackendNetPlay::DisconnectLocalDevice(int classId, int localIndex) @@ -91,24 +90,16 @@ void BackendNetPlay::DisconnectLocalDevice(int classId, int localIndex) WARN_LOG(NETPLAY, "Local disconnection class %d device %d", classId, localIndex); g_Classes[classId]->SetIndex(-1, localIndex); - //Packet* pac = m_Client->GetPacketToSendLater(); Packet pac; pac.W((MessageId) NP_MSG_DISCONNECT_DEVICE); pac.W((u8) classId); pac.W((u8) localIndex); pac.W((u8) 0); // flags - m_Client->SendPacket(std::move(pac)); + m_Client->SendPacket(std::move(pac), /*queued=*/true); } void BackendNetPlay::EnqueueLocalReport(int classId, int localIndex, PWBuffer&& buf) { - /* - printf("sending ..."); - for (int i = 0; i < buf.size(); i++) - printf("%02x ", *(u8 *) (buf.data() + i)); - printf("\n"); - */ - //Packet* pac = m_Client->GetPacketToSendLater(); int ri = g_Classes[classId]->GetRemoteIndex(localIndex); if (ri == -1) return; @@ -123,7 +114,7 @@ void BackendNetPlay::EnqueueLocalReport(int classId, int localIndex, PWBuffer&& pac.vec->append(buf); // server won't send our own reports back to us ProcessPacket(pac.vec->copy()); - m_Client->SendPacket(std::move(pac)); + m_Client->SendPacket(std::move(pac), /*queued=*/true); } Packet BackendNetPlay::DequeueReport(int classId, int index, bool* keepGoing) @@ -132,6 +123,7 @@ Packet BackendNetPlay::DequeueReport(int classId, int index, bool* keepGoing) const bool& isConnected = g_Classes[classId]->IsConnected(index); while (1) { + //printf("dev=%llu past=%llu\n", deviceInfo.m_SubframeId, m_PastSubframeId); if (!isConnected || deviceInfo.m_SubframeId > m_PastSubframeId) { *keepGoing = false; @@ -150,6 +142,7 @@ Packet BackendNetPlay::DequeueReport(int classId, int index, bool* keepGoing) return PWBuffer(); } deviceInfo.m_SubframeId += skippedFrames; + //printf("--> dev=%llu past=%llu ql=%zd\n", deviceInfo.m_SubframeId, m_PastSubframeId, queue.size()); *keepGoing = deviceInfo.m_SubframeId < m_PastSubframeId; Packet q = std::move(p); queue.pop_front(); diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index cdfd02d30429..08082865fd45 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -50,7 +50,8 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam { // Direct or local connection. Don't use TraversalClient. m_direct_connection = true; - m_host_client.reset(new ENetHostClient(/*peerCount=*/1, /*port=*/0)); + m_host_client_store.reset(new ENetHostClient(/*peerCount=*/1, /*port=*/0)); + m_host_client = m_host_client_store.get(); if (!m_host_client->m_Host) return; m_host_client->m_Client = this; @@ -71,6 +72,7 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam EnsureTraversalClient(SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayCentralServer, 0); if (!g_TraversalClient) return; + m_host_client = g_TraversalClient.get(); // If we were disconnected in the background, reconnect. if (g_TraversalClient->m_State == TraversalClient::Failure) g_TraversalClient->ReconnectToServer(); @@ -94,12 +96,12 @@ void NetPlayClient::SetDialog(NetPlayUI* dialog) m_have_dialog_event.Set(); } -void NetPlayClient::SendPacket(Packet&& packet) +void NetPlayClient::SendPacket(Packet&& packet, bool queued) { CopyAsMove tmp(std::move(packet)); g_TraversalClient->RunOnThread([=]() mutable { ASSUME_ON(NET); - ENetUtil::BroadcastPacket(m_host, std::move(*tmp)); + m_host_client->BroadcastPacket(std::move(*tmp), NULL, queued); }); } @@ -111,7 +113,7 @@ void NetPlayClient::OnPacketErrorFromIOSync() }); } -void NetPlayClient::OnData(Packet&& packet) +void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) { if (m_state == WaitingForHelloResponse) { @@ -303,14 +305,16 @@ void NetPlayClient::OnData(Packet&& packet) u32 ping_key = 0; packet.Do(ping_key); if (packet.failure) + { return OnDisconnect(InvalidPacket); + } Packet pong; pong.W((MessageId)NP_MSG_PONG); pong.W(ping_key); std::lock_guard lk(m_crit); - ENetUtil::BroadcastPacket(m_host, std::move(pong)); + m_host_client->BroadcastPacket(std::move(pong)); } break; @@ -371,7 +375,7 @@ void NetPlayClient::OnENetEvent(ENetEvent* event) hello.W(std::string(NETPLAY_VERSION)); hello.W(std::string(netplay_dolphin_ver)); hello.W(m_local_name); - ENetUtil::BroadcastPacket(m_host, std::move(hello)); + m_host_client->BroadcastPacket(std::move(hello)); m_state = WaitingForHelloResponse; if (m_state_callback) m_state_callback(this); @@ -380,9 +384,6 @@ void NetPlayClient::OnENetEvent(ENetEvent* event) case ENET_EVENT_TYPE_DISCONNECT: OnDisconnect(ReceivedENetDisconnect); break; - case ENET_EVENT_TYPE_RECEIVE: - OnData(ENetUtil::MakePacket(event->packet)); - break; default: break; } diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 785a5f500780..898b5144dc77 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -92,13 +92,13 @@ class NetPlayClient : public TraversalClientClient void SetDialog(NetPlayUI* dialog); - virtual void OnENetEvent(ENetEvent*) override ON(NET); + virtual void OnENetEvent(ENetEvent* event) override ON(NET); + virtual void OnData(ENetEvent* event, Packet&& packet) override ON(NET); virtual void OnTraversalStateChanged() override ON(NET); virtual void OnConnectReady(ENetAddress addr) override ON(NET); virtual void OnConnectFailed(u8 reason) override ON(NET); - // temporarily public, to replace with GetPacketToSendLater - void SendPacket(Packet&& packet); + void SendPacket(Packet&& packet, bool queued = false); void OnPacketErrorFromIOSync(); std::function m_state_callback; @@ -128,12 +128,12 @@ class NetPlayClient : public TraversalClientClient bool m_is_recording; private: - void OnData(Packet&& packet) ON(NET); void OnDisconnect(int reason) ON(NET); void DoDirectConnect(const ENetAddress& addr); std::map m_players GUARDED_BY(m_crit); - std::unique_ptr m_host_client; + std::unique_ptr m_host_client_store; + ENetHostClient* m_host_client; Common::Event m_have_dialog_event; }; diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 72cc33d2a3df..eb640a885d57 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -85,9 +85,6 @@ void NetPlayServer::OnENetEvent(ENetEvent* event) case ENET_EVENT_TYPE_DISCONNECT: OnDisconnect(pid); break; - case ENET_EVENT_TYPE_RECEIVE: - OnData(pid, ENetUtil::MakePacket(event->packet)); - break; default: // notably, ignore connects until we get a hello message break; @@ -218,20 +215,6 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) if (m_num_players >= MAX_CLIENTS) return CON_ERR_SERVER_FULL; - UpdatePings(); - -#if 0 - // try to automatically assign new user a pad - for (unsigned int m = 0; m < 4; ++m) - { - if (m_pad_map[m] == -1) - { - m_pad_map[m] = pid; - break; - } - } -#endif - // send join message to already connected clients { Packet opacket; @@ -239,7 +222,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) opacket.W(pid); opacket.W(player.name); opacket.W(player.revision); - SendToClientsOnThread(std::move(opacket)); + SendToClientsOnThread(std::move(opacket), pid); } // send new client success message with their id @@ -247,9 +230,11 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) Packet opacket; opacket.W((MessageId)0); opacket.W(pid); - ENetUtil::SendPacket(peer, std::move(opacket)); + g_TraversalClient->SendPacket(peer, std::move(opacket)); } + UpdatePings(); + // send new client the selected game { std::lock_guard lk(m_crit); @@ -258,7 +243,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) Packet opacket; opacket.W((MessageId)NP_MSG_CHANGE_GAME); opacket.W(m_selected_game); - ENetUtil::SendPacket(peer, std::move(opacket)); + g_TraversalClient->SendPacket(peer, std::move(opacket)); } } @@ -267,7 +252,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) Packet opacket; opacket.W((MessageId)NP_MSG_PAD_BUFFER); opacket.W((u32)m_target_buffer_size); - ENetUtil::SendPacket(peer, std::move(opacket)); + g_TraversalClient->SendPacket(peer, std::move(opacket)); } // send players @@ -281,7 +266,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) opacket.W((PlayerId)opid); opacket.W(oplayer.name); opacket.W(oplayer.revision); - ENetUtil::SendPacket(peer, std::move(opacket)); + g_TraversalClient->SendPacket(peer, std::move(opacket)); } } @@ -331,8 +316,11 @@ void NetPlayServer::AdjustPadBufferSize(unsigned int size) SendToClients(std::move(opacket)); } -void NetPlayServer::OnData(PlayerId pid, Packet&& packet) +void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) { + /*printf("Server sees\n"); + DumpBuf(*packet.vec);*/ + PlayerId pid = event->peer - m_host->peers; ENetPeer* peer = &m_host->peers[pid]; if (pid >= m_players.size()) m_players.resize(pid + 1); @@ -346,12 +334,12 @@ void NetPlayServer::OnData(PlayerId pid, Packet&& packet) Packet opacket; opacket.W(error); opacket.W((PlayerId)0); - ENetUtil::SendPacket(peer, std::move(opacket)); + g_TraversalClient->SendPacket(peer, std::move(opacket)); enet_peer_disconnect_later(peer, 0); } else { - player.is_localhost = peer->address.host == 0x0100007f; + //player.is_localhost = peer->address.host == 0x0100007f; player.connected = true; m_num_players++; } @@ -482,7 +470,7 @@ void NetPlayServer::OnData(PlayerId pid, Packet&& packet) if (m_device_map[classId][index].first == pid) { - SendToClientsOnThread(std::move(packet), pid); + SendToClientsOnThread(std::move(packet), pid, /*queued=*/true); } else { @@ -585,7 +573,7 @@ bool NetPlayServer::StartGame(const std::string &path) return true; } -void NetPlayServer::SendToClients(Packet&& packet, const PlayerId skip_pid) +void NetPlayServer::SendToClients(Packet&& packet, const PlayerId skip_pid, bool queued) { CopyAsMove tmp(std::move(packet)); g_TraversalClient->RunOnThread([=]() mutable { @@ -595,19 +583,9 @@ void NetPlayServer::SendToClients(Packet&& packet, const PlayerId skip_pid) } -void NetPlayServer::SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid) +void NetPlayServer::SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid, bool queued) { - ENetPacket* epacket = NULL; - for (size_t pid = 0; pid < m_players.size(); pid++) - { - if (pid != skip_pid && - m_players[pid].connected) - { - if (!epacket) - epacket = ENetUtil::MakeENetPacket(std::move(packet), ENET_PACKET_FLAG_RELIABLE); - enet_peer_send(&m_host->peers[pid], 0, epacket); - } - } + g_TraversalClient->BroadcastPacket(std::move(packet), skip_pid >= m_host->peerCount ? NULL : &m_host->peers[skip_pid], queued); } void NetPlayServer::SetDialog(NetPlayUI* dialog) diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index a7495d77dc9a..e6503ca61bd6 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -38,6 +38,7 @@ class NetPlayServer : public TraversalClientClient void SetDialog(NetPlayUI* dialog); virtual void OnENetEvent(ENetEvent*) override ON(NET); + virtual void OnData(ENetEvent* event, Packet&& packet) ON(NET); virtual void OnTraversalStateChanged() override ON(NET); virtual void OnConnectReady(ENetAddress addr) override {} virtual void OnConnectFailed(u8 reason) override ON(NET) {} @@ -55,15 +56,13 @@ class NetPlayServer : public TraversalClientClient u32 ping; u32 current_game; bool connected; - // Should we not bother with unreliable packets? - bool is_localhost; + //bool is_localhost; }; - void SendToClients(Packet&& packet, const PlayerId skip_pid = -1) NOT_ON(NET); - void SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid = -1) ON(NET); + void SendToClients(Packet&& packet, const PlayerId skip_pid = -1, bool queued = false) NOT_ON(NET); + void SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid = -1, bool queued = false) ON(NET); MessageId OnConnect(PlayerId pid, Packet& hello) ON(NET); void OnDisconnect(PlayerId pid) ON(NET); - void OnData(PlayerId pid, Packet&& packet) ON(NET); void UpdatePings() ON(NET); std::vector> GetInterfaceListInternal(); From 196ef6aad49bd00774db93105bbea99f3996a623 Mon Sep 17 00:00:00 2001 From: comex Date: Sat, 26 Oct 2013 23:48:15 -0400 Subject: [PATCH 062/202] Add copy methods for PWBuffer JUST FOR WIN32 because VC is special and thinks that things require copies (a) when they don't (b) without even bothering to provide a stack trace. --- Source/Core/Common/Src/ChunkFile.h | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index 32160fa13f9b..4b623b48c9bb 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -39,7 +39,10 @@ struct LinkedListItem : public T }; // Like std::vector but without initialization to 0 and some extra methods. -class PWBuffer : public NonCopyable +class PWBuffer +#ifndef _WIN32 + : public NonCopyable +#endif { public: static struct _NoCopy {} NoCopy; @@ -80,6 +83,19 @@ class PWBuffer : public NonCopyable { return PWBuffer(m_Data, m_Size); } +#ifdef _WIN32 + // Get rid of this crap when we switch to VC2013. + PWBuffer(const PWBuffer& buffer) + { + init(); + append(buffer.data(), buffer.size()); + } + void operator=(const PWBuffer& buffer) + { + clear(); + append(buffer.data(), buffer.size()); + } +#endif void swap(PWBuffer& other) { std::swap(m_Data, other.m_Data); @@ -103,7 +119,7 @@ class PWBuffer : public NonCopyable } } void clear() { resize(0); } - void append(void* inData, size_t _size) + void append(const void* inData, size_t _size) { size_t old = m_Size; resize(old + _size); From cdf60a554226b21fc621965512146940652e8821 Mon Sep 17 00:00:00 2001 From: comex Date: Sun, 27 Oct 2013 00:29:19 -0400 Subject: [PATCH 063/202] Fix stupid deadlocks. --- Source/Core/Common/Src/TraversalClient.cpp | 27 ++++++++++++++-------- Source/Core/Common/Src/TraversalClient.h | 10 +++++--- Source/Core/Core/Src/IOSyncBackends.cpp | 6 ++--- Source/Core/Core/Src/NetPlayClient.cpp | 4 ++-- Source/Core/Core/Src/NetPlayClient.h | 2 +- Source/Core/Core/Src/NetPlayServer.cpp | 8 +++---- Source/Core/Core/Src/NetPlayServer.h | 4 ++-- 7 files changed, 36 insertions(+), 25 deletions(-) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index 8744442b3b6e..be64fb2eb2a8 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -131,12 +131,12 @@ void ENetHostClient::Reset() m_Client = NULL; } -void ENetHostClient::BroadcastPacket(Packet&& packet, ENetPeer* except, bool queued) +void ENetHostClient::BroadcastPacket(Packet&& packet, ENetPeer* except) { - if (queued && packet.vec->size() < MaxShortPacketLength) + if (packet.vec->size() < MaxShortPacketLength) { u16 seq = m_GlobalSequenceNumber++; - m_OutgoingPacketInfo.push_back(OutgoingPacketInfo(std::move(packet), except, seq)); + m_OutgoingPacketInfo.push_back(OutgoingPacketInfo(std::move(packet), except, seq, m_GlobalTicker++)); size_t peer = 0; for (auto it = m_PeerInfo.begin(); it != m_PeerInfo.end(); ++it, ++peer) { @@ -144,11 +144,6 @@ void ENetHostClient::BroadcastPacket(Packet&& packet, ENetPeer* except, bool que continue; (*it).m_GlobalSeqToSeq[seq] = (*it).m_OutgoingSequenceNumber++; } - if (m_SendTimer.GetTimeDifference() > 6) - { - ProcessPacketQueue(); - m_SendTimer.Update(); - } } else { @@ -195,6 +190,15 @@ void ENetHostClient::SendPacket(ENetPeer* peer, Packet&& packet) ENetUtil::SendPacket(peer, std::move(container)); } +void ENetHostClient::MaybeProcessPacketQueue() +{ + if (m_SendTimer.GetTimeDifference() > 6) + { + ProcessPacketQueue(); + m_SendTimer.Update(); + } +} + void ENetHostClient::ProcessPacketQueue() { // The idea is that we send packets n-1 times unreliably and n times @@ -226,7 +230,8 @@ void ENetHostClient::ProcessPacketQueue() OutgoingPacketInfo& info = *it; if (info.m_Except == peer) continue; - // XXX - fix situation where someone connects while this is queued + if (pi.m_ConnectTicker > info.m_Ticker) + continue; p.W(pi.m_GlobalSeqToSeq[info.m_GlobalSequenceNumber]); p.Do((PointerWrap&) info.m_Packet); info.m_DidSendReliably = info.m_DidSendReliably || needReliable; @@ -257,7 +262,7 @@ void ENetHostClient::ThreadFunc() PanicAlert("enet_socket_get_address failed."); continue; } - int count = enet_host_service(m_Host, &event, m_Host->connectedPeers > 0 ? 10 : 300); + int count = enet_host_service(m_Host, &event, m_Host->connectedPeers > 0 ? 5 : 300); if (count < 0) { PanicAlert("enet_host_service failed... do something about this."); @@ -282,12 +287,14 @@ void ENetHostClient::ThreadFunc() m_PeerInfo[pid].m_IncomingPackets.clear(); m_PeerInfo[pid].m_IncomingSequenceNumber = 0; m_PeerInfo[pid].m_OutgoingSequenceNumber = 0; + m_PeerInfo[pid].m_ConnectTicker = m_GlobalTicker++; } /* fall through */ default: m_Client->OnENetEvent(&event); } } + MaybeProcessPacketQueue(); } } diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index 378f19b1e422..ddcbe2b262cd 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -77,8 +77,9 @@ class ENetHostClient void CreateThread(); void Reset(); - void BroadcastPacket(Packet&& packet, ENetPeer* except = NULL, bool queued = false) ON(NET); + void BroadcastPacket(Packet&& packet, ENetPeer* except = NULL) ON(NET); void SendPacket(ENetPeer* peer, Packet&& packet) ON(NET); + void MaybeProcessPacketQueue() ON(NET); void ProcessPacketQueue() ON(NET); TraversalClientClient* m_Client; @@ -88,14 +89,15 @@ class ENetHostClient private: struct OutgoingPacketInfo { - OutgoingPacketInfo(Packet&& packet, ENetPeer* except, u16 seq) - : m_Packet(std::move(packet)), m_Except(except), m_DidSendReliably(false), m_NumSends(0), m_GlobalSequenceNumber(seq) {} + OutgoingPacketInfo(Packet&& packet, ENetPeer* except, u16 seq, u64 ticker) + : m_Packet(std::move(packet)), m_Except(except), m_DidSendReliably(false), m_NumSends(0), m_GlobalSequenceNumber(seq), m_Ticker(ticker) {} Packet m_Packet; ENetPeer* m_Except; bool m_DidSendReliably; int m_NumSends; u16 m_GlobalSequenceNumber; + u64 m_Ticker; }; struct PeerInfo @@ -105,6 +107,7 @@ class ENetHostClient u16 m_IncomingSequenceNumber; u16 m_OutgoingSequenceNumber; u16 m_GlobalSeqToSeq[65536]; + u64 m_ConnectTicker; }; void ThreadFunc() /* ON(NET) */; @@ -121,6 +124,7 @@ class ENetHostClient Common::Timer m_SendTimer ACCESS_ON(NET); std::vector m_PeerInfo ACCESS_ON(NET); u16 m_GlobalSequenceNumber ACCESS_ON(NET); + u64 m_GlobalTicker ACCESS_ON(NET); }; class TraversalClient : public ENetHostClient diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index 0f707938d2ac..4e7516251f90 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -82,7 +82,7 @@ void BackendNetPlay::ConnectLocalDevice(int classId, int localIndex, PWBuffer&& pac.W((PlayerId) 0); // dummy pac.W((u8) 0); // dummy pac.vec->append(buf); - m_Client->SendPacket(std::move(pac), /*queued=*/true); + m_Client->SendPacket(std::move(pac)); } void BackendNetPlay::DisconnectLocalDevice(int classId, int localIndex) @@ -95,7 +95,7 @@ void BackendNetPlay::DisconnectLocalDevice(int classId, int localIndex) pac.W((u8) classId); pac.W((u8) localIndex); pac.W((u8) 0); // flags - m_Client->SendPacket(std::move(pac), /*queued=*/true); + m_Client->SendPacket(std::move(pac)); } void BackendNetPlay::EnqueueLocalReport(int classId, int localIndex, PWBuffer&& buf) @@ -114,7 +114,7 @@ void BackendNetPlay::EnqueueLocalReport(int classId, int localIndex, PWBuffer&& pac.vec->append(buf); // server won't send our own reports back to us ProcessPacket(pac.vec->copy()); - m_Client->SendPacket(std::move(pac), /*queued=*/true); + m_Client->SendPacket(std::move(pac)); } Packet BackendNetPlay::DequeueReport(int classId, int index, bool* keepGoing) diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 08082865fd45..68ba36cab35e 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -96,12 +96,12 @@ void NetPlayClient::SetDialog(NetPlayUI* dialog) m_have_dialog_event.Set(); } -void NetPlayClient::SendPacket(Packet&& packet, bool queued) +void NetPlayClient::SendPacket(Packet&& packet) { CopyAsMove tmp(std::move(packet)); g_TraversalClient->RunOnThread([=]() mutable { ASSUME_ON(NET); - m_host_client->BroadcastPacket(std::move(*tmp), NULL, queued); + m_host_client->BroadcastPacket(std::move(*tmp), NULL); }); } diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 898b5144dc77..4c375924ce5c 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -98,7 +98,7 @@ class NetPlayClient : public TraversalClientClient virtual void OnConnectReady(ENetAddress addr) override ON(NET); virtual void OnConnectFailed(u8 reason) override ON(NET); - void SendPacket(Packet&& packet, bool queued = false); + void SendPacket(Packet&& packet); void OnPacketErrorFromIOSync(); std::function m_state_callback; diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index eb640a885d57..4783e08f4cf1 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -470,7 +470,7 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) if (m_device_map[classId][index].first == pid) { - SendToClientsOnThread(std::move(packet), pid, /*queued=*/true); + SendToClientsOnThread(std::move(packet), pid); } else { @@ -573,7 +573,7 @@ bool NetPlayServer::StartGame(const std::string &path) return true; } -void NetPlayServer::SendToClients(Packet&& packet, const PlayerId skip_pid, bool queued) +void NetPlayServer::SendToClients(Packet&& packet, const PlayerId skip_pid) { CopyAsMove tmp(std::move(packet)); g_TraversalClient->RunOnThread([=]() mutable { @@ -583,9 +583,9 @@ void NetPlayServer::SendToClients(Packet&& packet, const PlayerId skip_pid, bool } -void NetPlayServer::SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid, bool queued) +void NetPlayServer::SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid) { - g_TraversalClient->BroadcastPacket(std::move(packet), skip_pid >= m_host->peerCount ? NULL : &m_host->peers[skip_pid], queued); + g_TraversalClient->BroadcastPacket(std::move(packet), skip_pid >= m_host->peerCount ? NULL : &m_host->peers[skip_pid]); } void NetPlayServer::SetDialog(NetPlayUI* dialog) diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index e6503ca61bd6..e33351af8dda 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -59,8 +59,8 @@ class NetPlayServer : public TraversalClientClient //bool is_localhost; }; - void SendToClients(Packet&& packet, const PlayerId skip_pid = -1, bool queued = false) NOT_ON(NET); - void SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid = -1, bool queued = false) ON(NET); + void SendToClients(Packet&& packet, const PlayerId skip_pid = -1) NOT_ON(NET); + void SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid = -1) ON(NET); MessageId OnConnect(PlayerId pid, Packet& hello) ON(NET); void OnDisconnect(PlayerId pid) ON(NET); void UpdatePings() ON(NET); From f8dbeccca2452968afd3788bdcc602a20dcb69ad Mon Sep 17 00:00:00 2001 From: comex Date: Sun, 27 Oct 2013 00:33:10 -0400 Subject: [PATCH 064/202] Apparently old versions of GCC are also noncompliant. --- Source/Core/Common/Src/ChunkFile.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index 4b623b48c9bb..8bde868aa5a3 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -40,7 +40,7 @@ struct LinkedListItem : public T // Like std::vector but without initialization to 0 and some extra methods. class PWBuffer -#ifndef _WIN32 +#if !defined(__APPLE__) : public NonCopyable #endif { @@ -83,7 +83,7 @@ class PWBuffer { return PWBuffer(m_Data, m_Size); } -#ifdef _WIN32 +#if !defined(__APPLE__) // Get rid of this crap when we switch to VC2013. PWBuffer(const PWBuffer& buffer) { From 971308c3e3eac3562c20f5237dd61df254bf9389 Mon Sep 17 00:00:00 2001 From: comex Date: Sun, 27 Oct 2013 00:44:07 -0400 Subject: [PATCH 065/202] Add missing initialization --- Source/Core/Common/Src/TraversalClient.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index be64fb2eb2a8..d046a2808bcf 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -71,6 +71,7 @@ ENetHostClient::ENetHostClient(size_t peerCount, u16 port, bool isTraversalClien { m_isTraversalClient = isTraversalClient; m_GlobalSequenceNumber = 0; + m_GlobalTicker = 0; ENetAddress addr = { ENET_HOST_ANY, port }; m_Host = enet_host_create( From d1834b30584f064354addbde0c15265f55e9d872 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Sun, 27 Oct 2013 22:08:41 -0400 Subject: [PATCH 066/202] [Android] Overlay now works during emulation. --- .../dolphinemu/dolphinemu/NativeLibrary.java | 36 ++++++++ .../emulation/overlay/InputOverlay.java | 91 ++++++++++++------- .../overlay/InputOverlayDrawable.java | 45 +++++++++ 3 files changed, 141 insertions(+), 31 deletions(-) create mode 100644 Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawable.java diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java index dd4693b347e5..53db64b194e5 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java @@ -15,6 +15,42 @@ */ public final class NativeLibrary { + /** + * Button type for use in onTouchEvent + */ + public static final class ButtonType + { + public static final int BUTTON_A = 0; + public static final int BUTTON_B = 1; + public static final int BUTTON_START = 2; + public static final int BUTTON_X = 3; + public static final int BUTTON_Y = 4; + public static final int BUTTON_Z = 5; + public static final int BUTTON_UP = 6; + public static final int BUTTON_DOWN = 7; + public static final int BUTTON_LEFT = 8; + public static final int BUTTON_RIGHT = 9; + public static final int STICK_MAIN_UP = 10; + public static final int STICK_MAIN_DOWN = 11; + public static final int STICK_MAIN_LEFT = 12; + public static final int STICK_MAIN_RIGHT = 13; + public static final int STICK_C_UP = 14; + public static final int STICK_C_DOWN = 15; + public static final int STICK_C_LEFT = 16; + public static final int STICK_C_RIGHT = 17; + public static final int TRIGGER_L = 18; + public static final int TRIGGER_R = 19; + } + + /** + * Button states + */ + public class ButtonState + { + public static final int RELEASED = 0; + public static final int PRESSED = 1; + } + /** * Handles touch events. * diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java index 5053a03edc50..d33beee45dae 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java @@ -9,13 +9,18 @@ import java.util.HashSet; import java.util.Set; +import org.dolphinemu.dolphinemu.NativeLibrary; +import org.dolphinemu.dolphinemu.NativeLibrary.ButtonState; +import org.dolphinemu.dolphinemu.NativeLibrary.ButtonType; import org.dolphinemu.dolphinemu.R; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.preference.PreferenceManager; import android.util.AttributeSet; import android.view.MotionEvent; @@ -29,7 +34,7 @@ */ public final class InputOverlay extends SurfaceView implements OnTouchListener { - private final Set overlayItems = new HashSet(); + private final Set overlayItems = new HashSet(); /** * Constructor @@ -42,9 +47,12 @@ public InputOverlay(Context context, AttributeSet attrs) super(context, attrs); // Add all the overlay items to the HashSet. - overlayItems.add(initializeOverlayDrawable(context, R.drawable.button_a)); - overlayItems.add(initializeOverlayDrawable(context, R.drawable.button_b)); - overlayItems.add(initializeOverlayDrawable(context, R.drawable.button_start)); + overlayItems.add(initializeOverlayDrawable(context, R.drawable.button_a, ButtonType.BUTTON_A)); + overlayItems.add(initializeOverlayDrawable(context, R.drawable.button_b, ButtonType.BUTTON_B)); + overlayItems.add(initializeOverlayDrawable(context, R.drawable.button_start, ButtonType.BUTTON_START)); + + // Set the on touch listener. + setOnTouchListener(this); // Force draw setWillNotDraw(false); @@ -56,16 +64,35 @@ public InputOverlay(Context context, AttributeSet attrs) @Override public boolean onTouch(View v, MotionEvent event) { - switch (event.getAction()) + // Determine the button state to apply based on the MotionEvent action flag. + int buttonState = (event.getAction() == MotionEvent.ACTION_DOWN) ? ButtonState.PRESSED : ButtonState.RELEASED; + + for (InputOverlayDrawable item : overlayItems) { - case MotionEvent.ACTION_DOWN: + // Check if there was a touch within the bounds of a drawable. + if (item.getBounds().contains((int)event.getX(), (int)event.getY())) { - // TODO: Handle down presses. - return true; + switch (item.getId()) + { + case ButtonType.BUTTON_A: + NativeLibrary.onTouchEvent(ButtonType.BUTTON_A, buttonState); + break; + + case ButtonType.BUTTON_B: + NativeLibrary.onTouchEvent(ButtonType.BUTTON_B, buttonState); + break; + + case ButtonType.BUTTON_START: + NativeLibrary.onTouchEvent(ButtonType.BUTTON_START, buttonState); + break; + + default: + break; + } } } - return false; + return true; } @Override @@ -74,14 +101,14 @@ public void onDraw(Canvas canvas) super.onDraw(canvas); // Draw all overlay items. - for (BitmapDrawable item : overlayItems) + for (InputOverlayDrawable item : overlayItems) { item.draw(canvas); } } /** - * Initializes a drawable, given by resId, with all of the + * Initializes an InputOverlayDrawable, given by resId, with all of the * parameters set for it to be properly shown on the InputOverlay. *

* This works due to the way the X and Y coordinates are stored within @@ -101,46 +128,48 @@ public void onDraw(Canvas canvas) * *

* Technically no modifications should need to be performed on the returned - * BitmapDrawable. Simply add it to the HashSet of overlay items and wait + * InputOverlayDrawable. Simply add it to the HashSet of overlay items and wait * for Android to call the onDraw method. * - * @param context The current {@link Context}. - * @param resId The resource ID of the {@link BitmapDrawable} to get. + * @param context The current {@link Context}. + * @param resId The resource ID of the {@link Drawable} to get the {@link Bitmap} of. + * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawable represents. * - * @return A {@link BitmapDrawable} with the correct drawing bounds set. + * @return An {@link InputOverlayDrawable} with the correct drawing bounds set. * */ - private static BitmapDrawable initializeOverlayDrawable(Context context, int resId) + private static InputOverlayDrawable initializeOverlayDrawable(Context context, int resId, int buttonId) { - // Resources handle for fetching the drawable, etc. + // Resources handle for fetching the initial Drawable resource. final Resources res = context.getResources(); - // SharedPreference to retrieve the X and Y coordinates for the drawable. + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawable. final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); - // Get the desired drawable. - BitmapDrawable drawable = (BitmapDrawable) res.getDrawable(resId); + // Initialize the InputOverlayDrawable. + final Bitmap bitmap = BitmapFactory.decodeResource(res, resId); + final InputOverlayDrawable overlayDrawable = new InputOverlayDrawable(res, bitmap, buttonId); - // String ID of the drawable. This is what is passed into SharedPreferences + // String ID of the Drawable. This is what is passed into SharedPreferences // to check whether or not a value has been set. - String drawableId = res.getResourceEntryName(resId); + final String drawableId = res.getResourceEntryName(resId); - // The X and Y coordinates of the drawable on the InputOverlay. + // The X and Y coordinates of the InputOverlayDrawable on the InputOverlay. // These were set in the input overlay configuration menu. int drawableX = (int) sPrefs.getFloat(drawableId+"-X", 0f); int drawableY = (int) sPrefs.getFloat(drawableId+"-Y", 0f); - // Intrinsic width and height of the drawable. + // Intrinsic width and height of the InputOverlayDrawable. // For any who may not know, intrinsic width/height // are the original unmodified width and height of the image. - int intrinWidth = drawable.getIntrinsicWidth(); - int intrinHeight = drawable.getIntrinsicHeight(); + int intrinWidth = overlayDrawable.getIntrinsicWidth(); + int intrinHeight = overlayDrawable.getIntrinsicHeight(); - // Now set the bounds for the drawable. - // This will dictate where on the screen (and the what the size) of the drawable will be. - drawable.setBounds(drawableX, drawableY, drawableX+intrinWidth, drawableY+intrinHeight); + // Now set the bounds for the InputOverlayDrawable. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawable will be. + overlayDrawable.setBounds(drawableX, drawableY, drawableX+intrinWidth, drawableY+intrinHeight); - return drawable; + return overlayDrawable; } } diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawable.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawable.java new file mode 100644 index 000000000000..a4f445b5e3a9 --- /dev/null +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawable.java @@ -0,0 +1,45 @@ +/** + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2 + * Refer to the license.txt file included. + */ + +package org.dolphinemu.dolphinemu.emulation.overlay; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; + +/** + * Custom {@link BitmapDrawable} that is capable + * of storing it's own ID. + */ +public class InputOverlayDrawable extends BitmapDrawable +{ + // The ID identifying what type of button this Drawable represents. + private int buttonType; + + /** + * Constructor + * + * @param res {@link Resources} instance. + * @param bitmap {@link Bitmap} to use with this Drawable. + * @param buttonType Identifier for this type of button. + */ + public InputOverlayDrawable(Resources res, Bitmap bitmap, int buttonType) + { + super(res, bitmap); + + this.buttonType = buttonType; + } + + /** + * Gets this InputOverlayDrawable's button ID. + * + * @return this InputOverlayDrawable's button ID. + */ + public int getId() + { + return buttonType; + } +} From 4e999fe0ee1100ef4bee2e3fefe908222a5c2e92 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Sun, 27 Oct 2013 22:15:49 -0400 Subject: [PATCH 067/202] [Android] General cleanup. We no longer need the buttons in Assets, considering they are now resources (ie. drawables). Also remove (now) junk code from VideoSettingsFragment.java. We handle the input overlay within the InputSettingsFragment. Also add a TODO detailing what needs to be refactored when axis support is finally added. --- Source/Android/assets/ButtonA.png | Bin 5178 -> 0 bytes Source/Android/assets/ButtonB.png | Bin 4971 -> 0 bytes Source/Android/assets/ButtonStart.png | Bin 1511 -> 0 bytes .../emulation/overlay/InputOverlay.java | 2 ++ .../settings/video/VideoSettingsFragment.java | 4 ---- 5 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 Source/Android/assets/ButtonA.png delete mode 100644 Source/Android/assets/ButtonB.png delete mode 100644 Source/Android/assets/ButtonStart.png diff --git a/Source/Android/assets/ButtonA.png b/Source/Android/assets/ButtonA.png deleted file mode 100644 index 7e685324ae8a4ad016eb1033d64b659966cfcd0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5178 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 diff --git a/Source/Android/assets/ButtonB.png b/Source/Android/assets/ButtonB.png deleted file mode 100644 index 21da4c20f123fe6409b57cc6d837fd0f60786098..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4971 zcmV-x6O`Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000u&Nkl#ha<5{UH9txuIOkZs809n`&%Ts0+5dL7Mp+hx=8#iZN56lus~=G z^GCTaaPJYVwOL@?*!;U~#ueG%;YhSLCE<7mhNB0Na5}l)^@HUA@@`!uHsv#ADq?zo zpYg=zwNdDhX!8d-c_aTCE$TUE&YT=*_Uzdn@p|suxjFOZ>sC!i70?@py>=vnsf* zABcEaGs-XfrT!6#bQYs~VAgKTFnuJ!bnJ?0)28K2ojNsV%9JU&(B#RJ)5w0N`*OK| z`t<2JGiT1ssjRH@!1;S5@3b(JN@!YiKc8p6`(ykD&dIg$6ywpV4`Zf9vFvPwk$;b-RBzM@e$?6 z$Wr6Kj#sx*H;kk5H85_N&9zL99XmEUX3Ut_op;_D8$EiorMO5+{3x7k%M;3nl;MCf)`3~PTphN*gfO?uFFfb?2Oo%oz0URm<_{0chZ(StP zy7A)7;!Z-5eOQ9|=>GQ#3c47zMPf055Qn6~`Fg)VYF()Ji^Krb0@CNj4)MQ7+@pI7 z#dtm>AoE0Vv_hZ9&?>Fx0ipFh-vYDFzD(4K);2o`R`S-nqw;^~(4k11VN)^LYMvyM zuZ)J|mf>gJyuI(hX_Ilj`(*GUpYu(#k%(N4=m6L3(|4-05#k#i8`h}8u zfdSTnU`@l*24pB09UW`tTteFdVUU+03lu$0AWuS z3Lbu|@%COl)MI>*T7o)|Z6GZLgu{+M6KfjBpyQr7wWYKAh7A-nm)K{_o3~0fE z1(rUi|N97(Q$;5Uuu@$CtVBR|1WY{#ZE7J<3Z*Iem>W*gm_!b~0S+8tc|L)t(PR+n zBKf6(P`B^H7A*?plyG=aZ6uEkX@fA|T8wuPQzc^dL{aa>ix;0JIzx2!k|j&d6I~#> z=>|;HqW+?D1jOe>eF02B^kyxzh_$5|ZO|}*Q6y8zM-xmqWj{Fr z!+;P~0)=CYAN?>A`-t;MlR&UZj!~<333GBto~P&G*gR4}@`ib&UMdhyg>f->ju^gB z%wD!^*|IN+1}tB`{94f<(U(Qnnb!k#&o%n|N`2>2z9TwE+Hxj9XuVznqbr)FXSD?c zfX5sp3_@qFt~qj&9st-r2~sLSiyp|@A0$%IR6hQ`vN}B|N8UMCL~M^s64*U9-ww%( zdLngg{46o>1;aQ@Uw`kt_ug{PJ@*XOb;!zeo7h^qzSB?97He1`#{o6&^U))Y*l%WeWx;F}hfb8oVs5k;o^b#D_fKE5X3cET{IzS>E)*@& z>qWfQbzW6fRi*Bkrh5To3_wUDZqZtp$7T2sqY1tBtS&fP0nm~VNmj|eBp?q2mr4I& zDq7Vj0D@n#($_Z@WH#+ zuV24PR3%!sVZ(+8MC}JzzH;5Vb<1?`0s%5ZU`!AYBL(8^_zgh7r(j}e8_y-j zAkhKR$)1%j|TsiBx z8*chZHUPNyzS*_=zaCw%P)f9VlFVU*wCX0Uf0dqdzCh?hcqCaOV-(~04geu$5G!{g z!v2hAkO&{Kx)cD3_-XAQ^%wITZ1VM9vCwZ%7AJ}Hm3m$z=?_BslH7PCh+4nhwfnz= zAZ*>b^}E})ZTp_C-{1b&V?TId(c)Kse`4v!!TVnB+wTLJ&@6#aE)c$IOaU!8QyS1y z8qiT*zm)(eU|;MHL9t(#NmI8-Mud#@1Hbx557UQO-;>Dw{PG6Gw8`DpZuxlN4u&w?tiak z8!vlv>9N^!)?*3+pp5l-juNG_$rz;pC7d0knH~UWr(P4LeE^sY(JS24n;rTO6e*9% zf-DRG!-o%#iwb0G#Nkc?fNTx>$NAkV0LI9P&64!**|>4zrngIbrvdQPQ&0Uu^opTh z?AWp6B|v%YimUz+1Ylc5#a012Lk{sy!XpV45rMQ>;kS!Spp7J6WRBV%0AYS0a83w6 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 Date: Mon, 28 Oct 2013 16:36:26 -0400 Subject: [PATCH 068/202] [Android] More cleanup. We don't need the preference to enable/disable the overlay in the video preferences anymore. --- Source/Android/res/values-ja/strings.xml | 1 - Source/Android/res/values/strings.xml | 1 - Source/Android/res/xml/video_prefs.xml | 6 ------ 3 files changed, 8 deletions(-) diff --git a/Source/Android/res/values-ja/strings.xml b/Source/Android/res/values-ja/strings.xml index ee3bb8d3e757..598799c30e5d 100644 --- a/Source/Android/res/values-ja/strings.xml +++ b/Source/Android/res/values-ja/strings.xml @@ -91,7 +91,6 @@ %s FPSを表示 エミュレーション速度の指標として、画面左上に毎秒レンダリングされた フレーム数を表示します。 - 画面上のコントロールを描画 画質向上の設定 内部解像度の変更 diff --git a/Source/Android/res/values/strings.xml b/Source/Android/res/values/strings.xml index 29e5a67b30e0..517a80dbd3be 100644 --- a/Source/Android/res/values/strings.xml +++ b/Source/Android/res/values/strings.xml @@ -91,7 +91,6 @@ %s Show FPS Show the number of frames rendered per second as a measure of emulation speed. - Draw on-screen controls Enhancements Internal Resolution diff --git a/Source/Android/res/xml/video_prefs.xml b/Source/Android/res/xml/video_prefs.xml index 5228c117ffb5..2f9c7eb36eba 100644 --- a/Source/Android/res/xml/video_prefs.xml +++ b/Source/Android/res/xml/video_prefs.xml @@ -136,10 +136,4 @@ android:key="showFPS" android:summary="@string/show_fps_descrip" android:title="@string/show_fps"/> - \ No newline at end of file From a038ac4884137ae741a6ebe234c8eb2e057b5718 Mon Sep 17 00:00:00 2001 From: comex Date: Sun, 27 Oct 2013 03:03:02 -0400 Subject: [PATCH 069/202] VS2013 stuff. --- Externals/enet/enet.vcxproj | 76 ++++++++++++++++++++++++ Externals/enet/enet.vcxproj.filters | 23 ------- Source/Core/Common/Common.vcxproj | 8 ++- Source/Core/Common/Src/ChunkFile.h | 6 +- Source/Core/Common/Src/TraversalClient.h | 1 + Source/Core/Core/Core.vcxproj | 3 + Source/Core/DolphinWX/DolphinWX.vcxproj | 5 +- Source/VSProps/Base.props | 1 - Source/dolphin-emu.sln | 20 +++---- 9 files changed, 101 insertions(+), 42 deletions(-) create mode 100644 Externals/enet/enet.vcxproj delete mode 100644 Externals/enet/enet.vcxproj.filters diff --git a/Externals/enet/enet.vcxproj b/Externals/enet/enet.vcxproj new file mode 100644 index 000000000000..41b605b9316a --- /dev/null +++ b/Externals/enet/enet.vcxproj @@ -0,0 +1,76 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {CBC76802-C128-4B17-BF6C-23B08C313E5E} + + + + StaticLibrary + v120 + Unicode + + + true + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Externals/enet/enet.vcxproj.filters b/Externals/enet/enet.vcxproj.filters deleted file mode 100644 index 695c2a58f9d1..000000000000 --- a/Externals/enet/enet.vcxproj.filters +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Source/Core/Common/Common.vcxproj b/Source/Core/Common/Common.vcxproj index 52e65b8d9003..256c1e7d3ebf 100644 --- a/Source/Core/Common/Common.vcxproj +++ b/Source/Core/Common/Common.vcxproj @@ -82,13 +82,13 @@ - - + + @@ -124,6 +124,7 @@ + @@ -138,6 +139,9 @@ {bdb6578b-0691-4e80-a46c-df21639fd3b8} + + {CBC76802-C128-4B17-BF6C-23B08C313E5E} + {41279555-f94f-4ebc-99de-af863c10c5c4} diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index 8bde868aa5a3..02fcefda8c86 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -15,6 +15,7 @@ // - Zero backwards/forwards compatibility // - Serialization code for anything complex has to be manually written. +#include #include #include #include @@ -160,8 +161,9 @@ class PWBuffer class Packet; // ewww -#if _LIBCPP_VERSION -#define IsTriviallyCopyable(T) std::is_trivially_copyable::value +#if _LIBCPP_VERSION || defined(_WIN32) +// is_pod is needed due to a VS2013 bug: https://connect.microsoft.com/VisualStudio/feedback/details/806233 +#define IsTriviallyCopyable(T) (std::is_trivially_copyable::value || std::is_pod::value) #elif __GNUC__ #define IsTriviallyCopyable(T) std::has_trivial_copy_constructor::value #elif defined(_WIN32) diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index ddcbe2b262cd..5e2f3c1e829b 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -8,6 +8,7 @@ #include "TraversalProto.h" #include "enet/enet.h" #include +#include #include #include "Timer.h" #include "ChunkFile.h" diff --git a/Source/Core/Core/Core.vcxproj b/Source/Core/Core/Core.vcxproj index a88d745611ae..65d368ea449b 100644 --- a/Source/Core/Core/Core.vcxproj +++ b/Source/Core/Core/Core.vcxproj @@ -440,6 +440,9 @@ {8ada04d7-6db1-4da4-ab55-64fb12a0997b} + + {CBC76802-C128-4B17-BF6C-23B08C313E5E} + {349ee8f9-7d25-4909-aaf5-ff3fade72187} diff --git a/Source/Core/DolphinWX/DolphinWX.vcxproj b/Source/Core/DolphinWX/DolphinWX.vcxproj index 13c0c69e31e5..7dbc1e0c3b88 100644 --- a/Source/Core/DolphinWX/DolphinWX.vcxproj +++ b/Source/Core/DolphinWX/DolphinWX.vcxproj @@ -179,9 +179,6 @@ {ab993f38-c31d-4897-b139-a620c42bc565} - - {31643fdb-1bb8-4965-9de7-000fc88d35ae} - {93d73454-2512-424e-9cda-4bb357fe13dd} @@ -240,4 +237,4 @@ - \ No newline at end of file + diff --git a/Source/VSProps/Base.props b/Source/VSProps/Base.props index 32cccdb09121..be8d7c5b79e0 100644 --- a/Source/VSProps/Base.props +++ b/Source/VSProps/Base.props @@ -46,7 +46,6 @@ $(ExternalsDir)GLew\include;%(AdditionalIncludeDirectories) $(ExternalsDir)libusbx\libusb;%(AdditionalIncludeDirectories) $(ExternalsDir)LZO;%(AdditionalIncludeDirectories) - $(ExternalsDir)miniupnpc\src;%(AdditionalIncludeDirectories) $(ExternalsDir)polarssl\include;%(AdditionalIncludeDirectories) $(ExternalsDir)portaudio\include;%(AdditionalIncludeDirectories) $(ExternalsDir)SDL2-2.0.0\include;%(AdditionalIncludeDirectories) diff --git a/Source/dolphin-emu.sln b/Source/dolphin-emu.sln index 97618922dd8b..61399829051f 100644 --- a/Source/dolphin-emu.sln +++ b/Source/dolphin-emu.sln @@ -27,7 +27,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dolphin Core", "Dolphin Cor EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LZO", "..\Externals\LZO\LZO.vcxproj", "{AB993F38-C31D-4897-B139-A620C42BC565}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "miniupnpc", "..\Externals\miniupnpc\miniupnpc.vcxproj", "{31643FDB-1BB8-4965-9DE7-000FC88D35AE}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "enet", "..\Externals\enet\enet.vcxproj", "{CBC76802-C128-4B17-BF6C-23B08C313E5E}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "png", "..\Externals\libpng\png\png.vcxproj", "{4C9F135B-A85E-430C-BAD4-4C67EF5FC12C}" EndProject @@ -151,14 +151,14 @@ Global {AB993F38-C31D-4897-B139-A620C42BC565}.Release|Win32.Build.0 = Release|Win32 {AB993F38-C31D-4897-B139-A620C42BC565}.Release|x64.ActiveCfg = Release|x64 {AB993F38-C31D-4897-B139-A620C42BC565}.Release|x64.Build.0 = Release|x64 - {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Debug|Win32.ActiveCfg = Debug|Win32 - {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Debug|Win32.Build.0 = Debug|Win32 - {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Debug|x64.ActiveCfg = Debug|x64 - {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Debug|x64.Build.0 = Debug|x64 - {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Release|Win32.ActiveCfg = Release|Win32 - {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Release|Win32.Build.0 = Release|Win32 - {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Release|x64.ActiveCfg = Release|x64 - {31643FDB-1BB8-4965-9DE7-000FC88D35AE}.Release|x64.Build.0 = Release|x64 + {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Debug|Win32.ActiveCfg = Debug|Win32 + {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Debug|Win32.Build.0 = Debug|Win32 + {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Debug|x64.ActiveCfg = Debug|x64 + {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Debug|x64.Build.0 = Debug|x64 + {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Release|Win32.ActiveCfg = Release|Win32 + {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Release|Win32.Build.0 = Release|Win32 + {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Release|x64.ActiveCfg = Release|x64 + {CBC76802-C128-4B17-BF6C-23B08C313E5E}.Release|x64.Build.0 = Release|x64 {4C9F135B-A85E-430C-BAD4-4C67EF5FC12C}.Debug|Win32.ActiveCfg = Debug|Win32 {4C9F135B-A85E-430C-BAD4-4C67EF5FC12C}.Debug|Win32.Build.0 = Debug|Win32 {4C9F135B-A85E-430C-BAD4-4C67EF5FC12C}.Debug|x64.ActiveCfg = Debug|x64 @@ -301,7 +301,7 @@ Global {AAD1BCD6-9804-44A5-A5FC-4782EA00E9D4} = {15670B2E-CED6-4ED5-94CE-A00B1B2B5BA6} {8ADA04D7-6DB1-4DA4-AB55-64FB12A0997B} = {87ADDFF9-5768-4DA2-A33B-2477593D6677} {AB993F38-C31D-4897-B139-A620C42BC565} = {87ADDFF9-5768-4DA2-A33B-2477593D6677} - {31643FDB-1BB8-4965-9DE7-000FC88D35AE} = {87ADDFF9-5768-4DA2-A33B-2477593D6677} + {CBC76802-C128-4B17-BF6C-23B08C313E5E} = {87ADDFF9-5768-4DA2-A33B-2477593D6677} {4C9F135B-A85E-430C-BAD4-4C67EF5FC12C} = {87ADDFF9-5768-4DA2-A33B-2477593D6677} {B441CC62-877E-4B3F-93E0-0DE80544F705} = {87ADDFF9-5768-4DA2-A33B-2477593D6677} {FF213B23-2C26-4214-9F88-85271E557E87} = {87ADDFF9-5768-4DA2-A33B-2477593D6677} From 998ac2fa32f2ea0088e0c3ac8e2152a3f493a042 Mon Sep 17 00:00:00 2001 From: comex Date: Sun, 27 Oct 2013 01:39:46 -0400 Subject: [PATCH 070/202] Fix Linux build issues. --- Source/Core/Common/Src/ChunkFile.h | 4 ++-- Source/Core/Common/Src/TraversalClient.cpp | 1 - Source/Core/Core/Src/IOSyncBackends.cpp | 2 +- Source/Core/Core/Src/NetPlayClient.cpp | 2 -- Source/pch.h | 1 + 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index 02fcefda8c86..3a6f9d8aefbf 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -132,10 +132,10 @@ class PWBuffer } u8* release_data() { - u8* data = m_Data; + u8* _data = m_Data; m_Data = NULL; m_Size = m_Capacity = 0; - return data; + return _data; } u8* data() { return m_Data; } const u8* data() const { return m_Data; } diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index d046a2808bcf..364a810b6451 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -333,7 +333,6 @@ void ENetHostClient::OnReceive(ENetEvent* event, Packet&& packet) goto skip; } - Packet sub; packet.Do(buf); if (packet.failure) goto failure; diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index 4e7516251f90..8dd39ce344e0 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -185,7 +185,7 @@ void BackendNetPlay::ProcessIncomingPackets() void BackendNetPlay::ProcessPacket(Packet&& p) { - MessageId packetType; + MessageId packetType = 0; u8 classId, index, flags; p.Do(packetType); if (packetType != NP_MSG_PAD_BUFFER) diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 68ba36cab35e..4db66a882ae5 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -153,7 +153,6 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) if (packet.failure) return OnDisconnect(InvalidPacket); - size_t offset; switch (mid) { case NP_MSG_PLAYER_JOIN : @@ -237,7 +236,6 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) packet.Do(m_delay); if (packet.failure) return OnDisconnect(InvalidPacket); - offset = 0; if (!m_backend) break; /* fall through */ diff --git a/Source/pch.h b/Source/pch.h index 64c135471738..2851adefcac9 100644 --- a/Source/pch.h +++ b/Source/pch.h @@ -1,4 +1,5 @@ #include "Common.h" +#include "Thread.h" #include #include #include From 3c99fef8fc553a6f88633514bcc02131ba1828e5 Mon Sep 17 00:00:00 2001 From: comex Date: Mon, 28 Oct 2013 19:03:44 -0400 Subject: [PATCH 071/202] Fix config. --- Source/Core/VideoCommon/Src/VideoConfig.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Core/VideoCommon/Src/VideoConfig.cpp b/Source/Core/VideoCommon/Src/VideoConfig.cpp index e8b3362a7134..b96b243e231a 100644 --- a/Source/Core/VideoCommon/Src/VideoConfig.cpp +++ b/Source/Core/VideoCommon/Src/VideoConfig.cpp @@ -127,8 +127,9 @@ void VideoConfig::GameIniLoad() // XXX: Again, bad place to put OSD messages at (see delroth's comment above) // XXX: This will add an OSD message for each projection hack value... meh #define CHECK_SETTING(section, key, var) do { \ - decltype(var) temp = var; \ - if (iniFile.Get(section, key, &var) && var != temp) { \ + auto temp = var; \ + if (iniFile.Get(section, key, &temp) && var != temp) { \ + var = temp; \ char buf[256]; \ snprintf(buf, sizeof(buf), "Note: Option \"%s\" is overridden by game ini.", key); \ OSD::AddMessage(buf, 7500); \ From 1b5ae60e6a20eaf2f5540fffde747f874f976a59 Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 29 Oct 2013 03:41:59 -0400 Subject: [PATCH 072/202] Don't double the delay. --- Source/Core/Core/Src/IOSyncBackends.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index 8dd39ce344e0..bd06acec4ab1 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -240,8 +240,7 @@ void BackendNetPlay::ProcessPacket(Packet&& p) { u32 delay; p.Do(delay); - // XXX - it should be possible to have a half-frame delay. - m_Delay = delay * 2; + m_Delay = delay; break; } default: From b4985597278fc3e76f08cbf0cd6bd6ab4dd4219a Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 29 Oct 2013 03:56:02 -0400 Subject: [PATCH 073/202] Add some stats. --- Source/Core/Common/Src/TraversalClient.cpp | 52 +++++++++++++++++++--- Source/Core/Common/Src/TraversalClient.h | 3 ++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index 364a810b6451..6e74c504d619 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -186,9 +186,11 @@ void ENetHostClient::BroadcastPacket(Packet&& packet, ENetPeer* except) void ENetHostClient::SendPacket(ENetPeer* peer, Packet&& packet) { Packet container; - container.W((u16) m_PeerInfo[peer - m_Host->peers].m_OutgoingSequenceNumber++); + auto& pi = m_PeerInfo[peer - m_Host->peers]; + container.W((u16) pi.m_OutgoingSequenceNumber++); container.Do((PointerWrap&) packet); ENetUtil::SendPacket(peer, std::move(container)); + pi.m_SentPackets++; } void ENetHostClient::MaybeProcessPacketQueue() @@ -238,12 +240,48 @@ void ENetHostClient::ProcessPacketQueue() info.m_DidSendReliably = info.m_DidSendReliably || needReliable; } if (p.vec->size()) + { ENetUtil::SendPacket(peer, std::move(p), needReliable); + pi.m_SentPackets++; + } } while (numToRemove--) m_OutgoingPacketInfo.pop_front(); } +void ENetHostClient::PrintStats() +{ +#if 1 + if (m_StatsTimer.GetTimeDifference() > 5000) + { + m_StatsTimer.Update(); + for (ENetPeer* peer = m_Host->peers, * end = &m_Host->peers[m_Host->peerCount]; peer != end; peer++) + { + if (peer->state != ENET_PEER_STATE_CONNECTED) + continue; + auto& pi = m_PeerInfo[peer - m_Host->peers]; + char ip[64] = "?"; + enet_address_get_host_ip(&peer->address, ip, sizeof(ip)); + WARN_LOG(NETPLAY, "%speer %u (%s): %f%%+-%f%% packet loss, %u+-%u ms round trip time, %f%% throttle, %u/%u outgoing, %u/%u incoming, %d packets sent since last time\n", + !m_isTraversalClient ? "(CLIENT) " : "", // ew + (unsigned) peer->incomingPeerID, + ip, + peer->packetLoss / (float) ENET_PEER_PACKET_LOSS_SCALE, + peer->packetLossVariance / (float) ENET_PEER_PACKET_LOSS_SCALE, + peer->roundTripTime, + peer->roundTripTimeVariance, + peer->packetThrottle / (float) ENET_PEER_PACKET_THROTTLE_SCALE, + (unsigned) enet_list_size(&peer->outgoingReliableCommands), + (unsigned) enet_list_size(&peer->outgoingUnreliableCommands), + peer->channels != NULL ? (unsigned) enet_list_size(&peer->channels->incomingReliableCommands) : 0, + peer->channels != NULL ? (unsigned) enet_list_size(&peer->channels->incomingUnreliableCommands) : 0, + pi.m_SentPackets); + pi.m_SentPackets = 0; + } + } +#endif +} + void ENetHostClient::ThreadFunc() { ASSUME_ON(NET); @@ -272,6 +310,8 @@ void ENetHostClient::ThreadFunc() HandleResends(); + PrintStats(); + // Even if there was nothing, forward it as a wakeup. if (m_Client) { @@ -285,10 +325,12 @@ void ENetHostClient::ThreadFunc() size_t pid = event.peer - m_Host->peers; if (pid >= m_PeerInfo.size()) m_PeerInfo.resize(pid + 1); - m_PeerInfo[pid].m_IncomingPackets.clear(); - m_PeerInfo[pid].m_IncomingSequenceNumber = 0; - m_PeerInfo[pid].m_OutgoingSequenceNumber = 0; - m_PeerInfo[pid].m_ConnectTicker = m_GlobalTicker++; + auto& pi = m_PeerInfo[pid]; + pi.m_IncomingPackets.clear(); + pi.m_IncomingSequenceNumber = 0; + pi.m_OutgoingSequenceNumber = 0; + pi.m_ConnectTicker = m_GlobalTicker++; + pi.m_SentPackets = 0; } /* fall through */ default: diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index 5e2f3c1e829b..ce299767c645 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -82,6 +82,7 @@ class ENetHostClient void SendPacket(ENetPeer* peer, Packet&& packet) ON(NET); void MaybeProcessPacketQueue() ON(NET); void ProcessPacketQueue() ON(NET); + void PrintStats() ON(NET); TraversalClientClient* m_Client; ENetHost* m_Host; @@ -109,6 +110,7 @@ class ENetHostClient u16 m_OutgoingSequenceNumber; u16 m_GlobalSeqToSeq[65536]; u64 m_ConnectTicker; + int m_SentPackets; }; void ThreadFunc() /* ON(NET) */; @@ -123,6 +125,7 @@ class ENetHostClient std::deque m_OutgoingPacketInfo ACCESS_ON(NET); Common::Timer m_SendTimer ACCESS_ON(NET); + Common::Timer m_StatsTimer ACCESS_ON(NET); std::vector m_PeerInfo ACCESS_ON(NET); u16 m_GlobalSequenceNumber ACCESS_ON(NET); u64 m_GlobalTicker ACCESS_ON(NET); From 1aa06b8fa4105d22cac0dd847230215099bbeb78 Mon Sep 17 00:00:00 2001 From: degasus Date: Thu, 7 Nov 2013 06:19:35 +0100 Subject: [PATCH 074/202] jit: change our linking module to be able to handle arbitrary exit addresses And also do this for all JIT backends... --- Source/Core/Core/Src/PowerPC/Jit64/Jit.cpp | 15 ++++++---- Source/Core/Core/Src/PowerPC/Jit64/Jit.h | 2 +- .../Core/Src/PowerPC/Jit64/Jit_Branch.cpp | 10 +++---- .../Core/Src/PowerPC/Jit64/Jit_Integer.cpp | 10 +++---- .../Core/Src/PowerPC/Jit64/Jit_LoadStore.cpp | 2 +- .../Src/PowerPC/Jit64/Jit_SystemRegisters.cpp | 2 +- .../Core/Core/Src/PowerPC/Jit64IL/IR_X86.cpp | 13 +++++---- .../Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp | 18 +++++++----- Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.h | 4 +-- Source/Core/Core/Src/PowerPC/JitArm32/Jit.cpp | 13 +++++---- Source/Core/Core/Src/PowerPC/JitArm32/Jit.h | 2 +- .../Src/PowerPC/JitArm32/JitArm_Branch.cpp | 10 +++---- .../Src/PowerPC/JitArm32/JitArm_LoadStore.cpp | 2 +- .../JitArm32/JitArm_SystemRegisters.cpp | 2 +- .../Core/Core/Src/PowerPC/JitArmIL/IR_Arm.cpp | 11 +++---- .../Core/Core/Src/PowerPC/JitArmIL/JitIL.cpp | 15 ++++++---- Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.h | 4 +-- .../Core/Src/PowerPC/JitCommon/JitCache.cpp | 29 +++++++------------ .../Core/Src/PowerPC/JitCommon/JitCache.h | 11 ++++--- 19 files changed, 92 insertions(+), 83 deletions(-) diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit.cpp b/Source/Core/Core/Src/PowerPC/Jit64/Jit.cpp index 394c7bc86ae3..87fb248c1f7e 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit.cpp @@ -276,7 +276,7 @@ void Jit64::Cleanup() ABI_CallFunctionCCC((void *)&PowerPC::UpdatePerformanceMonitor, js.downcountAmount, jit->js.numLoadStoreInst, jit->js.numFloatingPointInst); } -void Jit64::WriteExit(u32 destination, int exit_num) +void Jit64::WriteExit(u32 destination) { Cleanup(); @@ -284,8 +284,9 @@ void Jit64::WriteExit(u32 destination, int exit_num) //If nobody has taken care of this yet (this can be removed when all branches are done) JitBlock *b = js.curBlock; - b->exitAddress[exit_num] = destination; - b->exitPtrs[exit_num] = GetWritableCodePtr(); + JitBlock::LinkData linkData; + linkData.exitAddress = destination; + linkData.exitPtrs = GetWritableCodePtr(); // Link opportunity! if (jo.enableBlocklink) @@ -295,12 +296,14 @@ void Jit64::WriteExit(u32 destination, int exit_num) { // It exists! Joy of joy! JMP(blocks.GetBlock(block)->checkedEntry, true); - b->linkStatus[exit_num] = true; + linkData.linkStatus = true; return; } } MOV(32, M(&PC), Imm32(destination)); JMP(asm_routines.dispatcher, true); + + b->linkData.push_back(linkData); } void Jit64::WriteExitDestInEAX() @@ -625,7 +628,7 @@ const u8* Jit64::DoJit(u32 em_address, PPCAnalyst::CodeBuffer *code_buf, JitBloc TEST(32, M((void*)PowerPC::GetStatePtr()), Imm32(0xFFFFFFFF)); FixupBranch noBreakpoint = J_CC(CC_Z); - WriteExit(ops[i].address, 0); + WriteExit(ops[i].address); SetJumpTarget(noBreakpoint); } @@ -707,7 +710,7 @@ const u8* Jit64::DoJit(u32 em_address, PPCAnalyst::CodeBuffer *code_buf, JitBloc { gpr.Flush(FLUSH_ALL); fpr.Flush(FLUSH_ALL); - WriteExit(nextPC, 0); + WriteExit(nextPC); } b->flags = js.block_flags; diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit.h b/Source/Core/Core/Src/PowerPC/Jit64/Jit.h index 8676b1cb5a8b..b4997be74bd0 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit.h +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit.h @@ -99,7 +99,7 @@ class Jit64 : public Jitx86Base // Utilities for use by opcodes - void WriteExit(u32 destination, int exit_num); + void WriteExit(u32 destination); void WriteExitDestInEAX(); void WriteExceptionExit(); void WriteExternalExceptionExit(); diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit_Branch.cpp b/Source/Core/Core/Src/PowerPC/Jit64/Jit_Branch.cpp index b0f3f1cd1853..a1e28ac6ea80 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit_Branch.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit_Branch.cpp @@ -91,7 +91,7 @@ void Jit64::bx(UGeckoInstruction inst) // make idle loops go faster js.downcountAmount += 8; } - WriteExit(destination, 0); + WriteExit(destination); } // TODO - optimize to hell and beyond @@ -136,13 +136,13 @@ void Jit64::bcx(UGeckoInstruction inst) destination = SignExt16(inst.BD << 2); else destination = js.compilerPC + SignExt16(inst.BD << 2); - WriteExit(destination, 0); + WriteExit(destination); if ((inst.BO & BO_DONT_CHECK_CONDITION) == 0) SetJumpTarget( pConditionDontBranch ); if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) SetJumpTarget( pCTRDontBranch ); - WriteExit(js.compilerPC + 4, 1); + WriteExit(js.compilerPC + 4); } void Jit64::bcctrx(UGeckoInstruction inst) @@ -190,7 +190,7 @@ void Jit64::bcctrx(UGeckoInstruction inst) WriteExitDestInEAX(); // Would really like to continue the block here, but it ends. TODO. SetJumpTarget(b); - WriteExit(js.compilerPC + 4, 1); + WriteExit(js.compilerPC + 4); } } @@ -245,5 +245,5 @@ void Jit64::bclrx(UGeckoInstruction inst) SetJumpTarget( pConditionDontBranch ); if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) SetJumpTarget( pCTRDontBranch ); - WriteExit(js.compilerPC + 4, 1); + WriteExit(js.compilerPC + 4); } diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/Src/PowerPC/Jit64/Jit_Integer.cpp index 244a051aaa93..8299f2b044ff 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit_Integer.cpp @@ -400,7 +400,7 @@ void Jit64::cmpXX(UGeckoInstruction inst) destination = SignExt16(js.next_inst.BD << 2); else destination = js.next_compilerPC + SignExt16(js.next_inst.BD << 2); - WriteExit(destination, 0); + WriteExit(destination); } else if ((js.next_inst.OPCD == 19) && (js.next_inst.SUBOP10 == 528)) // bcctrx { @@ -424,7 +424,7 @@ void Jit64::cmpXX(UGeckoInstruction inst) } else { - WriteExit(js.next_compilerPC + 4, 0); + WriteExit(js.next_compilerPC + 4); } js.cancel = true; @@ -507,7 +507,7 @@ void Jit64::cmpXX(UGeckoInstruction inst) destination = SignExt16(js.next_inst.BD << 2); else destination = js.next_compilerPC + SignExt16(js.next_inst.BD << 2); - WriteExit(destination, 0); + WriteExit(destination); } else if ((js.next_inst.OPCD == 19) && (js.next_inst.SUBOP10 == 528)) // bcctrx { @@ -534,7 +534,7 @@ void Jit64::cmpXX(UGeckoInstruction inst) if (!!(4 & test_bit) == condition) SetJumpTarget(continue2); if (!!(2 & test_bit) == condition) SetJumpTarget(continue1); - WriteExit(js.next_compilerPC + 4, 1); + WriteExit(js.next_compilerPC + 4); js.cancel = true; } @@ -2221,5 +2221,5 @@ void Jit64::twx(UGeckoInstruction inst) SetJumpTarget(exit3); SetJumpTarget(exit4); SetJumpTarget(exit5); - WriteExit(js.compilerPC + 4, 1); + WriteExit(js.compilerPC + 4); } diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit_LoadStore.cpp b/Source/Core/Core/Src/PowerPC/Jit64/Jit_LoadStore.cpp index a1c3bd971c11..1b41c2f1e93b 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit_LoadStore.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit_LoadStore.cpp @@ -480,5 +480,5 @@ void Jit64::stmw(UGeckoInstruction inst) void Jit64::icbi(UGeckoInstruction inst) { Default(inst); - WriteExit(js.compilerPC + 4, 0); + WriteExit(js.compilerPC + 4); } diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit_SystemRegisters.cpp b/Source/Core/Core/Src/PowerPC/Jit64/Jit_SystemRegisters.cpp index 573feb375690..8282758a74c1 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit_SystemRegisters.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit_SystemRegisters.cpp @@ -137,7 +137,7 @@ void Jit64::mtmsr(UGeckoInstruction inst) SetJumpTarget(noExceptionsPending); SetJumpTarget(eeDisabled); - WriteExit(js.compilerPC + 4, 0); + WriteExit(js.compilerPC + 4); js.firstFPInstructionFound = false; } diff --git a/Source/Core/Core/Src/PowerPC/Jit64IL/IR_X86.cpp b/Source/Core/Core/Src/PowerPC/Jit64IL/IR_X86.cpp index 69dc1c2bd3c1..b086c0203844 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64IL/IR_X86.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64IL/IR_X86.cpp @@ -552,7 +552,8 @@ static void regEmitICmpInst(RegInfo& RI, InstLoc I, CCFlags flag) { static void regWriteExit(RegInfo& RI, InstLoc dest) { if (isImm(*dest)) { - RI.Jit->WriteExit(RI.Build->GetImmValue(dest), RI.exitNumber++); + RI.exitNumber++; + RI.Jit->WriteExit(RI.Build->GetImmValue(dest)); } else { RI.Jit->WriteExitDestInOpArg(regLocForInst(RI, dest)); } @@ -564,7 +565,7 @@ static bool checkIsSNAN() { return MathUtil::IsSNAN(isSNANTemp[0][0]) || MathUtil::IsSNAN(isSNANTemp[1][0]); } -static void DoWriteCode(IRBuilder* ibuild, JitIL* Jit) { +static void DoWriteCode(IRBuilder* ibuild, JitIL* Jit, u32 exitAddress) { //printf("Writing block: %x\n", js.blockStart); RegInfo RI(Jit, ibuild->getFirstInst(), ibuild->getNumInsts()); RI.Build = ibuild; @@ -1791,7 +1792,7 @@ static void DoWriteCode(IRBuilder* ibuild, JitIL* Jit) { Jit->ABI_CallFunction(reinterpret_cast(&PowerPC::CheckBreakPoints)); Jit->TEST(32, M((void*)PowerPC::GetStatePtr()), Imm32(0xFFFFFFFF)); FixupBranch noBreakpoint = Jit->J_CC(CC_Z); - Jit->WriteExit(InstLoc, 0); + Jit->WriteExit(InstLoc); Jit->SetJumpTarget(noBreakpoint); break; } @@ -1819,10 +1820,10 @@ static void DoWriteCode(IRBuilder* ibuild, JitIL* Jit) { } } - Jit->WriteExit(jit->js.curBlock->exitAddress[0], 0); + Jit->WriteExit(exitAddress); Jit->UD2(); } -void JitIL::WriteCode() { - DoWriteCode(&ibuild, this); +void JitIL::WriteCode(u32 exitAddress) { + DoWriteCode(&ibuild, this, exitAddress); } diff --git a/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp b/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp index d7ccddb83c45..088336c43b18 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp @@ -379,7 +379,7 @@ void JitIL::Cleanup() ABI_CallFunctionCCC((void *)&PowerPC::UpdatePerformanceMonitor, js.downcountAmount, jit->js.numLoadStoreInst, jit->js.numFloatingPointInst); } -void JitIL::WriteExit(u32 destination, int exit_num) +void JitIL::WriteExit(u32 destination) { Cleanup(); if (SConfig::GetInstance().m_LocalCoreStartupParameter.bJITILTimeProfiling) { @@ -389,8 +389,9 @@ void JitIL::WriteExit(u32 destination, int exit_num) //If nobody has taken care of this yet (this can be removed when all branches are done) JitBlock *b = js.curBlock; - b->exitAddress[exit_num] = destination; - b->exitPtrs[exit_num] = GetWritableCodePtr(); + JitBlock::LinkData linkData; + linkData.exitAddress = destination; + linkData.exitPtrs = GetWritableCodePtr(); // Link opportunity! int block = blocks.GetBlockNumberFromStartAddress(destination); @@ -398,13 +399,14 @@ void JitIL::WriteExit(u32 destination, int exit_num) { // It exists! Joy of joy! JMP(blocks.GetBlock(block)->checkedEntry, true); - b->linkStatus[exit_num] = true; + linkData.linkStatus = true; } else { MOV(32, M(&PC), Imm32(destination)); JMP(asm_routines.dispatcher, true); } + b->linkData.push_back(linkData); } void JitIL::WriteExitDestInOpArg(const Gen::OpArg& arg) @@ -539,14 +541,16 @@ const u8* JitIL::DoJit(u32 em_address, PPCAnalyst::CodeBuffer *code_buf, JitBloc // Analyze the block, collect all instructions it is made of (including inlining, // if that is enabled), reorder instructions for optimal performance, and join joinable instructions. - b->exitAddress[0] = em_address; + u32 exitAddress = em_address; + u32 merged_addresses[32]; const int capacity_of_merged_addresses = sizeof(merged_addresses) / sizeof(merged_addresses[0]); int size_of_merged_addresses = 0; if (!memory_exception) { // If there is a memory exception inside a block (broken_block==true), compile up to that instruction. - b->exitAddress[0] = PPCAnalyst::Flatten(em_address, &size, &js.st, &js.gpa, &js.fpa, broken_block, code_buf, blockSize, merged_addresses, capacity_of_merged_addresses, size_of_merged_addresses); + // TODO + exitAddress = PPCAnalyst::Flatten(em_address, &size, &js.st, &js.gpa, &js.fpa, broken_block, code_buf, blockSize, merged_addresses, capacity_of_merged_addresses, size_of_merged_addresses); } PPCAnalyst::CodeOp *ops = code_buf->codebuffer; @@ -705,7 +709,7 @@ const u8* JitIL::DoJit(u32 em_address, PPCAnalyst::CodeBuffer *code_buf, JitBloc } // Perform actual code generation - WriteCode(); + WriteCode(exitAddress); b->codeSize = (u32)(GetCodePtr() - normalEntry); b->originalSize = size; diff --git a/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.h b/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.h index 3f0e847d9159..8dfbcd522c97 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.h +++ b/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.h @@ -109,7 +109,7 @@ class JitIL : public JitILBase, public EmuCodeBlock // Utilities for use by opcodes - void WriteExit(u32 destination, int exit_num); + void WriteExit(u32 destination); void WriteExitDestInOpArg(const Gen::OpArg& arg); void WriteExceptionExit(); void WriteRfiExitDestInOpArg(const Gen::OpArg& arg); @@ -125,7 +125,7 @@ class JitIL : public JitILBase, public EmuCodeBlock void regimmop(int d, int a, bool binary, u32 value, Operation doop, void (Gen::XEmitter::*op)(int, const Gen::OpArg&, const Gen::OpArg&), bool Rc = false, bool carry = false); void fp_tri_op(int d, int a, int b, bool reversible, bool dupe, void (Gen::XEmitter::*op)(Gen::X64Reg, Gen::OpArg)); - void WriteCode(); + void WriteCode(u32 exitAddress); // OPCODES void unknown_instruction(UGeckoInstruction _inst) override; diff --git a/Source/Core/Core/Src/PowerPC/JitArm32/Jit.cpp b/Source/Core/Core/Src/PowerPC/JitArm32/Jit.cpp index 4083a383e976..eab7f3711a30 100644 --- a/Source/Core/Core/Src/PowerPC/JitArm32/Jit.cpp +++ b/Source/Core/Core/Src/PowerPC/JitArm32/Jit.cpp @@ -186,15 +186,16 @@ void JitArm::WriteExceptionExit() MOVI2R(A, (u32)asm_routines.testExceptions); B(A); } -void JitArm::WriteExit(u32 destination, int exit_num) +void JitArm::WriteExit(u32 destination) { Cleanup(); DoDownCount(); //If nobody has taken care of this yet (this can be removed when all branches are done) JitBlock *b = js.curBlock; - b->exitAddress[exit_num] = destination; - b->exitPtrs[exit_num] = GetWritableCodePtr(); + JitBlock::LinkData linkData; + linkData.exitAddress = destination; + linkData.exitPtrs = GetWritableCodePtr(); // Link opportunity! int block = blocks.GetBlockNumberFromStartAddress(destination); @@ -202,7 +203,7 @@ void JitArm::WriteExit(u32 destination, int exit_num) { // It exists! Joy of joy! B(blocks.GetBlock(block)->checkedEntry); - b->linkStatus[exit_num] = true; + linkData.linkStatus = true; } else { @@ -212,6 +213,8 @@ void JitArm::WriteExit(u32 destination, int exit_num) MOVI2R(A, (u32)asm_routines.dispatcher); B(A); } + + b->linkData.push_back(linkData); } void STACKALIGN JitArm::Run() @@ -496,7 +499,7 @@ const u8* JitArm::DoJit(u32 em_address, PPCAnalyst::CodeBuffer *code_buf, JitBlo if (broken_block) { printf("Broken Block going to 0x%08x\n", nextPC); - WriteExit(nextPC, 0); + WriteExit(nextPC); } b->flags = js.block_flags; diff --git a/Source/Core/Core/Src/PowerPC/JitArm32/Jit.h b/Source/Core/Core/Src/PowerPC/JitArm32/Jit.h index fc1911c5bfcc..4d1d2d8a4e60 100644 --- a/Source/Core/Core/Src/PowerPC/JitArm32/Jit.h +++ b/Source/Core/Core/Src/PowerPC/JitArm32/Jit.h @@ -109,7 +109,7 @@ class JitArm : public JitBase, public ArmGen::ARMXCodeBlock // Utilities for use by opcodes - void WriteExit(u32 destination, int exit_num); + void WriteExit(u32 destination); void WriteExitDestInR(ARMReg Reg); void WriteRfiExitDestInR(ARMReg Reg); void WriteExceptionExit(); diff --git a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_Branch.cpp b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_Branch.cpp index 1879e4af8557..c70a2b2f3de9 100644 --- a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_Branch.cpp +++ b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_Branch.cpp @@ -152,7 +152,7 @@ void JitArm::bx(UGeckoInstruction inst) MOVI2R(R14, (u32)asm_routines.testExceptions); B(R14); } - WriteExit(destination, 0); + WriteExit(destination); } void JitArm::bcx(UGeckoInstruction inst) @@ -207,14 +207,14 @@ void JitArm::bcx(UGeckoInstruction inst) destination = SignExt16(inst.BD << 2); else destination = js.compilerPC + SignExt16(inst.BD << 2); - WriteExit(destination, 0); + WriteExit(destination); if ((inst.BO & BO_DONT_CHECK_CONDITION) == 0) SetJumpTarget( pConditionDontBranch ); if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) SetJumpTarget( pCTRDontBranch ); - WriteExit(js.compilerPC + 4, 1); + WriteExit(js.compilerPC + 4); } void JitArm::bcctrx(UGeckoInstruction inst) { @@ -276,7 +276,7 @@ void JitArm::bcctrx(UGeckoInstruction inst) WriteExitDestInR(rA); SetJumpTarget(b); - WriteExit(js.compilerPC + 4, 1); + WriteExit(js.compilerPC + 4); } } void JitArm::bclrx(UGeckoInstruction inst) @@ -350,5 +350,5 @@ void JitArm::bclrx(UGeckoInstruction inst) SetJumpTarget( pConditionDontBranch ); if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) SetJumpTarget( pCTRDontBranch ); - WriteExit(js.compilerPC + 4, 1); + WriteExit(js.compilerPC + 4); } diff --git a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_LoadStore.cpp b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_LoadStore.cpp index f04c88d6c313..3983772aa6df 100644 --- a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_LoadStore.cpp +++ b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_LoadStore.cpp @@ -531,6 +531,6 @@ void JitArm::dcbst(UGeckoInstruction inst) void JitArm::icbi(UGeckoInstruction inst) { Default(inst); - WriteExit(js.compilerPC + 4, 0); + WriteExit(js.compilerPC + 4); } diff --git a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_SystemRegisters.cpp b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_SystemRegisters.cpp index e4ab630fce31..62b15e1d5e72 100644 --- a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_SystemRegisters.cpp +++ b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_SystemRegisters.cpp @@ -205,7 +205,7 @@ void JitArm::mtmsr(UGeckoInstruction inst) gpr.Flush(); fpr.Flush(); - WriteExit(js.compilerPC + 4, 0); + WriteExit(js.compilerPC + 4); } void JitArm::mfmsr(UGeckoInstruction inst) diff --git a/Source/Core/Core/Src/PowerPC/JitArmIL/IR_Arm.cpp b/Source/Core/Core/Src/PowerPC/JitArmIL/IR_Arm.cpp index 10620d918313..c1bee507fc4f 100644 --- a/Source/Core/Core/Src/PowerPC/JitArmIL/IR_Arm.cpp +++ b/Source/Core/Core/Src/PowerPC/JitArmIL/IR_Arm.cpp @@ -156,7 +156,8 @@ static ARMReg regEnsureInReg(RegInfo& RI, InstLoc I) { static void regWriteExit(RegInfo& RI, InstLoc dest) { if (isImm(*dest)) { - RI.Jit->WriteExit(RI.Build->GetImmValue(dest), RI.exitNumber++); + RI.exitNumber++; + RI.Jit->WriteExit(RI.Build->GetImmValue(dest)); } else { RI.Jit->WriteExitDestInReg(regLocForInst(RI, dest)); } @@ -281,7 +282,7 @@ static void regEmitCmp(RegInfo& RI, InstLoc I) { } } -static void DoWriteCode(IRBuilder* ibuild, JitArmIL* Jit) { +static void DoWriteCode(IRBuilder* ibuild, JitArmIL* Jit, u32 exitAddress) { RegInfo RI(Jit, ibuild->getFirstInst(), ibuild->getNumInsts()); RI.Build = ibuild; @@ -733,10 +734,10 @@ static void DoWriteCode(IRBuilder* ibuild, JitArmIL* Jit) { } } - Jit->WriteExit(jit->js.curBlock->exitAddress[0], 0); + Jit->WriteExit(exitAddress); Jit->BKPT(0x111); } -void JitArmIL::WriteCode() { - DoWriteCode(&ibuild, this); +void JitArmIL::WriteCode(u32 exitAddress) { + DoWriteCode(&ibuild, this, exitAddress); } diff --git a/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.cpp b/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.cpp index 0aac4d6722e2..44ebc8b6eb24 100644 --- a/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.cpp +++ b/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.cpp @@ -117,13 +117,14 @@ void JitArmIL::WriteExceptionExit() MOVI2R(R14, (u32)asm_routines.testExceptions); B(R14); } -void JitArmIL::WriteExit(u32 destination, int exit_num) +void JitArmIL::WriteExit(u32 destination) { DoDownCount(); //If nobody has taken care of this yet (this can be removed when all branches are done) JitBlock *b = js.curBlock; - b->exitAddress[exit_num] = destination; - b->exitPtrs[exit_num] = GetWritableCodePtr(); + JitBlock::LinkData linkData; + linkData.exitAddress = destination; + linkData.exitPtrs = GetWritableCodePtr(); // Link opportunity! int block = blocks.GetBlockNumberFromStartAddress(destination); @@ -131,7 +132,7 @@ void JitArmIL::WriteExit(u32 destination, int exit_num) { // It exists! Joy of joy! B(blocks.GetBlock(block)->checkedEntry); - b->linkStatus[exit_num] = true; + linkData.linkStatus = true; } else { @@ -140,6 +141,8 @@ void JitArmIL::WriteExit(u32 destination, int exit_num) MOVI2R(R14, (u32)asm_routines.dispatcher); B(R14); } + + b->linkData.push_back(linkData); } void JitArmIL::PrintDebug(UGeckoInstruction inst, u32 level) { @@ -347,12 +350,12 @@ const u8* JitArmIL::DoJit(u32 em_address, PPCAnalyst::CodeBuffer *code_buf, JitB if (broken_block) { printf("Broken Block going to 0x%08x\n", nextPC); - WriteExit(nextPC, 0); + WriteExit(nextPC); } // Perform actual code generation - WriteCode(); + WriteCode(nextPC); b->flags = js.block_flags; b->codeSize = (u32)(GetCodePtr() - normalEntry); b->originalSize = size; diff --git a/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.h b/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.h index 4dec87ddeba6..71b09a32510d 100644 --- a/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.h +++ b/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.h @@ -64,8 +64,8 @@ class JitArmIL : public JitILBase, public ArmGen::ARMXCodeBlock void Run(); void SingleStep(); // - void WriteCode(); - void WriteExit(u32 destination, int exit_num); + void WriteCode(u32 exitAddress); + void WriteExit(u32 destination); void WriteExitDestInReg(ARMReg Reg); void WriteRfiExitDestInR(ARMReg Reg); void WriteExceptionExit(); diff --git a/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.cpp b/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.cpp index d7c78d9d174f..b81dadd39440 100644 --- a/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.cpp +++ b/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.cpp @@ -35,8 +35,6 @@ op_agent_t agent; using namespace Gen; -#define INVALID_EXIT 0xFFFFFFFF - bool JitBaseBlockCache::IsFull() const { return GetNumBlocks() >= MAX_NUM_BLOCKS - 1; @@ -167,12 +165,6 @@ using namespace Gen; JitBlock &b = blocks[num_blocks]; b.invalid = false; b.originalAddress = em_address; - b.exitAddress[0] = INVALID_EXIT; - b.exitAddress[1] = INVALID_EXIT; - b.exitPtrs[0] = 0; - b.exitPtrs[1] = 0; - b.linkStatus[0] = false; - b.linkStatus[1] = false; num_blocks++; //commit the current block return num_blocks - 1; } @@ -193,10 +185,9 @@ using namespace Gen; block_map[std::make_pair(pAddr + 4 * b.originalSize - 1, pAddr)] = block_num; if (block_link) { - for (int i = 0; i < 2; i++) + for (const auto& e : b.linkData) { - if (b.exitAddress[i] != INVALID_EXIT) - links_to.insert(std::pair(b.exitAddress[i], block_num)); + links_to.insert(std::pair(e.exitAddress, block_num)); } LinkBlock(block_num); @@ -275,15 +266,15 @@ using namespace Gen; // This block is dead. Don't relink it. return; } - for (int e = 0; e < 2; e++) + for (auto& e : b.linkData) { - if (b.exitAddress[e] != INVALID_EXIT && !b.linkStatus[e]) + if (!e.linkStatus) { - int destinationBlock = GetBlockNumberFromStartAddress(b.exitAddress[e]); + int destinationBlock = GetBlockNumberFromStartAddress(e.exitAddress); if (destinationBlock != -1) { - WriteLinkBlock(b.exitPtrs[e], blocks[destinationBlock].checkedEntry); - b.linkStatus[e] = true; + WriteLinkBlock(e.exitPtrs, blocks[destinationBlock].checkedEntry); + e.linkStatus = true; } } } @@ -316,10 +307,10 @@ using namespace Gen; return; for (multimap::iterator iter = ppp.first; iter != ppp.second; ++iter) { JitBlock &sourceBlock = blocks[iter->second]; - for (int e = 0; e < 2; e++) + for (auto& e : sourceBlock.linkData) { - if (sourceBlock.exitAddress[e] == b.originalAddress) - sourceBlock.linkStatus[e] = false; + if (e.exitAddress == b.originalAddress) + e.linkStatus = false; } } } diff --git a/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.h b/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.h index b81c5d837a23..ffbffaadb575 100644 --- a/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.h +++ b/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.h @@ -35,9 +35,6 @@ struct JitBlock const u8 *checkedEntry; const u8 *normalEntry; - u8 *exitPtrs[2]; // to be able to rewrite the exit jum - u32 exitAddress[2]; // 0xFFFFFFFF == unknown - u32 originalAddress; u32 codeSize; u32 originalSize; @@ -45,7 +42,13 @@ struct JitBlock int flags; bool invalid; - bool linkStatus[2]; + + struct LinkData { + u8 *exitPtrs; // to be able to rewrite the exit jum + u32 exitAddress; + bool linkStatus; // is it already linked? + }; + std::vector linkData; #ifdef _WIN32 // we don't really need to save start and stop From 1942d79c5b2971e3c7bb8c6d62593985fe67c909 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Wed, 13 Nov 2013 16:36:30 -0600 Subject: [PATCH 075/202] [Android-overlay] Had the action reversed. --- Source/Core/DolphinWX/Src/Android/ButtonManager.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp b/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp index d7fa34e4e0d2..a0b488e2b7cd 100644 --- a/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp +++ b/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp @@ -125,11 +125,7 @@ namespace ButtonManager } void TouchEvent(int button, int action) { - // Actions - // 0 is press - // 1 is let go - // 2 is move - m_buttons[button]->SetState(action ? BUTTON_RELEASED : BUTTON_PRESSED); + m_buttons[button]->SetState(action ? BUTTON_PRESSED : BUTTON_RELEASED); } void GamepadEvent(std::string dev, int button, int action) From f15a0c17d03e8846867d92c0c3a6945016f397e1 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Thu, 14 Nov 2013 10:45:45 -0500 Subject: [PATCH 076/202] [Android] Get rid of some unnecessary onAttach overrides in AboutFragment, VideoSettingsFragment, and FolderBrowser. These can just be replaced with calls to getActivity(). --- .../dolphinemu/dolphinemu/AboutFragment.java | 14 +------------ .../folderbrowser/FolderBrowser.java | 20 +++++-------------- .../settings/video/VideoSettingsFragment.java | 13 +----------- 3 files changed, 7 insertions(+), 40 deletions(-) diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/AboutFragment.java b/Source/Android/src/org/dolphinemu/dolphinemu/AboutFragment.java index 3695bfa40bfb..922dab0e8d94 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/AboutFragment.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/AboutFragment.java @@ -6,7 +6,6 @@ package org.dolphinemu.dolphinemu; -import android.app.Activity; import android.app.ListFragment; import android.content.Context; import android.os.Bundle; @@ -27,8 +26,6 @@ */ public final class AboutFragment extends ListFragment { - private static Activity m_activity; - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -42,22 +39,13 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa Input.add(new AboutFragmentItem(getString(R.string.build_revision), NativeLibrary.GetVersionString())); Input.add(new AboutFragmentItem(getString(R.string.supports_gles3), VideoSettingsFragment.SupportsGLES3() ? yes : no)); - AboutFragmentAdapter adapter = new AboutFragmentAdapter(m_activity, R.layout.about_layout, Input); + AboutFragmentAdapter adapter = new AboutFragmentAdapter(getActivity(), R.layout.about_layout, Input); mMainList.setAdapter(adapter); mMainList.setEnabled(false); // Makes the list view non-clickable. return mMainList; } - @Override - public void onAttach(Activity activity) - { - super.onAttach(activity); - - // Cache the activity instance. - m_activity = activity; - } - // Represents an item in the AboutFragment. private static final class AboutFragmentItem { diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/folderbrowser/FolderBrowser.java b/Source/Android/src/org/dolphinemu/dolphinemu/folderbrowser/FolderBrowser.java index 1697311fe3bd..2258bdb41ff6 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/folderbrowser/FolderBrowser.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/folderbrowser/FolderBrowser.java @@ -6,7 +6,6 @@ package org.dolphinemu.dolphinemu.folderbrowser; -import android.app.Activity; import android.app.ListFragment; import android.os.Bundle; import android.os.Environment; @@ -37,7 +36,6 @@ */ public final class FolderBrowser extends ListFragment { - private Activity m_activity; private FolderBrowserAdapter adapter; private ListView mFolderBrowserList; private ListView rootView; @@ -46,7 +44,9 @@ public final class FolderBrowser extends ListFragment // Populates the FolderView with the given currDir's contents. private void Fill(File currDir) { - m_activity.setTitle(String.format(getString(R.string.current_dir), currDir.getName())); + // Change the activity title to reflect the current directory the FolderBrowser is in. + getActivity().setTitle(String.format(getString(R.string.current_dir), currDir.getName())); + File[] dirs = currDir.listFiles(); List dir = new ArrayList(); List fls = new ArrayList(); @@ -96,7 +96,7 @@ else if (entry.isFile() && hasExtension) if (!currDir.getPath().equalsIgnoreCase("/")) dir.add(0, new FolderBrowserItem("..", getString(R.string.parent_directory), currDir.getParent())); - adapter = new FolderBrowserAdapter(m_activity, R.layout.gamelist_folderbrowser_list, dir); + adapter = new FolderBrowserAdapter(getActivity(), R.layout.gamelist_folderbrowser_list, dir); mFolderBrowserList = (ListView) rootView.findViewById(R.id.gamelist); mFolderBrowserList.setAdapter(adapter); } @@ -128,16 +128,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa return mFolderBrowserList; } - @Override - public void onAttach(Activity activity) - { - super.onAttach(activity); - - // Cache the activity instance. - m_activity = activity; - } - - private void FolderSelected() { String Directories = NativeLibrary.GetConfig("Dolphin.ini", "General", "GCMPathes", "0"); @@ -168,6 +158,6 @@ private void FolderSelected() NativeLibrary.SetConfig("Dolphin.ini", "General", "GCMPath" + Integer.toString(intDirectories), currentDir.getPath()); } - ((GameListActivity)m_activity).SwitchPage(0); + ((GameListActivity)getActivity()).SwitchPage(0); } } diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/video/VideoSettingsFragment.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/video/VideoSettingsFragment.java index 97ed412d8d12..69b647a45cac 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/video/VideoSettingsFragment.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/video/VideoSettingsFragment.java @@ -6,7 +6,6 @@ package org.dolphinemu.dolphinemu.settings.video; -import android.app.Activity; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Bundle; @@ -28,7 +27,6 @@ public final class VideoSettingsFragment extends PreferenceFragment public static String m_GLVendor; public static String m_GLRenderer; public static String m_GLExtensions; - private Activity m_activity; /** * Class which provides a means to retrieve various @@ -225,7 +223,7 @@ public void onCreate(Bundle savedInstanceState) // denotes the placement on the UI. So if more elements are // added to the video settings, these may need to change. // - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(m_activity); + final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); final PreferenceScreen mainScreen = getPreferenceScreen(); if (videoBackends.getValue().equals("Software Renderer")) @@ -266,13 +264,4 @@ else if (preference.getString(key, "Software Renderer").equals("OGL")) } }); } - - @Override - public void onAttach(Activity activity) - { - super.onAttach(activity); - - // Cache the activity instance. - m_activity = activity; - } } From 9d3d568ae452dcba91f821aa43a249ca12afeb49 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Thu, 14 Nov 2013 11:01:44 -0500 Subject: [PATCH 077/202] [Android] Bump the targetSdkVersion in the AndroidManifest XML file to 19 (KitKat). Nothing we do would require compatibility behaviors to be enabled to maintain forward compatibility. --- Source/Android/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Android/AndroidManifest.xml b/Source/Android/AndroidManifest.xml index 5ecd07f5de0c..b913f28d1d38 100644 --- a/Source/Android/AndroidManifest.xml +++ b/Source/Android/AndroidManifest.xml @@ -7,7 +7,7 @@ + android:targetSdkVersion="19" /> From feedee5c23a0493ae4f25564844ede88079ac777 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Thu, 14 Nov 2013 15:17:51 -0600 Subject: [PATCH 078/202] [Android-overlay] Support touch screen axises in native. Have a non-configurable main joystick on screen at this point. --- .../dolphinemu/dolphinemu/NativeLibrary.java | 8 + .../emulation/overlay/InputOverlay.java | 154 +++++++++++++----- ...e.java => InputOverlayDrawableButton.java} | 8 +- .../overlay/InputOverlayDrawableJoystick.java | 140 ++++++++++++++++ .../DolphinWX/Src/Android/ButtonManager.cpp | 26 ++- .../DolphinWX/Src/Android/ButtonManager.h | 15 +- Source/Core/DolphinWX/Src/MainAndroid.cpp | 13 +- 7 files changed, 300 insertions(+), 64 deletions(-) rename Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/{InputOverlayDrawable.java => InputOverlayDrawableButton.java} (75%) create mode 100644 Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java index 53db64b194e5..58b33d393d8b 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java @@ -59,6 +59,14 @@ public class ButtonState */ public static native void onTouchEvent(int Button, int Action); + /** + * Handles touch events. + * + * @param Button Key code identifying which button was pressed. + * @param Action Mask for the action being performed. + */ + public static native void onTouchAxisEvent(int Axis, float loc); + /** * Handles button press events for a gamepad. * diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java index 651df513e9b6..c3b4e5c734b6 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java @@ -6,36 +6,41 @@ package org.dolphinemu.dolphinemu.emulation.overlay; -import java.util.HashSet; -import java.util.Set; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.NativeLibrary.ButtonState; -import org.dolphinemu.dolphinemu.NativeLibrary.ButtonType; -import org.dolphinemu.dolphinemu.R; - import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; +import android.graphics.*; import android.graphics.drawable.Drawable; import android.preference.PreferenceManager; import android.util.AttributeSet; +import android.util.Log; import android.view.MotionEvent; +import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.View.OnTouchListener; +import org.dolphinemu.dolphinemu.NativeLibrary; +import org.dolphinemu.dolphinemu.NativeLibrary.ButtonState; +import org.dolphinemu.dolphinemu.NativeLibrary.ButtonType; +import org.dolphinemu.dolphinemu.R; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; /** * Draws the interactive input overlay on top of the * {@link NativeGLSurfaceView} that is rendering emulation. */ -public final class InputOverlay extends SurfaceView implements OnTouchListener +public final class InputOverlay extends SurfaceView implements OnTouchListener, Runnable { - private final Set overlayItems = new HashSet(); + private final Set overlayButtons = new HashSet(); + private final Set overlayJoysticks = new HashSet(); + Semaphore _sema; + SurfaceHolder surfaceHolder; + Thread thread = null; /** * Constructor * @@ -47,9 +52,13 @@ public InputOverlay(Context context, AttributeSet attrs) super(context, attrs); // Add all the overlay items to the HashSet. - overlayItems.add(initializeOverlayDrawable(context, R.drawable.button_a, ButtonType.BUTTON_A)); - overlayItems.add(initializeOverlayDrawable(context, R.drawable.button_b, ButtonType.BUTTON_B)); - overlayItems.add(initializeOverlayDrawable(context, R.drawable.button_start, ButtonType.BUTTON_START)); + overlayButtons.add(initializeOverlayButton(context, R.drawable.button_a, ButtonType.BUTTON_A)); + overlayButtons.add(initializeOverlayButton(context, R.drawable.button_b, ButtonType.BUTTON_B)); + overlayButtons.add(initializeOverlayButton(context, R.drawable.button_start, ButtonType.BUTTON_START)); + overlayJoysticks.add(initializeOverlayJoystick(context, + R.drawable.joy_outer, R.drawable.joy_inner, + ButtonType.STICK_MAIN_UP, ButtonType.STICK_MAIN_DOWN, + ButtonType.STICK_MAIN_LEFT, ButtonType.STICK_MAIN_RIGHT)); // Set the on touch listener. setOnTouchListener(this); @@ -59,8 +68,42 @@ public InputOverlay(Context context, AttributeSet attrs) // Request focus for the overlay so it has priority on presses. requestFocus(); - } + _sema = new Semaphore(0); + surfaceHolder = getHolder(); + surfaceHolder.setFormat(PixelFormat.TRANSPARENT); + thread = new Thread(this); + thread.start(); + } + private boolean Draw() + { + if(surfaceHolder.getSurface().isValid()){ + // Draw everything + Canvas canvas = surfaceHolder.lockCanvas(); + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + for (InputOverlayDrawableButton item : overlayButtons) + item.draw(canvas); + for (InputOverlayDrawableJoystick item : overlayJoysticks) + item.Draw(canvas); + surfaceHolder.unlockCanvasAndPost(canvas); + return true; + } + return false; + } + @Override + public void run() { + boolean didFirstPost = false; + while(!didFirstPost) + if (Draw()) + didFirstPost = true; + while(true){ + try + { + _sema.acquire(); + Draw(); + } catch (InterruptedException ex) {} + } + } @Override public boolean onTouch(View v, MotionEvent event) { @@ -69,7 +112,7 @@ public boolean onTouch(View v, MotionEvent event) // The reason it won't work is that when moving an axis, you would get the event MotionEvent.ACTION_MOVE. int buttonState = (event.getAction() == MotionEvent.ACTION_DOWN) ? ButtonState.PRESSED : ButtonState.RELEASED; - for (InputOverlayDrawable item : overlayItems) + for (InputOverlayDrawableButton item : overlayButtons) { // Check if there was a touch within the bounds of a drawable. if (item.getBounds().contains((int)event.getX(), (int)event.getY())) @@ -93,24 +136,22 @@ public boolean onTouch(View v, MotionEvent event) } } } - - return true; - } - - @Override - public void onDraw(Canvas canvas) - { - super.onDraw(canvas); - - // Draw all overlay items. - for (InputOverlayDrawable item : overlayItems) + for (InputOverlayDrawableJoystick item : overlayJoysticks) { - item.draw(canvas); + item.TrackEvent(event); + int[] axisIDs = item.getAxisIDs(); + float[] axises = item.getAxisValues(); + + for (int a = 0; a < 4; ++a) + NativeLibrary.onTouchAxisEvent(axisIDs[a], axises[a]); } + _sema.release(); + + return true; } /** - * Initializes an InputOverlayDrawable, given by resId, with all of the + * Initializes an InputOverlayDrawableButton, given by resId, with all of the * parameters set for it to be properly shown on the InputOverlay. *

* This works due to the way the X and Y coordinates are stored within @@ -130,47 +171,76 @@ public void onDraw(Canvas canvas) * *

* Technically no modifications should need to be performed on the returned - * InputOverlayDrawable. Simply add it to the HashSet of overlay items and wait + * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait * for Android to call the onDraw method. * * @param context The current {@link Context}. * @param resId The resource ID of the {@link Drawable} to get the {@link Bitmap} of. - * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawable represents. + * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents. * - * @return An {@link InputOverlayDrawable} with the correct drawing bounds set. + * @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set. * */ - private static InputOverlayDrawable initializeOverlayDrawable(Context context, int resId, int buttonId) + private static InputOverlayDrawableButton initializeOverlayButton(Context context, int resId, int buttonId) { // Resources handle for fetching the initial Drawable resource. final Resources res = context.getResources(); - // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawable. + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton. final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); - // Initialize the InputOverlayDrawable. + // Initialize the InputOverlayDrawableButton. final Bitmap bitmap = BitmapFactory.decodeResource(res, resId); - final InputOverlayDrawable overlayDrawable = new InputOverlayDrawable(res, bitmap, buttonId); + final InputOverlayDrawableButton overlayDrawable = new InputOverlayDrawableButton(res, bitmap, buttonId); // String ID of the Drawable. This is what is passed into SharedPreferences // to check whether or not a value has been set. final String drawableId = res.getResourceEntryName(resId); - // The X and Y coordinates of the InputOverlayDrawable on the InputOverlay. + // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. // These were set in the input overlay configuration menu. int drawableX = (int) sPrefs.getFloat(drawableId+"-X", 0f); int drawableY = (int) sPrefs.getFloat(drawableId+"-Y", 0f); - // Intrinsic width and height of the InputOverlayDrawable. + // Intrinsic width and height of the InputOverlayDrawableButton. // For any who may not know, intrinsic width/height // are the original unmodified width and height of the image. int intrinWidth = overlayDrawable.getIntrinsicWidth(); int intrinHeight = overlayDrawable.getIntrinsicHeight(); - // Now set the bounds for the InputOverlayDrawable. - // This will dictate where on the screen (and the what the size) the InputOverlayDrawable will be. + // Now set the bounds for the InputOverlayDrawableButton. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be. overlayDrawable.setBounds(drawableX, drawableY, drawableX+intrinWidth, drawableY+intrinHeight); + return overlayDrawable; + } + private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context, int resOuter, int resInner, int axisUp, int axisDown, int axisLeft, int axisRight) + { + // Resources handle for fetching the initial Drawable resource. + final Resources res = context.getResources(); + + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton. + final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); + + // Initialize the InputOverlayDrawableButton. + final Bitmap bitmapOuter = BitmapFactory.decodeResource(res, resOuter); + final Bitmap bitmapInner = BitmapFactory.decodeResource(res, resInner); + + // Now set the bounds for the InputOverlayDrawableButton. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be. + int outerSize = bitmapOuter.getWidth() + 256; + int X = 0; + int Y = 0; + Rect outerRect = new Rect(X, Y, X + outerSize, Y + outerSize); + Rect innerRect = new Rect(0, 0, outerSize / 4, outerSize / 4); + + final InputOverlayDrawableJoystick overlayDrawable + = new InputOverlayDrawableJoystick(res, + bitmapOuter, bitmapInner, + outerRect, innerRect, + axisUp, axisDown, axisLeft, axisRight); + + return overlayDrawable; } diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawable.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableButton.java similarity index 75% rename from Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawable.java rename to Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableButton.java index a4f445b5e3a9..12343b586f54 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawable.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableButton.java @@ -14,7 +14,7 @@ * Custom {@link BitmapDrawable} that is capable * of storing it's own ID. */ -public class InputOverlayDrawable extends BitmapDrawable +public class InputOverlayDrawableButton extends BitmapDrawable { // The ID identifying what type of button this Drawable represents. private int buttonType; @@ -26,7 +26,7 @@ public class InputOverlayDrawable extends BitmapDrawable * @param bitmap {@link Bitmap} to use with this Drawable. * @param buttonType Identifier for this type of button. */ - public InputOverlayDrawable(Resources res, Bitmap bitmap, int buttonType) + public InputOverlayDrawableButton(Resources res, Bitmap bitmap, int buttonType) { super(res, bitmap); @@ -34,9 +34,9 @@ public InputOverlayDrawable(Resources res, Bitmap bitmap, int buttonType) } /** - * Gets this InputOverlayDrawable's button ID. + * Gets this InputOverlayDrawableButton's button ID. * - * @return this InputOverlayDrawable's button ID. + * @return this InputOverlayDrawableButton's button ID. */ public int getId() { diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java new file mode 100644 index 000000000000..0ceb5b918632 --- /dev/null +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java @@ -0,0 +1,140 @@ +/** + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2 + * Refer to the license.txt file included. + */ + +package org.dolphinemu.dolphinemu.emulation.overlay; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.view.MotionEvent; + +/** + * Custom {@link android.graphics.drawable.BitmapDrawable} that is capable + * of storing it's own ID. + */ +public class InputOverlayDrawableJoystick extends BitmapDrawable +{ + // The ID identifying what type of button this Drawable represents. + private int axisIDs[] = {0, 0, 0, 0}; + private float axises[] = {0f, 0f}; + private int trackid = -1; + private BitmapDrawable ringInner; + + /** + * Constructor + * + * @param res {@link android.content.res.Resources} instance. + * @param bitmapOuter {@link android.graphics.Bitmap} to use with this Drawable. + * @param axisUp Identifier for this type of axis. + * @param axisDown Identifier for this type of axis. + * @param axisLeft Identifier for this type of axis. + * @param axisRight Identifier for this type of axis. + */ + public InputOverlayDrawableJoystick(Resources res, + Bitmap bitmapOuter, Bitmap bitmapInner, + Rect rectOuter, Rect rectInner, + int axisUp, int axisDown, int axisLeft, int axisRight) + { + super(res, bitmapOuter); + this.setBounds(rectOuter); + + this.ringInner = new BitmapDrawable(res, bitmapInner); + this.ringInner.setBounds(rectInner); + SetInnerBounds(); + this.axisIDs[0] = axisUp; + this.axisIDs[1] = axisDown; + this.axisIDs[2] = axisLeft; + this.axisIDs[3] = axisRight; + } + public void Draw(Canvas canvas) + { + this.draw(canvas); + ringInner.draw(canvas); + } + public void TrackEvent(MotionEvent event) + { + int pointerIndex = event.getActionIndex(); + if (trackid == -1) + { + if (event.getAction() == MotionEvent.ACTION_DOWN) + if(getBounds().contains((int)event.getX(), (int)event.getY())) + trackid = event.getPointerId(pointerIndex); + } + else + { + if (event.getAction() == MotionEvent.ACTION_UP) + { + if (trackid == event.getPointerId(pointerIndex)) + { + axises[0] = axises[1] = 0.0f; + SetInnerBounds(); + trackid = -1; + } + } + } + if (trackid == -1) + return; + float touchX = event.getX(); + float touchY = event.getY(); + float maxY = this.getBounds().bottom; + float maxX = this.getBounds().right; + touchX -= this.getBounds().centerX(); + maxX -= this.getBounds().centerX(); + touchY -= this.getBounds().centerY(); + maxY -= this.getBounds().centerY(); + final float AxisX = touchX / maxX; + final float AxisY = touchY/maxY; + + this.axises[0] = AxisY; + this.axises[1] = AxisX; + + SetInnerBounds(); + } + public float[] getAxisValues() + { + float[] joyaxises = new float[4]; + if (axises[0] >= 0.0f) + { + joyaxises[0] = 0.0f; + joyaxises[1] = Math.min(axises[0], 1.0f); + } + else + { + joyaxises[0] = Math.min(Math.abs(axises[0]), 1.0f); + joyaxises[1] = 0.0f; + } + if (axises[1] >= 0.0) + { + joyaxises[2] = 0.0f; + joyaxises[3] = Math.min(axises[1], 1.0f); + } + else + { + joyaxises[2] = Math.min(Math.abs(axises[1]), 1.0f); + joyaxises[3] = 0.0f; + } + return joyaxises; + } + public int[] getAxisIDs() + { + return axisIDs; + } + private void SetInnerBounds() + { + float floatX = this.getBounds().centerX(); + float floatY = this.getBounds().centerY(); + floatY += axises[0] * (this.getBounds().height() / 2); + floatX += axises[1] * (this.getBounds().width() / 2); + int X = (int)(floatX); + int Y = (int)(floatY); + int width = this.ringInner.getBounds().width() / 2; + int height = this.ringInner.getBounds().height() / 2; + this.ringInner.setBounds(X - width, Y - height, + X + width, Y + height); + } +} diff --git a/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp b/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp index a0b488e2b7cd..314cdb8c8375 100644 --- a/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp +++ b/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp @@ -25,6 +25,7 @@ extern void DrawButton(GLuint tex, float *coords); namespace ButtonManager { std::unordered_map m_buttons; + std::unordered_map m_axises; std::unordered_map m_controllers; const char *configStrings[] = { "InputA", "InputB", @@ -74,6 +75,16 @@ namespace ButtonManager m_buttons[BUTTON_LEFT] = new Button(); m_buttons[BUTTON_RIGHT] = new Button(); + m_axises[STICK_MAIN_UP] = new Axis(); + m_axises[STICK_MAIN_DOWN] = new Axis(); + m_axises[STICK_MAIN_LEFT] = new Axis(); + m_axises[STICK_MAIN_RIGHT] = new Axis(); + m_axises[STICK_C_UP] = new Axis(); + m_axises[STICK_C_DOWN] = new Axis(); + m_axises[STICK_C_LEFT] = new Axis(); + m_axises[STICK_C_RIGHT] = new Axis(); + m_axises[TRIGGER_L] = new Axis(); + m_axises[TRIGGER_R] = new Axis(); // Init our controller bindings IniFile ini; @@ -118,16 +129,22 @@ namespace ButtonManager } float GetAxisValue(ButtonType axis) { + float value = 0.0f; + value = m_axises[axis]->AxisValue(); + auto it = m_controllers.begin(); if (it == m_controllers.end()) - return 0.0f; + return value; return it->second->AxisValue(axis); } void TouchEvent(int button, int action) { m_buttons[button]->SetState(action ? BUTTON_PRESSED : BUTTON_RELEASED); } - + void TouchAxisEvent(int axis, float value) + { + m_axises[axis]->SetValue(value); + } void GamepadEvent(std::string dev, int button, int action) { auto it = m_controllers.find(dev); @@ -160,11 +177,6 @@ namespace ButtonManager m_buttons.clear(); } - void DrawButtons() - { - // XXX: Make platform specific drawing - } - // InputDevice void InputDevice::PressEvent(int button, int action) { diff --git a/Source/Core/DolphinWX/Src/Android/ButtonManager.h b/Source/Core/DolphinWX/Src/Android/ButtonManager.h index 2273b786f9a8..5a99bb092f13 100644 --- a/Source/Core/DolphinWX/Src/Android/ButtonManager.h +++ b/Source/Core/DolphinWX/Src/Android/ButtonManager.h @@ -67,7 +67,18 @@ namespace ButtonManager void SetState(ButtonState state) { m_state = state; } bool Pressed() { return m_state == BUTTON_PRESSED; } - ~Button() { } + ~Button() {} + }; + class Axis + { + private: + float m_value; + public: + Axis() : m_value(0.0f) {} + void SetValue(float value) { m_value = value; } + float AxisValue() { return m_value; } + + ~Axis() {} }; struct sBind @@ -107,10 +118,10 @@ namespace ButtonManager }; void Init(); - void DrawButtons(); bool GetButtonPressed(ButtonType button); float GetAxisValue(ButtonType axis); void TouchEvent(int button, int action); + void TouchAxisEvent(int axis, float value); void GamepadEvent(std::string dev, int button, int action); void GamepadAxisEvent(std::string dev, int axis, float value); void Shutdown(); diff --git a/Source/Core/DolphinWX/Src/MainAndroid.cpp b/Source/Core/DolphinWX/Src/MainAndroid.cpp index f94a90a4df96..0d75d91e2d99 100644 --- a/Source/Core/DolphinWX/Src/MainAndroid.cpp +++ b/Source/Core/DolphinWX/Src/MainAndroid.cpp @@ -241,6 +241,10 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onTouchEvent { ButtonManager::TouchEvent(Button, Action); } +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onTouchAxisEvent(JNIEnv *env, jobject obj, jint Button, jfloat Action) +{ + ButtonManager::TouchAxisEvent(Button, Action); +} JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onGamePadEvent(JNIEnv *env, jobject obj, jstring jDevice, jint Button, jint Action) { const char *Device = env->GetStringUTFChars(jDevice, NULL); @@ -375,15 +379,6 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run(JNIEnv * VideoBackend::ActivateBackend(SConfig::GetInstance().m_LocalCoreStartupParameter.m_strVideoBackend); WiimoteReal::LoadSettings(); - // Load our Android specific settings - IniFile ini; - bool onscreencontrols = true; - ini.Load(File::GetUserPath(D_CONFIG_IDX) + std::string("Dolphin.ini")); - ini.Get("Android", "ScreenControls", &onscreencontrols, true); - - if (onscreencontrols) - OSD::AddCallback(OSD::OSD_ONFRAME, ButtonManager::DrawButtons); - // No use running the loop when booting fails if ( BootManager::BootCore( g_filename.c_str() ) ) while (PowerPC::GetState() != PowerPC::CPU_POWERDOWN) From 1e90a838f2bb77ead35eac5c0f270d3727732e38 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Thu, 14 Nov 2013 15:20:44 -0600 Subject: [PATCH 079/202] Make sure to add our resources for the joystick. --- Source/Android/res/drawable/joy_inner.png | Bin 0 -> 1125 bytes Source/Android/res/drawable/joy_outer.png | Bin 0 -> 1690 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Source/Android/res/drawable/joy_inner.png create mode 100644 Source/Android/res/drawable/joy_outer.png diff --git a/Source/Android/res/drawable/joy_inner.png b/Source/Android/res/drawable/joy_inner.png new file mode 100644 index 0000000000000000000000000000000000000000..04e3bb975b4e7edbbeeda352842940bad586fc5a GIT binary patch literal 1125 zcmV-r1e*JaP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000BjNkl!0oSVK5k^lJ)!jU-tg10qY+e9K4zb(2D?w z(l-4W96oPsY@}#@4Pfls@k=0D0gz_fG-Es$D1Ac!w$~$>EPLvfV3-e z?_7?zx3?3vv$KSxnj$InnLy?NtuP<8ft2M@O|C9v;@u0m53oDS&v! zhh^fEsHg#`)Jo13F^-OpkJHJ?Njg0}Z5YpZ2RMLw5ClR%$^ul_-*4(h= zoQUQ4?CdO^pP#46WK#WNJOdPK04flo!&5;P%2V4k0Tu4-rhEh=gI;A04*BO5;Q~r2(f1SG` z=o=BIu5ZPSN&u-&KOI1^83~BGV(f(IrxZODX07_(SXF;hA zyWwQHF%_E_Kj!23DIQ4vDDFB95uHv$?_Auw=W|gI;TtATd*)Yz6 zng^wLH;N@LHl0JMUOGM0mk$BaNFJx-(lqRy8{d`c=K{c@d?TvHb3dr+)QlIC(tSXR zk7@6uz`vwC^L?P@g_7k1#O5e ro$vAye#dDhW4N%tS6;e|#`pgKfv(jmP<}eb00000NkvXXu0mjfZ|U!! literal 0 HcmV?d00001 diff --git a/Source/Android/res/drawable/joy_outer.png b/Source/Android/res/drawable/joy_outer.png new file mode 100644 index 0000000000000000000000000000000000000000..7238bdbd29c048e16cf9bd1df83e49d8a8c2e98a GIT binary patch literal 1690 zcmV;L24(q)P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000IFNklXoD&d>dsnW0XnGZeFpjg37zGBPr6Ys16C zhhy48UA2M-po0K>3Scjej*i|$>o~Um(C;e7E@A9koumXOKs)7_?KbeW(L46u3yEqCA6ci#xoT#?gZM3g%lFIFgrVYm#e&@emxT~ zK0ZDVgSHIdyZF9}NO&!Tfs`>ZF);@s7y*nPjjC-D(W?$`0$Q!s{V?Ds!=N1)d3Jhw z`kr2m${SCwL&ryG1g}JFFztMGdJzDHFPPrI@8vpyXTd2EnTS~QA)yihR1BqG80@bj zJP-OP4lzce@E60LOI01=OaKK#(G@C##~FTSLx2b&1Gc=4#@a)>wqW0|t6V1nAn*Y4 z?uIB}u0QXpAXY!t0obQgZyVnax#%t>fSxzNbDnk*?4mq$A9^14{iBX@iG)G|B#fPj$p^gG0RVnM+1$&X&jOs8nK=M^Z;9>eMT@4~2*BXiwcuf~ z^GsDw^t->LL$Yu}3V)VokCgy~O-rjP2A}CS7+?J?)-q#txMH=*u@ZoRze@OA^HsiA z#sSC2f~9Rsg*yn)&}RhnbCDB@Kv^|%TC}D@7XecBwR2-bKT94{L84%n1=9S#ivV;y zDNI?zC6rhCAqUv5+oGV00CZ6ApX`T`b?VHEf|LMGbvoyVI&7#DkdJJQ0^m${jwu1S z_=6B|!AE!##%PLjxvcgk1W55qc2lVQN16b}$3|HpCI=~ch>{S13r}nD^AX&nF~FH@ z>3On5yAuLGf*vxqngqtzR^UxG zNjy>;G7E#Rz^07@c)fpdG%bL*c(Z9heC-9+6`2W6u>kUp4yT57Ux7^<$MXSQ7L$Mz z@?)iGKz!}(B7k>q_cZ}vV}BC>?=MIVSoIC5rj5(i0N&@@S49wbKNrmBhfo6!%a3-` zJn3sM@U9Bx1&fY$o&#iw;+ls9@C-yZL&-@{FZbDfy%Bqyx%KBNShvHvMI5-J zuq?rN{N@Rn_P5R5;2Ft@%dtHYTOV+q69Rrt2{2h1^g00W*TvcIrUXb{BwIw(f#+g- zC6+#He;*d`ovEp*k(2;_!5?4BpKGxUimeAT-Y0@_Lv^R8!tng#Ivt|pXl&!^)(7w3 z3-HOo4{*M72LTcV`gFxtbsH30K7fB9-rpe^c?7&Nh*7}Bo_e1Lz$W>&o1+>91G6~- z_(chQ9*1|Ox9#@$OBw|jbS!qLhSo>G3Lc-Qv-0yEkkCh9w{fTF!O%v@Oo7&h6Ej06UKx5!;6CkCYKtN&p4ORPwixQ{R;_Tc$YV|8lC8Y;)--{BGm(F&7<9 z1PBR?gv&-cY(mjfE=ua`LodUoWU7-!yGlJ@I1T0*EKAC-4P{ii+>dCG%}uhxGd$0i ztSW8foFl*_MRU!F1{(2R3?c;haR5!82sdn4DysHgvp`YkGRL6sT80D?v54z)p~D_0 zxDP=#Z{p}v;F|9@V8gS%`n)1Qm2ozJ@;5Unp}F`wqk?sUGf7ngWf?ms(YBVOW6`#} zt)i8p+h%8N1Z0)54sd=J kK-LtXhgn=>%qD8z{|`W#HIGzAdH?_b07*qoM6N<$f>+%3bpQYW literal 0 HcmV?d00001 From 07765aa6f0f84992fbb61e16025bd343821cc378 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Fri, 15 Nov 2013 12:26:17 -0500 Subject: [PATCH 080/202] [Android] Documentation and some cleanup. --- .../dolphinemu/dolphinemu/NativeLibrary.java | 11 +- .../emulation/overlay/InputOverlay.java | 100 +++++++++--------- .../overlay/InputOverlayDrawableButton.java | 2 +- .../overlay/InputOverlayDrawableJoystick.java | 26 +++-- 4 files changed, 71 insertions(+), 68 deletions(-) diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java index 58b33d393d8b..07bea528ba38 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java @@ -45,7 +45,7 @@ public static final class ButtonType /** * Button states */ - public class ButtonState + public static final class ButtonState { public static final int RELEASED = 0; public static final int PRESSED = 1; @@ -60,12 +60,13 @@ public class ButtonState public static native void onTouchEvent(int Button, int Action); /** - * Handles touch events. + * Handles axis-related touch events. * - * @param Button Key code identifying which button was pressed. - * @param Action Mask for the action being performed. + * @param Axis Axis ID for the type of axis being altered. (Example: Main stick up, down, left, right, etc). + * @param force How 'far down' the joystick is pushed down. 0.0f indicates center (or no force), + * 1.0f indicates max force (or joystick pushed all the way down in any arbitrary direction). */ - public static native void onTouchAxisEvent(int Axis, float loc); + public static native void onTouchAxisEvent(int Axis, float force); /** * Handles button press events for a gamepad. diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java index c3b4e5c734b6..359f25f17ed9 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java @@ -13,9 +13,7 @@ import android.graphics.drawable.Drawable; import android.preference.PreferenceManager; import android.util.AttributeSet; -import android.util.Log; import android.view.MotionEvent; -import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.View.OnTouchListener; @@ -26,21 +24,16 @@ import java.util.HashSet; import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Semaphore; /** * Draws the interactive input overlay on top of the * {@link NativeGLSurfaceView} that is rendering emulation. */ -public final class InputOverlay extends SurfaceView implements OnTouchListener, Runnable +public final class InputOverlay extends SurfaceView implements OnTouchListener { private final Set overlayButtons = new HashSet(); private final Set overlayJoysticks = new HashSet(); - Semaphore _sema; - SurfaceHolder surfaceHolder; - Thread thread = null; /** * Constructor * @@ -68,56 +61,40 @@ public InputOverlay(Context context, AttributeSet attrs) // Request focus for the overlay so it has priority on presses. requestFocus(); - _sema = new Semaphore(0); - - surfaceHolder = getHolder(); - surfaceHolder.setFormat(PixelFormat.TRANSPARENT); - thread = new Thread(this); - thread.start(); } - private boolean Draw() + + @Override + public void draw(Canvas canvas) { - if(surfaceHolder.getSurface().isValid()){ - // Draw everything - Canvas canvas = surfaceHolder.lockCanvas(); - canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); - for (InputOverlayDrawableButton item : overlayButtons) - item.draw(canvas); - for (InputOverlayDrawableJoystick item : overlayJoysticks) - item.Draw(canvas); - surfaceHolder.unlockCanvasAndPost(canvas); - return true; + super.onDraw(canvas); + + for (InputOverlayDrawableButton button : overlayButtons) + { + button.draw(canvas); } - return false; - } - @Override - public void run() { - boolean didFirstPost = false; - while(!didFirstPost) - if (Draw()) - didFirstPost = true; - while(true){ - try - { - _sema.acquire(); - Draw(); - } catch (InterruptedException ex) {} + + for (InputOverlayDrawableJoystick joystick: overlayJoysticks) + { + joystick.draw(canvas); } } + @Override public boolean onTouch(View v, MotionEvent event) { // Determine the button state to apply based on the MotionEvent action flag. // TODO: This will not work when Axis support is added. Make sure to refactor this when that time comes // The reason it won't work is that when moving an axis, you would get the event MotionEvent.ACTION_MOVE. + // + // TODO: Refactor this so we detect either Axis movements or button presses so we don't run two loops all the time. + // int buttonState = (event.getAction() == MotionEvent.ACTION_DOWN) ? ButtonState.PRESSED : ButtonState.RELEASED; - - for (InputOverlayDrawableButton item : overlayButtons) + for (InputOverlayDrawableButton button : overlayButtons) { // Check if there was a touch within the bounds of a drawable. - if (item.getBounds().contains((int)event.getX(), (int)event.getY())) + if (button.getBounds().contains((int)event.getX(), (int)event.getY())) { - switch (item.getId()) + switch (button.getId()) { case ButtonType.BUTTON_A: NativeLibrary.onTouchEvent(ButtonType.BUTTON_A, buttonState); @@ -136,16 +113,18 @@ public boolean onTouch(View v, MotionEvent event) } } } - for (InputOverlayDrawableJoystick item : overlayJoysticks) + + for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { - item.TrackEvent(event); - int[] axisIDs = item.getAxisIDs(); - float[] axises = item.getAxisValues(); + joystick.TrackEvent(event); + int[] axisIDs = joystick.getAxisIDs(); + float[] axises = joystick.getAxisValues(); for (int a = 0; a < 4; ++a) + { NativeLibrary.onTouchAxisEvent(axisIDs[a], axises[a]); + } } - _sema.release(); return true; } @@ -214,20 +193,37 @@ private static InputOverlayDrawableButton initializeOverlayButton(Context contex return overlayDrawable; } + + /** + * Initializes an {@link InputOverlayDrawableJoystick} + * + * @param context The current {@link Context} + * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds). + * @param resInner Resource ID for the inner image of the joystick (the one you actually move around). + * @param axisUp Identifier for this type of axis. + * @param axisDown Identifier for this type of axis. + * @param axisLeft Identifier for this type of axis. + * @param axisRight Identifier for this type of axis. + * + * @return the initialized {@link InputOverlayDrawableJoystick}. + */ private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context, int resOuter, int resInner, int axisUp, int axisDown, int axisLeft, int axisRight) { // Resources handle for fetching the initial Drawable resource. final Resources res = context.getResources(); - // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton. + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick. final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); - // Initialize the InputOverlayDrawableButton. + // Initialize the InputOverlayDrawableJoystick. final Bitmap bitmapOuter = BitmapFactory.decodeResource(res, resOuter); final Bitmap bitmapInner = BitmapFactory.decodeResource(res, resInner); - // Now set the bounds for the InputOverlayDrawableButton. - // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be. + // TODO: Load coordinates for the drawable from the SharedPreference keys + // made from the overlay configuration window in the settings. + + // Now set the bounds for the InputOverlayDrawableJoystick. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be. int outerSize = bitmapOuter.getWidth() + 256; int X = 0; int Y = 0; diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableButton.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableButton.java index 12343b586f54..4c63f9541bea 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableButton.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableButton.java @@ -14,7 +14,7 @@ * Custom {@link BitmapDrawable} that is capable * of storing it's own ID. */ -public class InputOverlayDrawableButton extends BitmapDrawable +public final class InputOverlayDrawableButton extends BitmapDrawable { // The ID identifying what type of button this Drawable represents. private int buttonType; diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java index 0ceb5b918632..a115305fa298 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java @@ -14,22 +14,21 @@ import android.view.MotionEvent; /** - * Custom {@link android.graphics.drawable.BitmapDrawable} that is capable + * Custom {@link BitmapDrawable} that is capable * of storing it's own ID. */ -public class InputOverlayDrawableJoystick extends BitmapDrawable +public final class InputOverlayDrawableJoystick extends BitmapDrawable { - // The ID identifying what type of button this Drawable represents. - private int axisIDs[] = {0, 0, 0, 0}; - private float axises[] = {0f, 0f}; + private final int[] axisIDs = {0, 0, 0, 0}; + private final float[] axises = {0f, 0f}; + private final BitmapDrawable ringInner; private int trackid = -1; - private BitmapDrawable ringInner; /** * Constructor * - * @param res {@link android.content.res.Resources} instance. - * @param bitmapOuter {@link android.graphics.Bitmap} to use with this Drawable. + * @param res {@link Resources} instance. + * @param bitmapOuter {@link Bitmap} to use with this Drawable. * @param axisUp Identifier for this type of axis. * @param axisDown Identifier for this type of axis. * @param axisLeft Identifier for this type of axis. @@ -51,11 +50,15 @@ public InputOverlayDrawableJoystick(Resources res, this.axisIDs[2] = axisLeft; this.axisIDs[3] = axisRight; } - public void Draw(Canvas canvas) + + @Override + public void draw(Canvas canvas) { - this.draw(canvas); + super.draw(canvas); + ringInner.draw(canvas); } + public void TrackEvent(MotionEvent event) { int pointerIndex = event.getActionIndex(); @@ -95,6 +98,7 @@ public void TrackEvent(MotionEvent event) SetInnerBounds(); } + public float[] getAxisValues() { float[] joyaxises = new float[4]; @@ -120,10 +124,12 @@ public float[] getAxisValues() } return joyaxises; } + public int[] getAxisIDs() { return axisIDs; } + private void SetInnerBounds() { float floatX = this.getBounds().centerX(); From ae11fba0692dcdbf0fe6e4edb799f1bd100ddcf8 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Mon, 18 Nov 2013 14:48:08 -0600 Subject: [PATCH 081/202] [Android] Make joysticks less dumb from a configuration standpoint. --- Source/Android/assets/GCPadNew.ini | 16 ++++---- .../dolphinemu/dolphinemu/NativeLibrary.java | 22 +++++----- .../emulation/overlay/InputOverlay.java | 26 ++++++------ .../overlay/InputOverlayDrawableJoystick.java | 16 ++++---- .../dolphinemu/gamelist/GameListActivity.java | 12 +++--- .../DolphinWX/Src/Android/ButtonManager.h | 40 ++++++++++--------- 6 files changed, 68 insertions(+), 64 deletions(-) diff --git a/Source/Android/assets/GCPadNew.ini b/Source/Android/assets/GCPadNew.ini index 42144db1cebb..7e770f60d33c 100644 --- a/Source/Android/assets/GCPadNew.ini +++ b/Source/Android/assets/GCPadNew.ini @@ -6,16 +6,16 @@ Buttons/X = `Button 3` Buttons/Y = `Button 4` Buttons/Z = `Button 5` Buttons/Start = `Button 2` -Main Stick/Up = `Axis 10` -Main Stick/Down = `Axis 11` -Main Stick/Left = `Axis 12` -Main Stick/Right = `Axis 13` +Main Stick/Up = `Axis 11` +Main Stick/Down = `Axis 12` +Main Stick/Left = `Axis 13` +Main Stick/Right = `Axis 14` Main Stick/Modifier = Shift_L Main Stick/Modifier/Range = 50.000000 -C-Stick/Up = `Axis 14` -C-Stick/Down = `Axis 15` -C-Stick/Left = `Axis 16` -C-Stick/Right = `Axis 17` +C-Stick/Up = `Axis 15` +C-Stick/Down = `Axis 16` +C-Stick/Left = `Axis 17` +C-Stick/Right = `Axis 18` C-Stick/Modifier = Control_L C-Stick/Modifier/Range = 50.000000 Triggers/L = `Axis 18` diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java index 07bea528ba38..7745deadbac6 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java @@ -30,16 +30,18 @@ public static final class ButtonType public static final int BUTTON_DOWN = 7; public static final int BUTTON_LEFT = 8; public static final int BUTTON_RIGHT = 9; - public static final int STICK_MAIN_UP = 10; - public static final int STICK_MAIN_DOWN = 11; - public static final int STICK_MAIN_LEFT = 12; - public static final int STICK_MAIN_RIGHT = 13; - public static final int STICK_C_UP = 14; - public static final int STICK_C_DOWN = 15; - public static final int STICK_C_LEFT = 16; - public static final int STICK_C_RIGHT = 17; - public static final int TRIGGER_L = 18; - public static final int TRIGGER_R = 19; + public static final int STICK_MAIN = 10; + public static final int STICK_MAIN_UP = 11; + public static final int STICK_MAIN_DOWN = 12; + public static final int STICK_MAIN_LEFT = 13; + public static final int STICK_MAIN_RIGHT = 14; + public static final int STICK_C = 15; + public static final int STICK_C_UP = 16; + public static final int STICK_C_DOWN = 17; + public static final int STICK_C_LEFT = 18; + public static final int STICK_C_RIGHT = 19; + public static final int TRIGGER_L = 20; + public static final int TRIGGER_R = 21; } /** diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java index 359f25f17ed9..26f628ed46ce 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java @@ -50,8 +50,7 @@ public InputOverlay(Context context, AttributeSet attrs) overlayButtons.add(initializeOverlayButton(context, R.drawable.button_start, ButtonType.BUTTON_START)); overlayJoysticks.add(initializeOverlayJoystick(context, R.drawable.joy_outer, R.drawable.joy_inner, - ButtonType.STICK_MAIN_UP, ButtonType.STICK_MAIN_DOWN, - ButtonType.STICK_MAIN_LEFT, ButtonType.STICK_MAIN_RIGHT)); + ButtonType.STICK_MAIN)); // Set the on touch listener. setOnTouchListener(this); @@ -200,14 +199,11 @@ private static InputOverlayDrawableButton initializeOverlayButton(Context contex * @param context The current {@link Context} * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds). * @param resInner Resource ID for the inner image of the joystick (the one you actually move around). - * @param axisUp Identifier for this type of axis. - * @param axisDown Identifier for this type of axis. - * @param axisLeft Identifier for this type of axis. - * @param axisRight Identifier for this type of axis. + * @param joystick Identifier for which joystick this is. * * @return the initialized {@link InputOverlayDrawableJoystick}. */ - private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context, int resOuter, int resInner, int axisUp, int axisDown, int axisLeft, int axisRight) + private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context, int resOuter, int resInner, int joystick) { // Resources handle for fetching the initial Drawable resource. final Resources res = context.getResources(); @@ -219,22 +215,26 @@ private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context co final Bitmap bitmapOuter = BitmapFactory.decodeResource(res, resOuter); final Bitmap bitmapInner = BitmapFactory.decodeResource(res, resInner); - // TODO: Load coordinates for the drawable from the SharedPreference keys - // made from the overlay configuration window in the settings. + // String ID of the Drawable. This is what is passed into SharedPreferences + // to check whether or not a value has been set. + final String drawableId = res.getResourceEntryName(resOuter); + + // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. + // These were set in the input overlay configuration menu. + int drawableX = (int) sPrefs.getFloat(drawableId+"-X", 0f); + int drawableY = (int) sPrefs.getFloat(drawableId+"-Y", 0f); // Now set the bounds for the InputOverlayDrawableJoystick. // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be. int outerSize = bitmapOuter.getWidth() + 256; - int X = 0; - int Y = 0; - Rect outerRect = new Rect(X, Y, X + outerSize, Y + outerSize); + Rect outerRect = new Rect(drawableX, drawableY, drawableX + outerSize, drawableY + outerSize); Rect innerRect = new Rect(0, 0, outerSize / 4, outerSize / 4); final InputOverlayDrawableJoystick overlayDrawable = new InputOverlayDrawableJoystick(res, bitmapOuter, bitmapInner, outerRect, innerRect, - axisUp, axisDown, axisLeft, axisRight); + joystick); return overlayDrawable; diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java index a115305fa298..9b5dd6b86033 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java @@ -29,15 +29,12 @@ public final class InputOverlayDrawableJoystick extends BitmapDrawable * * @param res {@link Resources} instance. * @param bitmapOuter {@link Bitmap} to use with this Drawable. - * @param axisUp Identifier for this type of axis. - * @param axisDown Identifier for this type of axis. - * @param axisLeft Identifier for this type of axis. - * @param axisRight Identifier for this type of axis. + * @param joystick Identifier for which joystick this is. */ public InputOverlayDrawableJoystick(Resources res, Bitmap bitmapOuter, Bitmap bitmapInner, Rect rectOuter, Rect rectInner, - int axisUp, int axisDown, int axisLeft, int axisRight) + int joystick) { super(res, bitmapOuter); this.setBounds(rectOuter); @@ -45,10 +42,10 @@ public InputOverlayDrawableJoystick(Resources res, this.ringInner = new BitmapDrawable(res, bitmapInner); this.ringInner.setBounds(rectInner); SetInnerBounds(); - this.axisIDs[0] = axisUp; - this.axisIDs[1] = axisDown; - this.axisIDs[2] = axisLeft; - this.axisIDs[3] = axisRight; + this.axisIDs[0] = joystick + 1; + this.axisIDs[1] = joystick + 2; + this.axisIDs[2] = joystick + 3; + this.axisIDs[3] = joystick + 4; } @Override @@ -142,5 +139,6 @@ private void SetInnerBounds() int height = this.ringInner.getBounds().height() / 2; this.ringInner.setBounds(X - width, Y - height, X + width, Y + height); + } } diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/gamelist/GameListActivity.java b/Source/Android/src/org/dolphinemu/dolphinemu/gamelist/GameListActivity.java index 96f1783310d6..c24d21a74b3a 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/gamelist/GameListActivity.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/gamelist/GameListActivity.java @@ -16,13 +16,12 @@ import android.os.Bundle; import android.support.v4.app.ActionBarDrawerToggle; import android.support.v4.widget.DrawerLayout; -import android.view.*; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; import android.widget.AdapterView; import android.widget.ListView; - -import java.util.ArrayList; -import java.util.List; - import org.dolphinemu.dolphinemu.AboutFragment; import org.dolphinemu.dolphinemu.NativeLibrary; import org.dolphinemu.dolphinemu.R; @@ -31,6 +30,9 @@ import org.dolphinemu.dolphinemu.sidemenu.SideMenuAdapter; import org.dolphinemu.dolphinemu.sidemenu.SideMenuItem; +import java.util.ArrayList; +import java.util.List; + /** * The activity that implements all of the functions * for the game list. diff --git a/Source/Core/DolphinWX/Src/Android/ButtonManager.h b/Source/Core/DolphinWX/Src/Android/ButtonManager.h index 5a99bb092f13..383e52f4455b 100644 --- a/Source/Core/DolphinWX/Src/Android/ButtonManager.h +++ b/Source/Core/DolphinWX/Src/Android/ButtonManager.h @@ -28,25 +28,27 @@ namespace ButtonManager enum ButtonType { BUTTON_A = 0, - BUTTON_B, - BUTTON_START, - BUTTON_X, - BUTTON_Y, - BUTTON_Z, - BUTTON_UP, - BUTTON_DOWN, - BUTTON_LEFT, - BUTTON_RIGHT, - STICK_MAIN_UP, - STICK_MAIN_DOWN, - STICK_MAIN_LEFT, - STICK_MAIN_RIGHT, - STICK_C_UP, - STICK_C_DOWN, - STICK_C_LEFT, - STICK_C_RIGHT, - TRIGGER_L, - TRIGGER_R + BUTTON_B = 1, + BUTTON_START = 2, + BUTTON_X = 3, + BUTTON_Y = 4, + BUTTON_Z = 5, + BUTTON_UP = 6, + BUTTON_DOWN = 7, + BUTTON_LEFT = 8, + BUTTON_RIGHT = 9, + STICK_MAIN = 10, /* Used on Java Side */ + STICK_MAIN_UP = 11, + STICK_MAIN_DOWN = 12, + STICK_MAIN_LEFT = 13, + STICK_MAIN_RIGHT = 14, + STICK_C = 15, /* Used on Java Side */ + STICK_C_UP = 16, + STICK_C_DOWN = 17, + STICK_C_LEFT = 18, + STICK_C_RIGHT = 19, + TRIGGER_L = 20, + TRIGGER_R = 21, }; enum ButtonState { From 53ab104d5f1a9523dbd2e12adb0a131c052d90e8 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Tue, 19 Nov 2013 15:53:30 -0500 Subject: [PATCH 082/202] [Android] Expand the input binding UI in the settings to handle 4 Gamecube controllers in the future. Other changes: - Broke out MotionAlertDialog into it's own separate class. - Made a preference specifically for handling input bindings. --- Source/Android/res/values-ja/strings.xml | 4 + Source/Android/res/values/strings.xml | 4 + Source/Android/res/xml/input_prefs.xml | 414 ++++++++++++++---- .../input/InputBindingPreference.java | 55 +++ .../settings/input/InputConfigFragment.java | 214 ++------- .../settings/input/MotionAlertDialog.java | 127 ++++++ 6 files changed, 557 insertions(+), 261 deletions(-) create mode 100644 Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputBindingPreference.java create mode 100644 Source/Android/src/org/dolphinemu/dolphinemu/settings/input/MotionAlertDialog.java diff --git a/Source/Android/res/values-ja/strings.xml b/Source/Android/res/values-ja/strings.xml index 598799c30e5d..bab1991ce35d 100644 --- a/Source/Android/res/values-ja/strings.xml +++ b/Source/Android/res/values-ja/strings.xml @@ -46,6 +46,10 @@ 入力オーバーレイレイアウト 入力オーバーレイのためのボタンのレイアウト。 ゲームキューブの入力バインディング + コントローラ1 + コントローラ2 + コントローラ3 + コントローラ4 入力バインディング %1$sにバインドするための入力を移動または押してください。 Aボタン diff --git a/Source/Android/res/values/strings.xml b/Source/Android/res/values/strings.xml index 517a80dbd3be..353559a0edcb 100644 --- a/Source/Android/res/values/strings.xml +++ b/Source/Android/res/values/strings.xml @@ -46,6 +46,10 @@ Input Overlay Layout Button layout for the input overlay. Gamecube Input Bindings + Controller 1 + Controller 2 + Controller 3 + Controller 4 Input Binding Press or move an input to bind it to %1$s. Button A diff --git a/Source/Android/res/xml/input_prefs.xml b/Source/Android/res/xml/input_prefs.xml index afd32b476bec..30b069c37a48 100644 --- a/Source/Android/res/xml/input_prefs.xml +++ b/Source/Android/res/xml/input_prefs.xml @@ -1,8 +1,7 @@ - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputBindingPreference.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputBindingPreference.java new file mode 100644 index 000000000000..e182f93a5cc3 --- /dev/null +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputBindingPreference.java @@ -0,0 +1,55 @@ +package org.dolphinemu.dolphinemu.settings.input; + +import org.dolphinemu.dolphinemu.R; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.preference.Preference; +import android.util.AttributeSet; + +/** + * {@link Preference} subclass that represents a preference + * used for assigning a key bind. + */ +public final class InputBindingPreference extends Preference +{ + /** + * Constructor that is called when inflating an InputBindingPreference from XML. + * + * @param context The current {@link Context}. + * @param attrs The attributes of the XML tag that is inflating the preference. + */ + public InputBindingPreference(Context context, AttributeSet attrs) + { + super(context, attrs); + } + + @Override + protected void onClick() + { + // Begin the creation of the input alert. + final MotionAlertDialog dialog = new MotionAlertDialog(getContext(), this); + + // Set the cancel button. + dialog.setButton(AlertDialog.BUTTON_NEGATIVE, getContext().getString(R.string.cancel), new AlertDialog.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + // Do nothing. Just makes the cancel button show up. + } + }); + + // Set the title and description message. + dialog.setTitle(R.string.input_binding); + dialog.setMessage(String.format(getContext().getString(R.string.input_binding_descrip), getTitle())); + + // Don't allow the dialog to close when a user taps + // outside of it. They must press cancel or provide an input. + dialog.setCanceledOnTouchOutside(false); + + // Everything is set, show the dialog. + dialog.show(); + } +} diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java index 657c5f081731..354401060412 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java @@ -6,21 +6,16 @@ package org.dolphinemu.dolphinemu.settings.input; -import android.app.AlertDialog; +import java.util.List; + import android.app.Fragment; -import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; -import android.util.Log; -import android.view.*; - -import java.util.ArrayList; -import java.util.List; +import android.view.InputDevice; import org.dolphinemu.dolphinemu.NativeLibrary; import org.dolphinemu.dolphinemu.R; @@ -31,6 +26,37 @@ */ public final class InputConfigFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + // Expand the preferences from the XML. + addPreferencesFromResource(R.xml.input_prefs); + + // Set the summary messages of the preferences to whatever binding + // is currently set within the Dolphin config. + final String[] keys = + { + "InputA", "InputB", "InputX", "InputY", "InputZ", "InputStart", + "DPadUp", "DPadDown", "DPadLeft", "DPadRight", + "MainUp", "MainDown", "MainLeft", "MainRight", + "CStickUp", "CStickDown", "CStickLeft", "CStickRight", + "InputL", "InputR" + }; + + // Loop through the keys for all 4 GameCube controllers. + for (int i = 1; i <= 4; i++) + { + for (String key : keys) + { + final String binding = NativeLibrary.GetConfig("Dolphin.ini", "Android", key+"_"+i, "None"); + final Preference pref = findPreference(key+"_"+i); + pref.setSummary(binding); + } + } + } + /** * Gets the descriptor for the given {@link InputDevice}. * @@ -59,66 +85,9 @@ public static String getInputDesc(InputDevice input) } } - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - // Expand the preferences from the XML. - addPreferencesFromResource(R.xml.input_prefs); - - // Set the summary messages of the preferences to whatever binding - // is currently set within the Dolphin config. - final String[] keys = - { - "InputA", "InputB", "InputX", "InputY", "InputZ", "InputStart", - "DPadUp", "DPadDown", "DPadLeft", "DPadRight", - "MainUp", "MainDown", "MainLeft", "MainRight", - "CStickUp", "CStickDown", "CStickLeft", "CStickRight", - "InputL", "InputR", - }; - - Preference pref; - for (String key : keys) - { - String binding = NativeLibrary.GetConfig("Dolphin.ini", "Android", key, "None"); - pref = findPreference(key); - pref.setSummary(binding); - } - } - @Override public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference pref) { - // If the user is on the preference screen to set Gamecube input bindings. - if (screen.getTitle().equals(getString(R.string.gamecube_bindings))) - { - // Begin the creation of the input alert. - final MotionAlertDialog dialog = new MotionAlertDialog(getActivity(), pref); - - // Set the cancel button. - dialog.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.cancel), new AlertDialog.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - // Do nothing. Just makes the cancel button show up. - } - }); - - // Set the title and description message. - dialog.setTitle(R.string.input_binding); - dialog.setMessage(String.format(getString(R.string.input_binding_descrip), pref.getTitle())); - - // Don't allow the dialog to close when a user taps - // outside of it. They must press cancel or provide an input. - dialog.setCanceledOnTouchOutside(false); - - // Everything is set, show the dialog. - dialog.show(); - return true; - } - // If the user has clicked the option to configure the input overlay. if (pref.getTitle().equals(getString(R.string.input_overlay_layout))) { @@ -129,119 +98,4 @@ public void onClick(DialogInterface dialog, int which) return false; } - - /** - * {@link AlertDialog} derivative that listens for - * motion events from controllers and joysticks. - */ - private static final class MotionAlertDialog extends AlertDialog - { - // The selected input preference - private final Preference inputPref; - - private boolean firstEvent = true; - private final ArrayList m_values = new ArrayList(); - - /** - * Constructor - * - * @param ctx The current {@link Context}. - * @param inputPref The Preference to show this dialog for. - */ - public MotionAlertDialog(Context ctx, Preference inputPref) - { - super(ctx); - - this.inputPref = inputPref; - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) - { - Log.d("InputConfigFragment", "Received key event: " + event.getAction()); - switch (event.getAction()) - { - case KeyEvent.ACTION_DOWN: - case KeyEvent.ACTION_UP: - InputDevice input = event.getDevice(); - String bindStr = "Device '" + getInputDesc(input) + "'-Button " + event.getKeyCode(); - NativeLibrary.SetConfig("Dolphin.ini", "Android", inputPref.getKey(), bindStr); - inputPref.setSummary(bindStr); - dismiss(); - return true; - - default: - break; - } - - return false; - } - - - // Method that will be called within dispatchGenericMotionEvent - // that handles joystick/controller movements. - private boolean onMotionEvent(MotionEvent event) - { - if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) - return false; - - Log.d("InputConfigFragment", "Received motion event: " + event.getAction()); - - InputDevice input = event.getDevice(); - List motions = input.getMotionRanges(); - if (firstEvent) - { - m_values.clear(); - - for (InputDevice.MotionRange range : motions) - { - m_values.add(event.getAxisValue(range.getAxis())); - } - - firstEvent = false; - } - else - { - for (int a = 0; a < motions.size(); ++a) - { - InputDevice.MotionRange range = motions.get(a); - - if (m_values.get(a) > (event.getAxisValue(range.getAxis()) + 0.5f)) - { - String bindStr = "Device '" + InputConfigFragment.getInputDesc(input) + "'-Axis " + range.getAxis() + "-"; - NativeLibrary.SetConfig("Dolphin.ini", "Android", inputPref.getKey(), bindStr); - inputPref.setSummary(bindStr); - dismiss(); - } - else if (m_values.get(a) < (event.getAxisValue(range.getAxis()) - 0.5f)) - { - String bindStr = "Device '" + InputConfigFragment.getInputDesc(input) + "'-Axis " + range.getAxis() + "+"; - NativeLibrary.SetConfig("Dolphin.ini", "Android", inputPref.getKey(), bindStr); - inputPref.setSummary(bindStr); - dismiss(); - } - } - } - - return true; - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) - { - if (onKeyDown(event.getKeyCode(), event)) - return true; - - return super.dispatchKeyEvent(event); - } - - @Override - public boolean dispatchGenericMotionEvent(MotionEvent event) - { - if (onMotionEvent(event)) - return true; - - return super.dispatchGenericMotionEvent(event); - } - } } diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/MotionAlertDialog.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/MotionAlertDialog.java new file mode 100644 index 000000000000..58d3a933a179 --- /dev/null +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/MotionAlertDialog.java @@ -0,0 +1,127 @@ +package org.dolphinemu.dolphinemu.settings.input; + +import java.util.ArrayList; +import java.util.List; + +import org.dolphinemu.dolphinemu.NativeLibrary; + +import android.app.AlertDialog; +import android.content.Context; +import android.preference.Preference; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; + +/** + * {@link AlertDialog} derivative that listens for + * motion events from controllers and joysticks. + */ +final class MotionAlertDialog extends AlertDialog +{ + // The selected input preference + private final Preference inputPref; + + private boolean firstEvent = true; + private final ArrayList m_values = new ArrayList(); + + /** + * Constructor + * + * @param ctx The current {@link Context}. + * @param inputPref The Preference to show this dialog for. + */ + public MotionAlertDialog(Context ctx, Preference inputPref) + { + super(ctx); + + this.inputPref = inputPref; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) + { + Log.d("InputConfigFragment", "Received key event: " + event.getAction()); + switch (event.getAction()) + { + case KeyEvent.ACTION_DOWN: + case KeyEvent.ACTION_UP: + InputDevice input = event.getDevice(); + String bindStr = "Device '" + InputConfigFragment.getInputDesc(input) + "'-Button " + event.getKeyCode(); + NativeLibrary.SetConfig("Dolphin.ini", "Android", inputPref.getKey(), bindStr); + inputPref.setSummary(bindStr); + dismiss(); + return true; + + default: + return false; + } + } + + + // Method that will be called within dispatchGenericMotionEvent + // that handles joystick/controller movements. + private boolean onMotionEvent(MotionEvent event) + { + if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) + return false; + + Log.d("InputConfigFragment", "Received motion event: " + event.getAction()); + + InputDevice input = event.getDevice(); + List motions = input.getMotionRanges(); + if (firstEvent) + { + m_values.clear(); + + for (InputDevice.MotionRange range : motions) + { + m_values.add(event.getAxisValue(range.getAxis())); + } + + firstEvent = false; + } + else + { + for (int a = 0; a < motions.size(); ++a) + { + InputDevice.MotionRange range = motions.get(a); + + if (m_values.get(a) > (event.getAxisValue(range.getAxis()) + 0.5f)) + { + String bindStr = "Device '" + InputConfigFragment.getInputDesc(input) + "'-Axis " + range.getAxis() + "-"; + NativeLibrary.SetConfig("Dolphin.ini", "Android", inputPref.getKey(), bindStr); + inputPref.setSummary(bindStr); + dismiss(); + } + else if (m_values.get(a) < (event.getAxisValue(range.getAxis()) - 0.5f)) + { + String bindStr = "Device '" + InputConfigFragment.getInputDesc(input) + "'-Axis " + range.getAxis() + "+"; + NativeLibrary.SetConfig("Dolphin.ini", "Android", inputPref.getKey(), bindStr); + inputPref.setSummary(bindStr); + dismiss(); + } + } + } + + return true; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) + { + if (onKeyDown(event.getKeyCode(), event)) + return true; + + return super.dispatchKeyEvent(event); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) + { + if (onMotionEvent(event)) + return true; + + return super.dispatchGenericMotionEvent(event); + } +} \ No newline at end of file From 7e544d8996d1365d270b57527583efcdb14b4e76 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Tue, 19 Nov 2013 16:14:40 -0500 Subject: [PATCH 083/202] [Android] Basic UI functionality for enabling/disabling aforementioned Gamecube controllers. --- Source/Android/res/values-ja/strings.xml | 1 + Source/Android/res/values/strings.xml | 1 + Source/Android/res/xml/input_prefs.xml | 96 ++++++++++++++++++++++++ 3 files changed, 98 insertions(+) diff --git a/Source/Android/res/values-ja/strings.xml b/Source/Android/res/values-ja/strings.xml index bab1991ce35d..2415e704bf15 100644 --- a/Source/Android/res/values-ja/strings.xml +++ b/Source/Android/res/values-ja/strings.xml @@ -50,6 +50,7 @@ コントローラ2 コントローラ3 コントローラ4 + コントローラを有効 入力バインディング %1$sにバインドするための入力を移動または押してください。 Aボタン diff --git a/Source/Android/res/values/strings.xml b/Source/Android/res/values/strings.xml index 353559a0edcb..ada8417fbfcf 100644 --- a/Source/Android/res/values/strings.xml +++ b/Source/Android/res/values/strings.xml @@ -50,6 +50,7 @@ Controller 2 Controller 3 Controller 4 + Enable controller Input Binding Press or move an input to bind it to %1$s. Button A diff --git a/Source/Android/res/xml/input_prefs.xml b/Source/Android/res/xml/input_prefs.xml index 30b069c37a48..439d80ebe30b 100644 --- a/Source/Android/res/xml/input_prefs.xml +++ b/Source/Android/res/xml/input_prefs.xml @@ -13,332 +13,428 @@ + + + + + + + + From 42f8562e5c7acfb6b697672f419f0f28366a5261 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Tue, 19 Nov 2013 19:28:10 -0500 Subject: [PATCH 084/202] [Android] Basic Wiimote binding settings now implemented. --- Source/Android/res/values-ja/strings.xml | 31 +- Source/Android/res/values/strings.xml | 39 +- Source/Android/res/xml/input_prefs.xml | 600 ++++++++++++++++++ .../settings/input/InputConfigFragment.java | 24 +- 4 files changed, 685 insertions(+), 9 deletions(-) diff --git a/Source/Android/res/values-ja/strings.xml b/Source/Android/res/values-ja/strings.xml index 2415e704bf15..5b71679efd55 100644 --- a/Source/Android/res/values-ja/strings.xml +++ b/Source/Android/res/values-ja/strings.xml @@ -45,14 +45,20 @@ 入力 入力オーバーレイレイアウト 入力オーバーレイのためのボタンのレイアウト。 - ゲームキューブの入力バインディング + ゲームキューブコントローラの入力バインディング コントローラ1 コントローラ2 コントローラ3 コントローラ4 コントローラを有効 + Wiiリモコンの入力バインディング + Wiiリモコン1 + Wiiリモコン2 + Wiiリモコン3 + Wiiリモコン4 入力バインディング %1$sにバインドするための入力を移動または押してください。 + Aボタン Bボタン スタートボタン @@ -73,6 +79,29 @@ C-スティック: → 左のトリガー 右のトリガー + + ボタン1 + ボタン2 + ボタン+ + ボタン- + ホームボタン + IR ↑ + IR ↓ + IR ← + IR → + IR前方 + IR後方 + スイング ↑ + スイング ↓ + スイング ← + スイング → + 前方を傾 + 後方を傾 + 左を傾 + 右を傾 + Xを振る + Yを振る + Zを振る Interpreter diff --git a/Source/Android/res/values/strings.xml b/Source/Android/res/values/strings.xml index ada8417fbfcf..86cba617635c 100644 --- a/Source/Android/res/values/strings.xml +++ b/Source/Android/res/values/strings.xml @@ -45,14 +45,21 @@ Input Input Overlay Layout Button layout for the input overlay. - Gamecube Input Bindings + Gamecube Controller Bindings Controller 1 Controller 2 Controller 3 Controller 4 Enable controller + Wiimote Bindings + Wiimote 1 + Wiimote 2 + Wiimote 3 + Wiimote 4 + Enable Wiimote Input Binding Press or move an input to bind it to %1$s. + Button A Button B Button Start @@ -73,6 +80,29 @@ C Stick Right Trigger L Trigger R + + Button 1 + Button 2 + Button + + Button - + Button Home + IR Up + IR Down + IR Left + IR Right + IR Forward + IR Backward + Swing Up + Swing Down + Swing Left + Swing Right + Tilt Forward + Tilt Backward + Tilt Left + Tilt Right + Shake X + Shake Y + Shake Z Interpreter @@ -96,7 +126,7 @@ %s Show FPS Show the number of frames rendered per second as a measure of emulation speed. - + Enhancements Internal Resolution Specifies the resolution used to render at. A high resolution will improve visual quality a lot but is also quite heavy on performance and might cause glitches in certain games. @@ -112,7 +142,7 @@ Force texture filtering even if the emulated game explicitly disabled it. Improves texture quality slightly but causes glitches in some games. Disable Fog Makes distant objects more visible by removing fog, thus increasing the overall detail. Disabling fog will break some games which rely on proper fog emulation. - + Hacks Embedded Frame Buffer Skip EFB Access from CPU @@ -140,13 +170,12 @@ Disables emulation of a hardware feature called destination alpha, which is used in many games for various effects. Fast Depth Calculation Uses a less accurate algorithm to calculate depth values. - + Yes No Cancel Disabled Other - diff --git a/Source/Android/res/xml/input_prefs.xml b/Source/Android/res/xml/input_prefs.xml index 439d80ebe30b..22cf0117e9bf 100644 --- a/Source/Android/res/xml/input_prefs.xml +++ b/Source/Android/res/xml/input_prefs.xml @@ -7,6 +7,7 @@ android:summary="@string/input_overlay_layout_desc" android:title="@string/input_overlay_layout"/> + @@ -439,4 +440,603 @@ android:title="@string/trigger_right" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java index 354401060412..b71baba56529 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputConfigFragment.java @@ -36,7 +36,7 @@ public void onCreate(Bundle savedInstanceState) // Set the summary messages of the preferences to whatever binding // is currently set within the Dolphin config. - final String[] keys = + final String[] gamecubeKeys = { "InputA", "InputB", "InputX", "InputY", "InputZ", "InputStart", "DPadUp", "DPadDown", "DPadLeft", "DPadRight", @@ -45,10 +45,28 @@ public void onCreate(Bundle savedInstanceState) "InputL", "InputR" }; - // Loop through the keys for all 4 GameCube controllers. + final String[] wiimoteKeys = + { + "WiimoteInputA", "WiimoteInputB", "WiimoteInputOne", "WiimoteInputTwo", "WiimoteInputPlus", "WiimoteInputMinus", "WiimoteInputHome", + "WiimoteIRUp", "WiimoteIRDown", "WiimoteIRLeft", "WiimoteIRRight", "WiimoteIRForward", "WiimoteIRBackward", + "WiimoteSwingUp", "WiimoteSwingDown", "WiimoteSwingLeft", "WiimoteSwingRight", + "WiimoteTiltForward", "WiimoteTiltBackward", "WiimoteTiltLeft", "WiimoteTiltRight", + "WiimoteShakeX", "WiimoteShakeY", "WiimoteShakeZ", + "WiimoteDPadUp", "WiimoteDPadDown", "WiimoteDPadLeft", "WiimoteDPadRight" + }; + for (int i = 1; i <= 4; i++) { - for (String key : keys) + // Loop through the keys for all 4 GameCube controllers. + for (String key : gamecubeKeys) + { + final String binding = NativeLibrary.GetConfig("Dolphin.ini", "Android", key+"_"+i, "None"); + final Preference pref = findPreference(key+"_"+i); + pref.setSummary(binding); + } + + // Loop through the keys for the Wiimote + for (String key : wiimoteKeys) { final String binding = NativeLibrary.GetConfig("Dolphin.ini", "Android", key+"_"+i, "None"); final Preference pref = findPreference(key+"_"+i); From 4ee062906938be202933a88d6c0fd7b3b10309aa Mon Sep 17 00:00:00 2001 From: comex Date: Wed, 20 Nov 2013 11:52:59 -0500 Subject: [PATCH 085/202] Cleanup: ENetHostClient -> NetHost, TraversalClient no longer a subclass --- Source/Core/Common/CMakeLists.txt | 1 + Source/Core/Common/Common.vcxproj | 2 + Source/Core/Common/Common.vcxproj.filters | 1 + Source/Core/Common/Src/NetHost.cpp | 422 ++++++++++++++++++ Source/Core/Common/Src/NetHost.h | 145 +++++++ Source/Core/Common/Src/TraversalClient.cpp | 476 +++------------------ Source/Core/Common/Src/TraversalClient.h | 139 +----- Source/Core/Core/Src/NetPlayClient.cpp | 51 +-- Source/Core/Core/Src/NetPlayClient.h | 8 +- Source/Core/Core/Src/NetPlayServer.cpp | 40 +- Source/Core/Core/Src/NetPlayServer.h | 7 +- Source/Core/DolphinWX/Src/NetWindow.cpp | 4 +- 12 files changed, 701 insertions(+), 595 deletions(-) create mode 100644 Source/Core/Common/Src/NetHost.cpp create mode 100644 Source/Core/Common/Src/NetHost.h diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index 6f6ede965e7b..473e44600365 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -13,6 +13,7 @@ set(SRCS Src/BreakPoints.cpp Src/Misc.cpp Src/MsgHandler.cpp Src/NandPaths.cpp + Src/NetHost.cpp Src/SettingsHandler.cpp Src/SDCardUtil.cpp Src/StringUtil.cpp diff --git a/Source/Core/Common/Common.vcxproj b/Source/Core/Common/Common.vcxproj index 24dc66fc017c..cae1343bb1f7 100644 --- a/Source/Core/Common/Common.vcxproj +++ b/Source/Core/Common/Common.vcxproj @@ -74,6 +74,7 @@ + @@ -110,6 +111,7 @@ + diff --git a/Source/Core/Common/Common.vcxproj.filters b/Source/Core/Common/Common.vcxproj.filters index efb3b286c8a1..e9a495cb944c 100644 --- a/Source/Core/Common/Common.vcxproj.filters +++ b/Source/Core/Common/Common.vcxproj.filters @@ -36,6 +36,7 @@ + diff --git a/Source/Core/Common/Src/NetHost.cpp b/Source/Core/Common/Src/NetHost.cpp new file mode 100644 index 000000000000..a5c06b3a00d4 --- /dev/null +++ b/Source/Core/Common/Src/NetHost.cpp @@ -0,0 +1,422 @@ +// Copyright 2013 Dolphin Emulator Project +// Licensed under GPLv2 +// Refer to the license.txt file included. + +#include "NetHost.h" +#include "TraversalClient.h" + +inline ENetPacket* ENetUtil::MakeENetPacket(Packet&& pac, enet_uint32 flags) +{ + ENetPacket* packet = (ENetPacket*) enet_malloc (sizeof (ENetPacket)); + packet->dataLength = pac.vec->size(); + packet->data = pac.vec->release_data(); + packet->referenceCount = 0; + packet->flags = flags; + packet->freeCallback = NULL; + return packet; +} + +void ENetUtil::SendPacket(ENetPeer* peer, Packet&& pac, bool reliable) +{ + enet_peer_send(peer, 0, MakeENetPacket(std::move(pac), reliable ? ENET_PACKET_FLAG_RELIABLE : ENET_PACKET_FLAG_UNSEQUENCED)); +} + +Packet ENetUtil::MakePacket(ENetPacket* epacket) +{ + Packet pac(PWBuffer(epacket->data, epacket->dataLength, PWBuffer::NoCopy)); + epacket->data = NULL; + enet_packet_destroy(epacket); + return pac; +} + +void ENetUtil::Wakeup(ENetHost* host) +{ + // Send ourselves a spurious message. This is hackier than it should be. + // I reported this as https://github.com/lsalzman/enet/issues/23, so + // hopefully there will be a better way to do it soon. + ENetAddress address; + if (host->address.port != 0) + address.port = host->address.port; + else + enet_socket_get_address(host->socket, &address); + address.host = 0x0100007f; // localhost + + u8 byte = 0; + ENetBuffer buf; + buf.data = &byte; + buf.dataLength = 1; + enet_socket_send(host->socket, &address, &buf, 1); +} + + + +NetHost::NetHost(size_t peerCount, u16 port) +{ + m_GlobalSequenceNumber = 0; + m_GlobalTicker = 0; + m_TraversalClient = NULL; + + ENetAddress addr = { ENET_HOST_ANY, port }; + m_Host = enet_host_create( + &addr, // address + peerCount, // peerCount + 1, // channelLimit + 0, // incomingBandwidth + 0); // outgoingBandwidth + + if (!m_Host) + return; + + m_Host->intercept = NetHost::InterceptCallback; + // Unfortunately, there is no good place to stash this. + m_Host->compressor.destroy = (decltype(m_Host->compressor.destroy)) this; + + m_Client = NULL; + m_ShouldEndThread = false; + m_Thread = std::thread(std::mem_fun(&NetHost::ThreadFunc), this); +} + +NetHost::~NetHost() +{ + // only happens during static deinit + if (m_TraversalClient) + m_TraversalClient->m_NetHost = NULL; + if (m_Host) + { + Reset(); + RunOnThread([=]() { + ASSUME_ON(NET); + m_ShouldEndThread = true; + }); + m_Thread.join(); + enet_host_destroy(m_Host); + } +} + +void NetHost::RunOnThread(std::function func) +{ + { + std::lock_guard lk(m_RunQueueWriteLock); + m_RunQueue.Push(func); + } + ENetUtil::Wakeup(m_Host); +} + +void NetHost::RunOnThreadSync(std::function func) +{ + Common::Event evt; + RunOnThread([&]() { + func(); + evt.Set(); + }); + evt.Wait(); +} + +void NetHost::Reset() +{ + // Sync up with the thread and disconnect everyone. + RunOnThreadSync([&]() { + for (size_t i = 0; i < m_Host->peerCount; i++) + { + ENetPeer* peer = &m_Host->peers[i]; + if (peer->state != ENET_PEER_STATE_DISCONNECTED) + enet_peer_disconnect_later(peer, 0); + } + }); + m_Client = NULL; +} + +void NetHost::BroadcastPacket(Packet&& packet, ENetPeer* except) +{ + if (packet.vec->size() < MaxShortPacketLength) + { + u16 seq = m_GlobalSequenceNumber++; + m_OutgoingPacketInfo.push_back(OutgoingPacketInfo(std::move(packet), except, seq, m_GlobalTicker++)); + size_t peer = 0; + for (auto& pi : m_PeerInfo) + { + if (&m_Host->peers[peer++] == except) + continue; + pi.m_GlobalSeqToSeq[seq] = pi.m_OutgoingSequenceNumber++; + } + } + else + { + Packet container; + container.W((u16) 0); + container.W((PointerWrap&) packet); + + // avoid copying + ENetPacket* epacket = NULL; + + for (ENetPeer* peer = m_Host->peers, * end = &m_Host->peers[m_Host->peerCount]; peer != end; peer++) + { + if (peer->state != ENET_PEER_STATE_CONNECTED) + continue; + if (peer == except) + continue; + u16 seq = m_PeerInfo[peer - m_Host->peers].m_OutgoingSequenceNumber++; + if (!epacket) + { + epacket = ENetUtil::MakeENetPacket(std::move(container), ENET_PACKET_FLAG_RELIABLE); + } + else + { + u16* oseqp = (u16 *) epacket->data; + if (*oseqp != seq) + { + epacket = enet_packet_create(epacket->data, epacket->dataLength, ENET_PACKET_FLAG_RELIABLE); + } + } + u16* oseqp = (u16 *) epacket->data; + *oseqp = seq; + enet_peer_send(peer, 0, epacket); + } + if (epacket && epacket->referenceCount == 0) + enet_packet_destroy(epacket); + } +} + +void NetHost::SendPacket(ENetPeer* peer, Packet&& packet) +{ + Packet container; + auto& pi = m_PeerInfo[peer - m_Host->peers]; + container.W((u16) pi.m_OutgoingSequenceNumber++); + container.Do((PointerWrap&) packet); + ENetUtil::SendPacket(peer, std::move(container)); + pi.m_SentPackets++; +} + +void NetHost::MaybeProcessPacketQueue() +{ + if (m_SendTimer.GetTimeDifference() > 6) + { + ProcessPacketQueue(); + m_SendTimer.Update(); + } +} + +void NetHost::ProcessPacketQueue() +{ + // The idea is that we send packets n-1 times unreliably and n times + // reliably. + bool needReliable = false; + int numToRemove = 0; + size_t totalSize = 0; + for (auto it = m_OutgoingPacketInfo.rbegin(); it != m_OutgoingPacketInfo.rend(); ++it) + { + OutgoingPacketInfo& info = *it; + totalSize += info.m_Packet.vec->size(); + if (++info.m_NumSends == MaxPacketSends || totalSize >= MaxShortPacketLength) + { + if (!info.m_DidSendReliably) + needReliable = true; + numToRemove++; + } + } + // this can occasionally cause packets to be sent unnecessarily + // reliably + for (ENetPeer* peer = m_Host->peers, * end = &m_Host->peers[m_Host->peerCount]; peer != end; peer++) + { + if (peer->state != ENET_PEER_STATE_CONNECTED) + continue; + Packet p; + auto& pi = m_PeerInfo[peer - m_Host->peers]; + for (OutgoingPacketInfo& info : m_OutgoingPacketInfo) + { + if (info.m_Except == peer) + continue; + if (pi.m_ConnectTicker > info.m_Ticker) + continue; + p.W(pi.m_GlobalSeqToSeq[info.m_GlobalSequenceNumber]); + p.Do((PointerWrap&) info.m_Packet); + info.m_DidSendReliably = info.m_DidSendReliably || needReliable; + } + if (p.vec->size()) + { + ENetUtil::SendPacket(peer, std::move(p), needReliable); + pi.m_SentPackets++; + } + } + while (numToRemove--) + m_OutgoingPacketInfo.pop_front(); +} + +void NetHost::PrintStats() +{ +#if 1 + if (m_StatsTimer.GetTimeDifference() > 5000) + { + m_StatsTimer.Update(); + for (ENetPeer* peer = m_Host->peers, * end = &m_Host->peers[m_Host->peerCount]; peer != end; peer++) + { + if (peer->state != ENET_PEER_STATE_CONNECTED) + continue; + auto& pi = m_PeerInfo[peer - m_Host->peers]; + char ip[64] = "?"; + enet_address_get_host_ip(&peer->address, ip, sizeof(ip)); + WARN_LOG(NETPLAY, "%speer %u (%s): %f%%+-%f%% packet loss, %u+-%u ms round trip time, %f%% throttle, %u/%u outgoing, %u/%u incoming, %d packets sent since last time\n", + m_TraversalClient ? "" : "(CLIENT) ", // ew + (unsigned) peer->incomingPeerID, + ip, + peer->packetLoss / (float) ENET_PEER_PACKET_LOSS_SCALE, + peer->packetLossVariance / (float) ENET_PEER_PACKET_LOSS_SCALE, + peer->roundTripTime, + peer->roundTripTimeVariance, + peer->packetThrottle / (float) ENET_PEER_PACKET_THROTTLE_SCALE, + (unsigned) enet_list_size(&peer->outgoingReliableCommands), + (unsigned) enet_list_size(&peer->outgoingUnreliableCommands), + peer->channels != NULL ? (unsigned) enet_list_size(&peer->channels->incomingReliableCommands) : 0, + peer->channels != NULL ? (unsigned) enet_list_size(&peer->channels->incomingUnreliableCommands) : 0, + pi.m_SentPackets); + pi.m_SentPackets = 0; + } + } +#endif +} + +u16 NetHost::GetPort() +{ + return m_Host->address.port; +} + + +void NetHost::ThreadFunc() +{ + ASSUME_ON(NET); + Common::SetCurrentThreadName(m_TraversalClient ? "TraversalClient thread" : "NetHost thread"); + while (1) + { + while (!m_RunQueue.Empty()) + { + m_RunQueue.Front()(); + m_RunQueue.Pop(); + } + if (m_ShouldEndThread) break; + ENetEvent event; + ENetAddress address; + if (enet_socket_get_address(m_Host->socket, &address) == -1) + { + PanicAlert("enet_socket_get_address failed."); + continue; + } + int count = enet_host_service(m_Host, &event, m_Host->connectedPeers > 0 ? 5 : 300); + if (count < 0) + { + PanicAlert("enet_host_service failed... do something about this."); + continue; + } + + if (m_TraversalClient) + m_TraversalClient->HandleResends(); + + PrintStats(); + + // Even if there was nothing, forward it as a wakeup. + if (m_Client) + { + switch (event.type) + { + case ENET_EVENT_TYPE_RECEIVE: + OnReceive(&event, ENetUtil::MakePacket(event.packet)); + break; + case ENET_EVENT_TYPE_CONNECT: + { + size_t pid = event.peer - m_Host->peers; + if (pid >= m_PeerInfo.size()) + m_PeerInfo.resize(pid + 1); + auto& pi = m_PeerInfo[pid]; + pi.m_IncomingPackets.clear(); + pi.m_IncomingSequenceNumber = 0; + pi.m_OutgoingSequenceNumber = 0; + pi.m_ConnectTicker = m_GlobalTicker++; + pi.m_SentPackets = 0; + } + /* fall through */ + default: + m_Client->OnENetEvent(&event); + } + } + MaybeProcessPacketQueue(); + } +} + +void NetHost::OnReceive(ENetEvent* event, Packet&& packet) +{ + auto& pi = m_PeerInfo[event->peer - m_Host->peers]; +#if 0 + printf("OnReceive isn=%x\n", pi.m_IncomingSequenceNumber); + DumpBuf(*packet.vec); +#endif + auto& incomingPackets = pi.m_IncomingPackets; + u16 seq; + + while (packet.vec->size() > packet.readOff) + { + { + packet.Do(seq); + if (packet.failure) + goto failure; + + s16 diff = (s16) (seq - pi.m_IncomingSequenceNumber); + if (diff < 0) + { + // assume a duplicate of something we already have + goto skip; + } + + while (incomingPackets.size() <= (size_t) diff) + incomingPackets.push_back(PWBuffer()); + + PWBuffer& buf = incomingPackets[diff]; + if (!buf.empty()) + { + // another type of duplicate + goto skip; + } + + packet.Do(buf); + if (packet.failure) + goto failure; + continue; + } + + skip: + { + u32 size; + packet.Do(size); + if (packet.vec->size() - packet.readOff < size) + goto failure; + packet.readOff += size; + continue; + } + + failure: + { + // strange + WARN_LOG(NETPLAY, "Failure splitting packet - truncation?"); + } + } + + while (!incomingPackets.empty() && !incomingPackets[0].empty()) + { + m_Client->OnData(event, std::move(incomingPackets.front())); + incomingPackets.pop_front(); + pi.m_IncomingSequenceNumber++; + } +} + +int ENET_CALLBACK NetHost::InterceptCallback(ENetHost* host, ENetEvent* event) +{ + ASSUME_ON(NET); + auto self = (NetHost*) host->compressor.destroy; + auto traversalClient = self->m_TraversalClient; + if (host->receivedDataLength == 1 /* wakeup packet */ || + (traversalClient && traversalClient->TestPacket(host->receivedData, host->receivedDataLength, &host->receivedAddress))) + { + event->type = (ENetEventType) 42; + return 1; + } + return 0; +} + diff --git a/Source/Core/Common/Src/NetHost.h b/Source/Core/Common/Src/NetHost.h new file mode 100644 index 000000000000..4a7179282215 --- /dev/null +++ b/Source/Core/Common/Src/NetHost.h @@ -0,0 +1,145 @@ +// Copyright 2013 Dolphin Emulator Project +// Licensed under GPLv2 +// Refer to the license.txt file included. + +#pragma once + +#include "Common.h" +#include "Thread.h" +#include "FifoQueue.h" +#include "enet/enet.h" +#include "Timer.h" +#include "ChunkFile.h" + +DEFINE_THREAD_HAT(NET); + +/* +static inline void DumpBuf(PWBuffer& buf) +{ + printf("+00:"); + int c = 0; + for (size_t i = 0; i < buf.size(); i++) + { + printf(" %02x", buf.data()[i]); + if (++c % 16 == 0) + printf("\n+%02x:", c); + } + printf("\n"); +} +*/ + +// Some trivial utilities that should be moved: + +// Apparently nobody on the C++11 standards committee thought of +// combining two of its most prominent features: lambdas and moves. Nor +// does bind work, despite a StackOverflow answer to the contrary. Derp. +template +class CopyAsMove +{ +public: + CopyAsMove(T&& t) : m_T(std::move(t)) {} + CopyAsMove(const CopyAsMove& other) : m_T((T&&) other.m_T) {} + T& operator*() { return m_T; } +private: + T m_T; +}; + +namespace ENetUtil +{ + ENetPacket* MakeENetPacket(Packet&& pac, enet_uint32 flags); + void SendPacket(ENetPeer* peer, Packet&& pac, bool reliable = true) ON(NET); + Packet MakePacket(ENetPacket* epacket); + void Wakeup(ENetHost* host); + int ENET_CALLBACK nterceptCallback(ENetHost* host, ENetEvent* event) /* ON(NET) */; +} + +class TraversalClient; + +class NetHostClient +{ +public: + virtual void OnENetEvent(ENetEvent* event) ON(NET) = 0; + virtual void OnData(ENetEvent* event, Packet&& packet) ON(NET) = 0; +}; + +// This class provides a wrapper around an ENetHost and adds a layer that +// provides packet merging (enet can fragment packets into multiple UDP packets +// but not merge multiple into one) and opportunistic resending (i.e. the (very +// small) data will be repeatedly sent in the next 4 actual packets, rather +// than waiting for a likely loss). +class NetHost +{ +public: + enum + { + MaxPacketSends = 4, + MaxShortPacketLength = 128, + // This is in here because it needs to be set before a client or server + // actually exists. A bunch of things in enet linearly iterate over + // peerCount peers; probably doesn't matter in practice, but it might + // cause a problem if sending to thousands of clients were ever desired + // (and "DolphinTV" would be nice to have!). + DefaultPeerCount = 50 + }; + + NetHost(size_t peerCount, u16 port); + ~NetHost(); + void RunOnThread(std::function func) NOT_ON(NET); + void RunOnThreadSync(std::function func) NOT_ON(NET); + void CreateThread(); + void Reset(); + + void BroadcastPacket(Packet&& packet, ENetPeer* except = NULL) ON(NET); + void SendPacket(ENetPeer* peer, Packet&& packet) ON(NET); + void PrintStats() ON(NET); + u16 GetPort(); + + NetHostClient* m_Client; + // The traversal client needs to be on the same socket. + TraversalClient* m_TraversalClient ACCESS_ON(NET); + std::function m_InterceptCallback ACCESS_ON(NET); + ENetHost* m_Host; +private: + struct OutgoingPacketInfo + { + OutgoingPacketInfo(Packet&& packet, ENetPeer* except, u16 seq, u64 ticker) + : m_Packet(std::move(packet)), m_Except(except), m_DidSendReliably(false), m_NumSends(0), m_GlobalSequenceNumber(seq), m_Ticker(ticker) {} + + Packet m_Packet; + ENetPeer* m_Except; + bool m_DidSendReliably; + int m_NumSends; + u16 m_GlobalSequenceNumber; + u64 m_Ticker; + }; + + struct PeerInfo + { + std::deque m_IncomingPackets; + // the sequence number of the first element of m_IncomingPackets + u16 m_IncomingSequenceNumber; + u16 m_OutgoingSequenceNumber; + u16 m_GlobalSeqToSeq[65536]; + u64 m_ConnectTicker; + int m_SentPackets; + }; + + void ThreadFunc() /* ON(NET) */; + void OnReceive(ENetEvent* event, Packet&& packet) ON(NET); + void MaybeProcessPacketQueue() ON(NET); + void ProcessPacketQueue() ON(NET); + static int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) /* ON(NET) */; + + Common::FifoQueue, false> m_RunQueue; + std::mutex m_RunQueueWriteLock; + std::thread m_Thread; + bool m_ShouldEndThread ACCESS_ON(NET); + + std::deque m_OutgoingPacketInfo ACCESS_ON(NET); + Common::Timer m_SendTimer ACCESS_ON(NET); + Common::Timer m_StatsTimer ACCESS_ON(NET); + std::vector m_PeerInfo ACCESS_ON(NET); + u16 m_GlobalSequenceNumber ACCESS_ON(NET); + u64 m_GlobalTicker ACCESS_ON(NET); +}; + diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index 8b64a40c303a..17abfd6cda94 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -3,61 +3,6 @@ #include "TraversalClient.h" #include "enet/enet.h" -// derp -inline ENetPacket* ENetUtil::MakeENetPacket(Packet&& pac, enet_uint32 flags) -{ - ENetPacket* packet = (ENetPacket*) enet_malloc (sizeof (ENetPacket)); - packet->dataLength = pac.vec->size(); - packet->data = pac.vec->release_data(); - packet->referenceCount = 0; - packet->flags = flags; - packet->freeCallback = NULL; - return packet; -} - -void ENetUtil::SendPacket(ENetPeer* peer, Packet&& pac, bool reliable) -{ - enet_peer_send(peer, 0, MakeENetPacket(std::move(pac), reliable ? ENET_PACKET_FLAG_RELIABLE : ENET_PACKET_FLAG_UNSEQUENCED)); -} - -Packet ENetUtil::MakePacket(ENetPacket* epacket) -{ - Packet pac(PWBuffer(epacket->data, epacket->dataLength, PWBuffer::NoCopy)); - epacket->data = NULL; - enet_packet_destroy(epacket); - return pac; -} - -int ENET_CALLBACK ENetUtil::InterceptCallback(ENetHost* host, ENetEvent* event) -{ - if (host->receivedDataLength == 1) - { - event->type = (ENetEventType) 42; - return 1; - } - return 0; -} - -void ENetUtil::Wakeup(ENetHost* host) -{ - // Send ourselves a spurious message. This is hackier than it should be. - // I reported this as https://github.com/lsalzman/enet/issues/23, so - // hopefully there will be a better way to do it soon. - ENetAddress address; - if (host->address.port != 0) - address.port = host->address.port; - else - enet_socket_get_address(host->socket, &address); - address.host = 0x0100007f; // localhost - - u8 byte = 0; - ENetBuffer buf; - buf.data = &byte; - buf.dataLength = 1; - enet_socket_send(host->socket, &address, &buf, 1); -} - - static void GetRandomishBytes(u8* buf, size_t size) { // We don't need high quality random numbers (which might not be available), @@ -67,361 +12,34 @@ static void GetRandomishBytes(u8* buf, size_t size) buf[i] = rand() & 0xff; } -ENetHostClient::ENetHostClient(size_t peerCount, u16 port, bool isTraversalClient) -{ - m_isTraversalClient = isTraversalClient; - m_GlobalSequenceNumber = 0; - m_GlobalTicker = 0; - - ENetAddress addr = { ENET_HOST_ANY, port }; - m_Host = enet_host_create( - &addr, // address - peerCount, // peerCount - 1, // channelLimit - 0, // incomingBandwidth - 0); // outgoingBandwidth - - if (!m_Host) - return; - - m_Host->intercept = ENetUtil::InterceptCallback; - - m_Client = NULL; - m_ShouldEndThread = false; - m_Thread = std::thread(std::mem_fun(&ENetHostClient::ThreadFunc), this); -} - -ENetHostClient::~ENetHostClient() -{ - if (m_Host) - { - Reset(); - RunOnThread([=]() { - ASSUME_ON(NET); - m_ShouldEndThread = true; - }); - m_Thread.join(); - enet_host_destroy(m_Host); - } -} - -void ENetHostClient::RunOnThread(std::function func) +TraversalClient::TraversalClient(NetHost* netHost, const std::string& server) { - { - std::lock_guard lk(m_RunQueueWriteLock); - m_RunQueue.Push(func); - } - ENetUtil::Wakeup(m_Host); -} - - -void ENetHostClient::Reset() -{ - // bleh, sync up with the thread - m_ResetEvent.Reset(); - RunOnThread([=]() { - for (size_t i = 0; i < m_Host->peerCount; i++) - { - ENetPeer* peer = &m_Host->peers[i]; - if (peer->state != ENET_PEER_STATE_DISCONNECTED) - enet_peer_disconnect_later(peer, 0); - } - m_ResetEvent.Set(); - }); - m_ResetEvent.Wait(); + m_NetHost = netHost; + m_Server = server; + m_State = InitFailure; m_Client = NULL; -} - -void ENetHostClient::BroadcastPacket(Packet&& packet, ENetPeer* except) -{ - if (packet.vec->size() < MaxShortPacketLength) - { - u16 seq = m_GlobalSequenceNumber++; - m_OutgoingPacketInfo.push_back(OutgoingPacketInfo(std::move(packet), except, seq, m_GlobalTicker++)); - size_t peer = 0; - for (auto& pi : m_PeerInfo) - { - if (&m_Host->peers[peer++] == except) - continue; - pi.m_GlobalSeqToSeq[seq] = pi.m_OutgoingSequenceNumber++; - } - } - else - { - Packet container; - container.W((u16) 0); - container.W((PointerWrap&) packet); - // avoid copying - ENetPacket* epacket = NULL; + m_NetHost->m_TraversalClient = this; - for (ENetPeer* peer = m_Host->peers, * end = &m_Host->peers[m_Host->peerCount]; peer != end; peer++) - { - if (peer->state != ENET_PEER_STATE_CONNECTED) - continue; - if (peer == except) - continue; - u16 seq = m_PeerInfo[peer - m_Host->peers].m_OutgoingSequenceNumber++; - if (!epacket) - { - epacket = ENetUtil::MakeENetPacket(std::move(container), ENET_PACKET_FLAG_RELIABLE); - } - else - { - u16* oseqp = (u16 *) epacket->data; - if (*oseqp != seq) - { - epacket = enet_packet_create(epacket->data, epacket->dataLength, ENET_PACKET_FLAG_RELIABLE); - } - } - u16* oseqp = (u16 *) epacket->data; - *oseqp = seq; - enet_peer_send(peer, 0, epacket); - } - if (epacket && epacket->referenceCount == 0) - enet_packet_destroy(epacket); - } -} - -void ENetHostClient::SendPacket(ENetPeer* peer, Packet&& packet) -{ - Packet container; - auto& pi = m_PeerInfo[peer - m_Host->peers]; - container.W((u16) pi.m_OutgoingSequenceNumber++); - container.Do((PointerWrap&) packet); - ENetUtil::SendPacket(peer, std::move(container)); - pi.m_SentPackets++; -} - -void ENetHostClient::MaybeProcessPacketQueue() -{ - if (m_SendTimer.GetTimeDifference() > 6) - { - ProcessPacketQueue(); - m_SendTimer.Update(); - } -} - -void ENetHostClient::ProcessPacketQueue() -{ - // The idea is that we send packets n-1 times unreliably and n times - // reliably. - bool needReliable = false; - int numToRemove = 0; - size_t totalSize = 0; - for (auto it = m_OutgoingPacketInfo.rbegin(); it != m_OutgoingPacketInfo.rend(); ++it) - { - OutgoingPacketInfo& info = *it; - totalSize += info.m_Packet.vec->size(); - if (++info.m_NumSends == MaxPacketSends || totalSize >= MaxShortPacketLength) - { - if (!info.m_DidSendReliably) - needReliable = true; - numToRemove++; - } - } - // this can occasionally cause packets to be sent unnecessarily - // reliably - for (ENetPeer* peer = m_Host->peers, * end = &m_Host->peers[m_Host->peerCount]; peer != end; peer++) - { - if (peer->state != ENET_PEER_STATE_CONNECTED) - continue; - Packet p; - auto& pi = m_PeerInfo[peer - m_Host->peers]; - for (OutgoingPacketInfo& info : m_OutgoingPacketInfo) - { - if (info.m_Except == peer) - continue; - if (pi.m_ConnectTicker > info.m_Ticker) - continue; - p.W(pi.m_GlobalSeqToSeq[info.m_GlobalSequenceNumber]); - p.Do((PointerWrap&) info.m_Packet); - info.m_DidSendReliably = info.m_DidSendReliably || needReliable; - } - if (p.vec->size()) - { - ENetUtil::SendPacket(peer, std::move(p), needReliable); - pi.m_SentPackets++; - } - } - while (numToRemove--) - m_OutgoingPacketInfo.pop_front(); -} - -void ENetHostClient::PrintStats() -{ -#if 1 - if (m_StatsTimer.GetTimeDifference() > 5000) - { - m_StatsTimer.Update(); - for (ENetPeer* peer = m_Host->peers, * end = &m_Host->peers[m_Host->peerCount]; peer != end; peer++) - { - if (peer->state != ENET_PEER_STATE_CONNECTED) - continue; - auto& pi = m_PeerInfo[peer - m_Host->peers]; - char ip[64] = "?"; - enet_address_get_host_ip(&peer->address, ip, sizeof(ip)); - WARN_LOG(NETPLAY, "%speer %u (%s): %f%%+-%f%% packet loss, %u+-%u ms round trip time, %f%% throttle, %u/%u outgoing, %u/%u incoming, %d packets sent since last time\n", - !m_isTraversalClient ? "(CLIENT) " : "", // ew - (unsigned) peer->incomingPeerID, - ip, - peer->packetLoss / (float) ENET_PEER_PACKET_LOSS_SCALE, - peer->packetLossVariance / (float) ENET_PEER_PACKET_LOSS_SCALE, - peer->roundTripTime, - peer->roundTripTimeVariance, - peer->packetThrottle / (float) ENET_PEER_PACKET_THROTTLE_SCALE, - (unsigned) enet_list_size(&peer->outgoingReliableCommands), - (unsigned) enet_list_size(&peer->outgoingUnreliableCommands), - peer->channels != NULL ? (unsigned) enet_list_size(&peer->channels->incomingReliableCommands) : 0, - peer->channels != NULL ? (unsigned) enet_list_size(&peer->channels->incomingUnreliableCommands) : 0, - pi.m_SentPackets); - pi.m_SentPackets = 0; - } - } -#endif -} - -void ENetHostClient::ThreadFunc() -{ - ASSUME_ON(NET); - Common::SetCurrentThreadName(m_isTraversalClient ? "TraversalClient thread" : "ENetHostClient thread"); - while (1) - { - while (!m_RunQueue.Empty()) - { - m_RunQueue.Front()(); - m_RunQueue.Pop(); - } - if (m_ShouldEndThread) break; - ENetEvent event; - ENetAddress address; - if (enet_socket_get_address(m_Host->socket, &address) == -1) - { - PanicAlert("enet_socket_get_address failed."); - continue; - } - int count = enet_host_service(m_Host, &event, m_Host->connectedPeers > 0 ? 5 : 300); - if (count < 0) - { - PanicAlert("enet_host_service failed... do something about this."); - continue; - } - - HandleResends(); - - PrintStats(); + Reset(); - // Even if there was nothing, forward it as a wakeup. - if (m_Client) - { - switch (event.type) - { - case ENET_EVENT_TYPE_RECEIVE: - OnReceive(&event, ENetUtil::MakePacket(event.packet)); - break; - case ENET_EVENT_TYPE_CONNECT: - { - size_t pid = event.peer - m_Host->peers; - if (pid >= m_PeerInfo.size()) - m_PeerInfo.resize(pid + 1); - auto& pi = m_PeerInfo[pid]; - pi.m_IncomingPackets.clear(); - pi.m_IncomingSequenceNumber = 0; - pi.m_OutgoingSequenceNumber = 0; - pi.m_ConnectTicker = m_GlobalTicker++; - pi.m_SentPackets = 0; - } - /* fall through */ - default: - m_Client->OnENetEvent(&event); - } - } - MaybeProcessPacketQueue(); - } + ReconnectToServer(); } -void ENetHostClient::OnReceive(ENetEvent* event, Packet&& packet) +TraversalClient::~TraversalClient() { - auto& pi = m_PeerInfo[event->peer - m_Host->peers]; -#if 0 - printf("OnReceive isn=%x\n", pi.m_IncomingSequenceNumber); - DumpBuf(*packet.vec); -#endif - auto& incomingPackets = pi.m_IncomingPackets; - u16 seq; - - while (packet.vec->size() > packet.readOff) - { - { - packet.Do(seq); - if (packet.failure) - goto failure; - - s16 diff = (s16) (seq - pi.m_IncomingSequenceNumber); - if (diff < 0) - { - // assume a duplicate of something we already have - goto skip; - } - - while (incomingPackets.size() <= (size_t) diff) - incomingPackets.push_back(PWBuffer()); - - PWBuffer& buf = incomingPackets[diff]; - if (!buf.empty()) - { - // another type of duplicate - goto skip; - } - - packet.Do(buf); - if (packet.failure) - goto failure; - continue; - } - - skip: - { - u32 size; - packet.Do(size); - if (packet.vec->size() - packet.readOff < size) - goto failure; - packet.readOff += size; - continue; - } - - failure: - { - // strange - WARN_LOG(NETPLAY, "Failure splitting packet - truncation?"); - } - } - - while (!incomingPackets.empty() && !incomingPackets[0].empty()) + if (m_NetHost) { - m_Client->OnData(event, std::move(incomingPackets.front())); - incomingPackets.pop_front(); - pi.m_IncomingSequenceNumber++; + Common::Event done; + m_NetHost->RunOnThread([&]() { + ASSUME_ON(NET); + m_NetHost->m_TraversalClient = NULL; + done.Set(); + }); + done.Wait(); } } -TraversalClient::TraversalClient(const std::string& server, u16 port) -: ENetHostClient(MAX_CLIENTS + 16, port, true), // leave some spaces free for server full notification -m_Server(server) -{ - m_State = InitFailure; - - if (!m_Host) - return; - - Reset(); - - m_Host->intercept = TraversalClient::InterceptCallback; - m_Host->compressor.destroy = (decltype(m_Host->compressor.destroy)) this; - - ReconnectToServer(); -} - void TraversalClient::ReconnectToServer() { m_Server = "vps.qoid.us"; // XXX @@ -434,7 +52,7 @@ void TraversalClient::ReconnectToServer() TraversalPacket hello = {0}; hello.type = TraversalPacketHelloFromClient; hello.helloFromClient.protoVersion = TraversalProtoVersion; - RunOnThread([=]() { + m_NetHost->RunOnThread([=]() { ASSUME_ON(NET); SendTraversalPacket(hello); if (m_Client) @@ -442,11 +60,6 @@ void TraversalClient::ReconnectToServer() }); } -u16 TraversalClient::GetPort() -{ - return m_Host->address.port; -} - static ENetAddress MakeENetAddress(TraversalInetAddress* address) { ENetAddress eaddr; @@ -476,26 +89,22 @@ void TraversalClient::ConnectToClient(const std::string& host) m_PendingConnect = true; } -int ENET_CALLBACK TraversalClient::InterceptCallback(ENetHost* host, ENetEvent* event) +bool TraversalClient::TestPacket(u8* data, size_t size, ENetAddress* from) { - ASSUME_ON(NET); - const ENetAddress* addr = &host->receivedAddress; - auto self = (TraversalClient*) host->compressor.destroy; - if (addr->host == self->m_ServerAddress.host && - addr->port == self->m_ServerAddress.port) + if (from->host == m_ServerAddress.host && + from->port == m_ServerAddress.port) { - if (host->receivedDataLength < sizeof(TraversalPacket)) + if (size < sizeof(TraversalPacket)) { ERROR_LOG(NETPLAY, "Received too-short traversal packet."); } else { - self->HandleServerPacket((TraversalPacket*) host->receivedData); - event->type = (ENetEventType) 42; - return 1; + HandleServerPacket((TraversalPacket*) data); + return true; } } - return ENetUtil::InterceptCallback(host, event); + return false; } void TraversalClient::HandleServerPacket(TraversalPacket* packet) @@ -541,7 +150,7 @@ void TraversalClient::HandleServerPacket(TraversalPacket* packet) ENetBuffer buf; buf.data = message; buf.dataLength = sizeof(message) - 1; - enet_socket_send(m_Host->socket, &addr, &buf, 1); + enet_socket_send(m_NetHost->m_Host->socket, &addr, &buf, 1); } else @@ -584,7 +193,7 @@ void TraversalClient::HandleServerPacket(TraversalPacket* packet) ENetBuffer buf; buf.data = &ack; buf.dataLength = sizeof(ack); - if (enet_socket_send(m_Host->socket, &m_ServerAddress, &buf, 1) == -1) + if (enet_socket_send(m_NetHost->m_Host->socket, &m_ServerAddress, &buf, 1) == -1) OnFailure(SocketSendError); } } @@ -604,7 +213,7 @@ void TraversalClient::ResendPacket(OutgoingTraversalPacketInfo* info) ENetBuffer buf; buf.data = &info->packet; buf.dataLength = sizeof(info->packet); - if (enet_socket_send(m_Host->socket, &m_ServerAddress, &buf, 1) == -1) + if (enet_socket_send(m_NetHost->m_Host->socket, &m_ServerAddress, &buf, 1) == -1) OnFailure(SocketSendError); } @@ -656,35 +265,58 @@ TraversalRequestId TraversalClient::SendTraversalPacket(const TraversalPacket& p void TraversalClient::Reset() { - ENetHostClient::Reset(); - m_PendingConnect = false; } std::unique_ptr g_TraversalClient; +std::unique_ptr g_MainNetHost; + +// The settings at the previous TraversalClient reset - notably, we +// need to know not just what port it's on, but whether it was +// explicitly requested. static std::string g_OldServer; static u16 g_OldPort; -void EnsureTraversalClient(const std::string& server, u16 port) +bool EnsureTraversalClient(const std::string& server, u16 port) { if (!g_TraversalClient || server != g_OldServer || port != g_OldPort) { g_OldServer = server; g_OldPort = port; - g_TraversalClient.reset(new TraversalClient(g_OldServer, g_OldPort)); + + g_MainNetHost.reset(new NetHost(NetHost::DefaultPeerCount, port)); + if (!g_MainNetHost->m_Host) + { + g_MainNetHost.reset(); + return false; + } + + g_TraversalClient.reset(new TraversalClient(g_MainNetHost.get(), server)); if (g_TraversalClient->m_State == TraversalClient::InitFailure) { g_TraversalClient.reset(); + g_MainNetHost.reset(); + return false; } } + return true; } void ReleaseTraversalClient() { if (!g_TraversalClient) return; + if (g_OldPort != 0) + { + // If we were listening at a specific port, kill the + // TraversalClient to avoid hanging on to the port. g_TraversalClient.reset(); + g_MainNetHost.reset(); + } else + { + // Reset any pending connection attempts. g_TraversalClient->Reset(); + } } diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index ce299767c645..a3b6a625ea3f 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -4,134 +4,21 @@ #include "Common.h" #include "Thread.h" -#include "FifoQueue.h" #include "TraversalProto.h" -#include "enet/enet.h" #include #include #include -#include "Timer.h" -#include "ChunkFile.h" - -DEFINE_THREAD_HAT(NET); - -#define MAX_CLIENTS 200 - -static inline void DumpBuf(PWBuffer& buf) -{ - printf("+00:"); - int c = 0; - for (size_t i = 0; i < buf.size(); i++) - { - printf(" %02x", buf.data()[i]); - if (++c % 16 == 0) - printf("\n+%02x:", c); - } - printf("\n"); -} - -namespace ENetUtil -{ - ENetPacket* MakeENetPacket(Packet&& pac, enet_uint32 flags); - void SendPacket(ENetPeer* peer, Packet&& pac, bool reliable = true) ON(NET); - Packet MakePacket(ENetPacket* epacket); - void Wakeup(ENetHost* host); - int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) /* ON(NET) */; -} - -// Apparently nobody on the C++11 standards committee thought of -// combining two of its most prominent features: lambdas and moves. Nor -// does bind work, despite a StackOverflow answer to the contrary. Derp. -template -class CopyAsMove -{ -public: - CopyAsMove(T&& t) : m_T(std::move(t)) {} - CopyAsMove(const CopyAsMove& other) : m_T((T&&) other.m_T) {} - T& operator*() { return m_T; } -private: - T m_T; -}; +#include "NetHost.h" class TraversalClientClient { public: - virtual void OnENetEvent(ENetEvent* event) ON(NET) = 0; - virtual void OnData(ENetEvent* event, Packet&& packet) ON(NET) = 0; virtual void OnTraversalStateChanged() ON(NET) = 0; virtual void OnConnectReady(ENetAddress addr) ON(NET) = 0; virtual void OnConnectFailed(u8 reason) ON(NET) = 0; }; -class ENetHostClient -{ -public: - enum - { - MaxPacketSends = 4, - MaxShortPacketLength = 128 - }; - - ENetHostClient(size_t peerCount, u16 port, bool isTraversalClient = false); - ~ENetHostClient(); - void RunOnThread(std::function func) NOT_ON(NET); - void CreateThread(); - void Reset(); - - void BroadcastPacket(Packet&& packet, ENetPeer* except = NULL) ON(NET); - void SendPacket(ENetPeer* peer, Packet&& packet) ON(NET); - void MaybeProcessPacketQueue() ON(NET); - void ProcessPacketQueue() ON(NET); - void PrintStats() ON(NET); - - TraversalClientClient* m_Client; - ENetHost* m_Host; -protected: - virtual void HandleResends() ON(NET) {} -private: - struct OutgoingPacketInfo - { - OutgoingPacketInfo(Packet&& packet, ENetPeer* except, u16 seq, u64 ticker) - : m_Packet(std::move(packet)), m_Except(except), m_DidSendReliably(false), m_NumSends(0), m_GlobalSequenceNumber(seq), m_Ticker(ticker) {} - - Packet m_Packet; - ENetPeer* m_Except; - bool m_DidSendReliably; - int m_NumSends; - u16 m_GlobalSequenceNumber; - u64 m_Ticker; - }; - - struct PeerInfo - { - std::deque m_IncomingPackets; - // the sequence number of the first element of m_IncomingPackets - u16 m_IncomingSequenceNumber; - u16 m_OutgoingSequenceNumber; - u16 m_GlobalSeqToSeq[65536]; - u64 m_ConnectTicker; - int m_SentPackets; - }; - - void ThreadFunc() /* ON(NET) */; - void OnReceive(ENetEvent* event, Packet&& packet) ON(NET); - - Common::FifoQueue, false> m_RunQueue; - std::mutex m_RunQueueWriteLock; - std::thread m_Thread; - Common::Event m_ResetEvent; - bool m_ShouldEndThread ACCESS_ON(NET); - bool m_isTraversalClient; - - std::deque m_OutgoingPacketInfo ACCESS_ON(NET); - Common::Timer m_SendTimer ACCESS_ON(NET); - Common::Timer m_StatsTimer ACCESS_ON(NET); - std::vector m_PeerInfo ACCESS_ON(NET); - u16 m_GlobalSequenceNumber ACCESS_ON(NET); - u64 m_GlobalTicker ACCESS_ON(NET); -}; - -class TraversalClient : public ENetHostClient +class TraversalClient { public: enum State @@ -151,17 +38,22 @@ class TraversalClient : public ENetHostClient ConnectFailedError = 0x400, }; - TraversalClient(const std::string& server, u16 port); + TraversalClient(NetHost* netHost, const std::string& server); + ~TraversalClient(); void Reset(); void ConnectToClient(const std::string& host) ON(NET); void ReconnectToServer(); - u16 GetPort(); + // called from NetHost + bool TestPacket(u8* data, size_t size, ENetAddress* from) ON(NET); + void HandleResends() ON(NET); + + NetHost* m_NetHost; + TraversalClientClient* m_Client; TraversalHostId m_HostId; State m_State; int m_FailureReason; -protected: - virtual void HandleResends() ON(NET); + private: struct OutgoingTraversalPacketInfo { @@ -174,17 +66,20 @@ class TraversalClient : public ENetHostClient void ResendPacket(OutgoingTraversalPacketInfo* info) ON(NET); TraversalRequestId SendTraversalPacket(const TraversalPacket& packet) ON(NET); void OnFailure(int reason) ON(NET); - static int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) /* ON(NET) */; void HandlePing() ON(NET); TraversalRequestId m_ConnectRequestId; bool m_PendingConnect; std::list m_OutgoingTraversalPackets ACCESS_ON(NET); ENetAddress m_ServerAddress; - enet_uint32 m_PingTime; std::string m_Server; + enet_uint32 m_PingTime; }; extern std::unique_ptr g_TraversalClient; -void EnsureTraversalClient(const std::string& server, u16 port); +// the NetHost connected to the TraversalClient. +extern std::unique_ptr g_MainNetHost; + +// Create g_TraversalClient and g_MainNetHost if necessary. +bool EnsureTraversalClient(const std::string& server, u16 port); void ReleaseTraversalClient(); diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 4db66a882ae5..29e8d8d97225 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -30,6 +30,9 @@ NetPlayClient::~NetPlayClient() if (m_is_running) StopGame(); + if (m_net_host) + m_net_host->Reset(); + if (!m_direct_connection) ReleaseTraversalClient(); } @@ -40,7 +43,7 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam m_delay = 20; m_state = Failure; m_dialog = NULL; - m_host = NULL; + m_net_host = NULL; m_state_callback = stateCallback; m_local_name = name; m_backend = NULL; @@ -50,13 +53,11 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam { // Direct or local connection. Don't use TraversalClient. m_direct_connection = true; - m_host_client_store.reset(new ENetHostClient(/*peerCount=*/1, /*port=*/0)); - m_host_client = m_host_client_store.get(); - if (!m_host_client->m_Host) + m_net_host_store.reset(new NetHost(/*peerCount=*/1, /*port=*/0)); + m_net_host = m_net_host_store.get(); + if (!m_net_host->m_Host) return; - m_host_client->m_Client = this; - - m_host = m_host_client->m_Host; + m_net_host->m_Client = this; std::string host = hostSpec.substr(0, pos); int port = atoi(hostSpec.substr(pos + 1).c_str()); @@ -69,16 +70,16 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam else { m_direct_connection = false; - EnsureTraversalClient(SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayCentralServer, 0); - if (!g_TraversalClient) + if (!EnsureTraversalClient(SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayCentralServer, 0)) return; - m_host_client = g_TraversalClient.get(); + m_net_host = g_MainNetHost.get(); + m_net_host->m_Client = this; + m_traversal_client = g_TraversalClient.get(); // If we were disconnected in the background, reconnect. - if (g_TraversalClient->m_State == TraversalClient::Failure) - g_TraversalClient->ReconnectToServer(); - g_TraversalClient->m_Client = this; + if (m_traversal_client->m_State == TraversalClient::Failure) + m_traversal_client->ReconnectToServer(); + m_traversal_client->m_Client = this; m_host_spec = hostSpec; - m_host = g_TraversalClient->m_Host; m_state = WaitingForTraversalClientConnection; OnTraversalStateChanged(); } @@ -87,7 +88,7 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam void NetPlayClient::DoDirectConnect(const ENetAddress& addr) { m_state = Connecting; - enet_host_connect(m_host, &addr, /*channelCount=*/0, /*data=*/0); + enet_host_connect(m_net_host->m_Host, &addr, /*channelCount=*/0, /*data=*/0); } void NetPlayClient::SetDialog(NetPlayUI* dialog) @@ -99,15 +100,15 @@ void NetPlayClient::SetDialog(NetPlayUI* dialog) void NetPlayClient::SendPacket(Packet&& packet) { CopyAsMove tmp(std::move(packet)); - g_TraversalClient->RunOnThread([=]() mutable { + m_net_host->RunOnThread([=]() mutable { ASSUME_ON(NET); - m_host_client->BroadcastPacket(std::move(*tmp), NULL); + m_net_host->BroadcastPacket(std::move(*tmp), NULL); }); } void NetPlayClient::OnPacketErrorFromIOSync() { - g_TraversalClient->RunOnThread([=]() { + m_net_host->RunOnThread([=]() { ASSUME_ON(NET); OnDisconnect(InvalidPacket); }); @@ -312,7 +313,7 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) pong.W(ping_key); std::lock_guard lk(m_crit); - m_host_client->BroadcastPacket(std::move(pong)); + m_net_host->BroadcastPacket(std::move(pong)); } break; @@ -373,7 +374,7 @@ void NetPlayClient::OnENetEvent(ENetEvent* event) hello.W(std::string(NETPLAY_VERSION)); hello.W(std::string(netplay_dolphin_ver)); hello.W(m_local_name); - m_host_client->BroadcastPacket(std::move(hello)); + m_net_host->BroadcastPacket(std::move(hello)); m_state = WaitingForHelloResponse; if (m_state_callback) m_state_callback(this); @@ -390,17 +391,17 @@ void NetPlayClient::OnENetEvent(ENetEvent* event) void NetPlayClient::OnTraversalStateChanged() { if (m_state == WaitingForTraversalClientConnection && - g_TraversalClient->m_State == TraversalClient::Connected) + m_traversal_client->m_State == TraversalClient::Connected) { m_state = WaitingForTraversalClientConnectReady; if (m_state_callback) m_state_callback(this); - g_TraversalClient->ConnectToClient(m_host_spec); + m_traversal_client->ConnectToClient(m_host_spec); } else if (m_state != Failure && - g_TraversalClient->m_State == TraversalClient::Failure) + m_traversal_client->m_State == TraversalClient::Failure) { - OnDisconnect(g_TraversalClient->m_FailureReason); + OnDisconnect(m_traversal_client->m_FailureReason); } } @@ -481,7 +482,7 @@ void NetPlayClient::SendChatMessage(const std::string& msg) void NetPlayClient::ChangeName(const std::string& name) { - g_TraversalClient->RunOnThread([=]() { + m_net_host->RunOnThread([=]() { ASSUME_ON(NET); std::lock_guard lk(m_crit); m_local_name = name; diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 4c375924ce5c..1fd02798a8e6 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -53,7 +53,7 @@ class Player u32 ping; }; -class NetPlayClient : public TraversalClientClient +class NetPlayClient : public NetHostClient, public TraversalClientClient { public: NetPlayClient(const std::string& hostSpec, const std::string& name, std::function stateCallback); @@ -107,7 +107,6 @@ class NetPlayClient : public TraversalClientClient std::recursive_mutex m_crit; NetPlayUI* m_dialog; - ENetHost* m_host; std::string m_host_spec; bool m_direct_connection; std::thread m_thread; @@ -132,8 +131,9 @@ class NetPlayClient : public TraversalClientClient void DoDirectConnect(const ENetAddress& addr); std::map m_players GUARDED_BY(m_crit); - std::unique_ptr m_host_client_store; - ENetHostClient* m_host_client; + std::unique_ptr m_net_host_store; + NetHost* m_net_host; + TraversalClient* m_traversal_client; Common::Event m_have_dialog_event; }; diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 86f9cf60f3da..daa25f0e5bb2 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -26,7 +26,9 @@ NetPlayServer::~NetPlayServer() if (m_prefs) CFRelease(m_prefs); #endif - // leave the host open for future use + // disconnect all + if (g_MainNetHost) + g_MainNetHost->Reset(); ReleaseTraversalClient(); } @@ -46,14 +48,16 @@ NetPlayServer::NetPlayServer() m_prefs = SCPreferencesCreate(NULL, CFSTR("NetPlayServer"), NULL); #endif - EnsureTraversalClient( + if (!EnsureTraversalClient( SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayCentralServer, - SConfig::GetInstance().m_LocalCoreStartupParameter.iNetPlayListenPort); - if (!g_TraversalClient) + SConfig::GetInstance().m_LocalCoreStartupParameter.iNetPlayListenPort)) return; g_TraversalClient->m_Client = this; - m_host = g_TraversalClient->m_Host; + g_MainNetHost->m_Client = this; + m_traversal_client = g_TraversalClient.get(); + m_net_host = g_MainNetHost.get(); + m_enet_host = m_net_host->m_Host; if (g_TraversalClient->m_State == TraversalClient::Failure) g_TraversalClient->ReconnectToServer(); @@ -79,7 +83,7 @@ void NetPlayServer::OnENetEvent(ENetEvent* event) if (m_ping_timer.GetTimeElapsed() > (10 * 1000)) UpdatePings(); - PlayerId pid = event->peer - m_host->peers; + PlayerId pid = event->peer - m_enet_host->peers; switch (event->type) { case ENET_EVENT_TYPE_DISCONNECT: @@ -181,7 +185,7 @@ std::unordered_set NetPlayServer::GetInterfaceSet() std::string NetPlayServer::GetInterfaceHost(std::string interface) { char buf[16]; - sprintf(buf, ":%d", g_TraversalClient->GetPort()); + sprintf(buf, ":%d", m_net_host->GetPort()); auto lst = GetInterfaceListInternal(); for (auto it = lst.begin(); it != lst.end(); ++it) { @@ -196,7 +200,7 @@ std::string NetPlayServer::GetInterfaceHost(std::string interface) MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) { Client& player = m_players[pid]; - ENetPeer* peer = &m_host->peers[pid]; + ENetPeer* peer = &m_enet_host->peers[pid]; std::string npver; hello.Do(npver); @@ -212,7 +216,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) return CON_ERR_GAME_RUNNING; // too many players - if (m_num_players >= MAX_CLIENTS) + if (m_num_players >= NetHost::DefaultPeerCount - 20) return CON_ERR_SERVER_FULL; // send join message to already connected clients @@ -230,7 +234,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) Packet opacket; opacket.W((MessageId)0); opacket.W(pid); - g_TraversalClient->SendPacket(peer, std::move(opacket)); + m_net_host->SendPacket(peer, std::move(opacket)); } UpdatePings(); @@ -243,7 +247,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) Packet opacket; opacket.W((MessageId)NP_MSG_CHANGE_GAME); opacket.W(m_selected_game); - g_TraversalClient->SendPacket(peer, std::move(opacket)); + m_net_host->SendPacket(peer, std::move(opacket)); } } @@ -252,7 +256,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) Packet opacket; opacket.W((MessageId)NP_MSG_PAD_BUFFER); opacket.W((u32)m_target_buffer_size); - g_TraversalClient->SendPacket(peer, std::move(opacket)); + m_net_host->SendPacket(peer, std::move(opacket)); } // send players @@ -266,7 +270,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) opacket.W((PlayerId)opid); opacket.W(oplayer.name); opacket.W(oplayer.revision); - g_TraversalClient->SendPacket(peer, std::move(opacket)); + m_net_host->SendPacket(peer, std::move(opacket)); } } @@ -320,8 +324,8 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) { /*printf("Server sees\n"); DumpBuf(*packet.vec);*/ - PlayerId pid = event->peer - m_host->peers; - ENetPeer* peer = &m_host->peers[pid]; + PlayerId pid = event->peer - m_enet_host->peers; + ENetPeer* peer = &m_enet_host->peers[pid]; if (pid >= m_players.size()) m_players.resize(pid + 1); Client& player = m_players[pid]; @@ -334,7 +338,7 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) Packet opacket; opacket.W(error); opacket.W((PlayerId)0); - g_TraversalClient->SendPacket(peer, std::move(opacket)); + m_net_host->SendPacket(peer, std::move(opacket)); enet_peer_disconnect_later(peer, 0); } else @@ -578,7 +582,7 @@ bool NetPlayServer::StartGame(const std::string &path) void NetPlayServer::SendToClients(Packet&& packet, const PlayerId skip_pid) { CopyAsMove tmp(std::move(packet)); - g_TraversalClient->RunOnThread([=]() mutable { + m_net_host->RunOnThread([=]() mutable { ASSUME_ON(NET); SendToClientsOnThread(std::move(*tmp), skip_pid); }); @@ -587,7 +591,7 @@ void NetPlayServer::SendToClients(Packet&& packet, const PlayerId skip_pid) void NetPlayServer::SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid) { - g_TraversalClient->BroadcastPacket(std::move(packet), skip_pid >= m_host->peerCount ? NULL : &m_host->peers[skip_pid]); + m_net_host->BroadcastPacket(std::move(packet), skip_pid >= m_enet_host->peerCount ? NULL : &m_enet_host->peers[skip_pid]); } void NetPlayServer::SetDialog(NetPlayUI* dialog) diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index e33351af8dda..5642a52f68c7 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -21,7 +21,7 @@ class NetPlayUI; -class NetPlayServer : public TraversalClientClient +class NetPlayServer : public NetHostClient, public TraversalClientClient { public: NetPlayServer(); @@ -45,6 +45,7 @@ class NetPlayServer : public TraversalClientClient std::unordered_set GetInterfaceSet(); std::string GetInterfaceHost(std::string interface); + private: class Client { @@ -94,7 +95,9 @@ class NetPlayServer : public TraversalClientClient std::string m_selected_game GUARDED_BY(m_crit); - ENetHost* m_host; + TraversalClient* m_traversal_client; + NetHost* m_net_host; + ENetHost* m_enet_host; NetPlayUI* m_dialog; #if defined(__APPLE__) diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index b181c68929c3..13caf362564f 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -753,7 +753,7 @@ void NetPlay::StartHosting(std::string id, wxWindow* parent) netplay_server.reset(new NetPlayServer()); - if (!g_TraversalClient) + if (!g_TraversalClient || !g_MainNetHost) { netplay_server.reset(); wxString error; @@ -774,7 +774,7 @@ void NetPlay::StartHosting(std::string id, wxWindow* parent) std::string nickname = SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayNickname; Common::Event ev; char buf[64]; - sprintf(buf, "127.0.0.1:%d", g_TraversalClient->GetPort()); + sprintf(buf, "127.0.0.1:%d", g_MainNetHost->GetPort()); netplay_client.reset(new NetPlayClient(buf, nickname, [&](NetPlayClient* npc) { if (npc->m_state == NetPlayClient::Connected || npc->m_state == NetPlayClient::Failure) From 3cee3977b5adde4fd28596dd7590cc0195504ffd Mon Sep 17 00:00:00 2001 From: comex Date: Wed, 20 Nov 2013 13:48:02 -0500 Subject: [PATCH 086/202] Fix lifetime/game stopping issues. --- Source/Core/Common/Src/TraversalClient.cpp | 5 +- Source/Core/Core/Src/IOSyncBackends.cpp | 8 +- Source/Core/Core/Src/IOSyncBackends.h | 3 + Source/Core/Core/Src/NetPlayClient.cpp | 113 ++++++++------------- Source/Core/Core/Src/NetPlayClient.h | 10 +- Source/Core/Core/Src/NetPlayProto.h | 1 - Source/Core/Core/Src/NetPlayServer.cpp | 26 ++++- Source/Core/Core/Src/NetPlayServer.h | 18 ++-- Source/Core/DolphinWX/Src/FrameTools.cpp | 3 +- Source/Core/DolphinWX/Src/NetWindow.cpp | 85 ++++++++++------ Source/Core/DolphinWX/Src/NetWindow.h | 22 ++-- 11 files changed, 163 insertions(+), 131 deletions(-) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index 17abfd6cda94..bde7440d98ad 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -30,13 +30,10 @@ TraversalClient::~TraversalClient() { if (m_NetHost) { - Common::Event done; - m_NetHost->RunOnThread([&]() { + m_NetHost->RunOnThreadSync([&]() { ASSUME_ON(NET); m_NetHost->m_TraversalClient = NULL; - done.Set(); }); - done.Wait(); } } diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index bd06acec4ab1..ee1c771b4aca 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -68,6 +68,7 @@ BackendNetPlay::BackendNetPlay(NetPlayClient* client, u32 delay) m_Client = client; m_SubframeId = -1; m_Delay = delay; + m_Abort = false; NewLocalSubframe(); } @@ -124,7 +125,7 @@ Packet BackendNetPlay::DequeueReport(int classId, int index, bool* keepGoing) while (1) { //printf("dev=%llu past=%llu\n", deviceInfo.m_SubframeId, m_PastSubframeId); - if (!isConnected || deviceInfo.m_SubframeId > m_PastSubframeId) + if (!isConnected || m_Abort || deviceInfo.m_SubframeId > m_PastSubframeId) { *keepGoing = false; return PWBuffer(); @@ -174,6 +175,11 @@ void BackendNetPlay::OnPacketReceived(Packet&& packet) m_PacketsPendingProcessing.Push(std::move(packet)); } +void BackendNetPlay::Abort() +{ + m_Abort = true; +} + void BackendNetPlay::ProcessIncomingPackets() { Packet p; diff --git a/Source/Core/Core/Src/IOSyncBackends.h b/Source/Core/Core/Src/IOSyncBackends.h index d0bff8f46b9f..54c90a0282b7 100644 --- a/Source/Core/Core/Src/IOSyncBackends.h +++ b/Source/Core/Core/Src/IOSyncBackends.h @@ -42,6 +42,7 @@ class BackendNetPlay : public Backend // from netplay void OnPacketReceived(Packet&& packet) ON(NET); + void Abort() ON(NET); // from (arbitrarily-ish) SI virtual void NewLocalSubframe() override; private: @@ -70,6 +71,8 @@ class BackendNetPlay : public Backend u32 m_Delay; // indexed by remote device DeviceInfo m_DeviceInfo[Class::NumClasses][Class::MaxDeviceIndex]; + // Need to quit because of a NP_MSG_STOP_GAME? + volatile bool m_Abort; }; } diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 29e8d8d97225..7341f9f19a36 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -19,16 +19,14 @@ #include "ConfigManager.h" #include "HW/WiimoteEmu/WiimoteEmu.h" -std::mutex crit_netplay_client; -static NetPlayClient * netplay_client = NULL; NetSettings g_NetPlaySettings; +static volatile bool g_is_running; #define RPT_SIZE_HACK (1 << 16) NetPlayClient::~NetPlayClient() { - // not perfect if (m_is_running) - StopGame(); + PanicAlert("NetPlayClient destroyed while still running"); if (m_net_host) m_net_host->Reset(); @@ -47,6 +45,7 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam m_state_callback = stateCallback; m_local_name = name; m_backend = NULL; + m_received_stop_request = false; size_t pos = hostSpec.find(':'); if (pos != std::string::npos) @@ -237,17 +236,14 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) packet.Do(m_delay); if (packet.failure) return OnDisconnect(InvalidPacket); - if (!m_backend) - break; /* fall through */ } - case NP_MSG_DISCONNECT_DEVICE: case NP_MSG_CONNECT_DEVICE: case NP_MSG_REPORT: { if (!m_backend) - return OnDisconnect(InvalidPacket); + break; packet.readOff = oldOff; m_backend->OnPacketReceived(std::move(packet)); break; @@ -264,6 +260,9 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) case NP_MSG_START_GAME : { + if (m_is_running) + return OnDisconnect(InvalidPacket); + { std::lock_guard lk(m_crit); packet.Do(m_current_game); @@ -280,25 +279,20 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) return OnDisconnect(InvalidPacket); } + m_received_stop_request = false; m_dialog->OnMsgStartGame(); } break; case NP_MSG_STOP_GAME : { + m_received_stop_request = true; + if (m_backend) + m_backend->Abort(); m_dialog->OnMsgStopGame(); } break; - case NP_MSG_DISABLE_GAME : - { - PanicAlertT("Other client disconnected while game is running!! NetPlay is disabled. You manually stop the game."); - std::lock_guard lk(m_crit); - m_is_running = false; - NetPlay_Disable(); - } - break; - case NP_MSG_PING : { u32 ping_key = 0; @@ -348,13 +342,15 @@ void NetPlayClient::OnDisconnect(int reason) std::lock_guard lk(m_crit); if (m_state == Connected) { - NetPlay_Disable(); m_dialog->AppendChat("< LOST CONNECTION TO SERVER >"); - PanicAlertT("Lost connection to server."); m_players.clear(); m_pid = -1; } - m_is_running = false; + if (m_is_running) + { + m_received_stop_request = true; + m_dialog->OnMsgStopGame(); + } m_state = Failure; m_failure_reason = reason; if (m_state_callback) @@ -504,7 +500,6 @@ bool NetPlayClient::StartGame(const std::string &path) return false; } - m_is_running = true; // tell server i started the game Packet packet; @@ -518,14 +513,19 @@ bool NetPlayClient::StartGame(const std::string &path) m_dialog->AppendChat(" -- STARTING GAME -- "); - NetPlay_Enable(this); - IOSync::g_Backend.reset(m_backend = new IOSync::BackendNetPlay(this, m_delay)); - // boot game m_dialog->BootGame(path); + if (Core::IsRunningAndStarted()) + { + g_is_running = true; + m_is_running = true; + } + + m_game_started_evt.Set(); + return true; } @@ -534,61 +534,32 @@ bool NetPlayClient::ChangeGame(const std::string&) return true; } -bool NetPlayClient::StopGame() +void NetPlayClient::GameStopped() { - // XXX - this is weird std::lock_guard lk(m_crit); - if (!m_is_running) - { - PanicAlertT("Game isn't running!"); - return false; - } - - // reset the IOSync backend - IOSync::ResetBackend(); - m_backend = NULL; - - m_dialog->AppendChat(" -- STOPPING GAME -- "); - + m_net_host->RunOnThreadSync([=]() { + IOSync::ResetBackend(); + m_backend = NULL; + }); + g_is_running = false; m_is_running = false; - NetPlay_Disable(); - // stop game - m_dialog->StopGame(); - - return true; -} - -/* -static bool Has - for (int c = 0; c < IOSync::ClassBase::NumClasses; c++) - for (int i = 0; i < IOSync::ClassBase::MaxDeviceIndex; i++) -*/ + m_dialog->AppendChat(" -- STOPPING GAME -- "); + m_dialog->GameStopped(); -void NetPlayClient::Stop() -{ - if (m_is_running == false) - return; - abort(); - // if we have a pad, then tell the server to stop (need a dialog about - // this); else just quit netplay + // This might have happened as a result of an incoming NP_MSG_STOP_GAME, in + // which case we should avoid making another stop request. Note that this + // stop request might be ignored if we're a spectator. + if (!m_received_stop_request) + { + Packet packet; + packet.W((MessageId)NP_MSG_STOP_GAME); + SendPacket(std::move(packet)); + } } - bool NetPlay::IsNetPlayRunning() { - return netplay_client != NULL; -} - -void NetPlay_Enable(NetPlayClient* const np) -{ - std::lock_guard lk(crit_netplay_client); - netplay_client = np; -} - -void NetPlay_Disable() -{ - std::lock_guard lk(crit_netplay_client); - netplay_client = NULL; + return g_is_running; } diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 1fd02798a8e6..223cf9f958b4 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -33,9 +33,9 @@ class NetPlayUI virtual ~NetPlayUI() {}; virtual void BootGame(const std::string& filename) = 0; - virtual void StopGame() = 0; virtual void Update() = 0; + virtual void GameStopped() = 0; virtual void AppendChat(const std::string& msg) = 0; virtual void OnMsgChangeGame(const std::string& filename) = 0; @@ -82,8 +82,7 @@ class NetPlayClient : public NetHostClient, public TraversalClientClient int m_failure_reason; bool StartGame(const std::string &path) /* ON(GUI) */; - bool StopGame() /* multiple threads */; - void Stop() /* ON(GUI) */; + void GameStopped() /* ON(GUI) */; bool ChangeGame(const std::string& game) /* ON(GUI) */; void SendChatMessage(const std::string& msg) /* ON(GUI) */; void ChangeName(const std::string& name) /* ON(GUI) */; @@ -123,6 +122,8 @@ class NetPlayClient : public NetHostClient, public TraversalClientClient IOSync::BackendNetPlay* m_backend; u32 m_current_game; + bool m_received_stop_request; + Common::Event m_game_started_evt; bool m_is_recording; @@ -137,7 +138,4 @@ class NetPlayClient : public NetHostClient, public TraversalClientClient Common::Event m_have_dialog_event; }; -void NetPlay_Enable(NetPlayClient* const np); -void NetPlay_Disable(); - #endif diff --git a/Source/Core/Core/Src/NetPlayProto.h b/Source/Core/Core/Src/NetPlayProto.h index 44a4e20afa3e..c9e2a1d21ae9 100644 --- a/Source/Core/Core/Src/NetPlayProto.h +++ b/Source/Core/Core/Src/NetPlayProto.h @@ -48,7 +48,6 @@ enum NP_MSG_START_GAME = 0xA0, NP_MSG_CHANGE_GAME = 0xA1, NP_MSG_STOP_GAME = 0xA2, - NP_MSG_DISABLE_GAME = 0xA3, NP_MSG_READY = 0xD0, NP_MSG_NOT_READY = 0xD1, diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index daa25f0e5bb2..78e276849e39 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -77,6 +77,20 @@ void NetPlayServer::UpdatePings() m_update_pings = false; } +// Does this "player" have no input devices assigned? +bool NetPlayServer::IsSpectator(PlayerId pid) +{ + for (int c = 0; c < IOSync::Class::NumClasses; c++) + { + for (int i = 0; i < IOSync::Class::MaxDeviceIndex; i++) + { + if (m_device_map[c][i].first == pid) + return false; + } + } + return true; +} + void NetPlayServer::OnENetEvent(ENetEvent* event) { // update pings every so many seconds @@ -515,6 +529,16 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) case NP_MSG_STOP_GAME: { + if (!m_is_running) + break; + if (IsSpectator(pid)) + { + // If they're a spectator, the game can keep going even after + // they stopped, but we need to note that they no longer can be + // assigned to controllers. + //UpdateDevicesPresent(); + break; + } // tell clients to stop game Packet opacket; opacket.W((MessageId)NP_MSG_STOP_GAME); @@ -525,7 +549,7 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) } break; - default : + default: PanicAlertT("Unknown message with id:%d received from player:%d Kicking player!", mid, pid); // unknown message, kick the client return OnDisconnect(pid); diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 5642a52f68c7..4387fd672dd2 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -57,6 +57,8 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient u32 ping; u32 current_game; bool connected; + bool in_game; + //bool m_devices_present[IOSync::Class::NumClasses][IOSync::Class::MaxDeviceIndex]; //bool is_localhost; }; @@ -65,6 +67,8 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient MessageId OnConnect(PlayerId pid, Packet& hello) ON(NET); void OnDisconnect(PlayerId pid) ON(NET); void UpdatePings() ON(NET); + bool IsSpectator(PlayerId pid); + std::vector> GetInterfaceListInternal(); NetSettings m_settings; @@ -76,17 +80,15 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient u32 m_current_game; u32 m_target_buffer_size; - // Note about disconnects: Imagine a single player plus - // spectators. The client should not have to wait for the - // server for each frame. However, if the server decides to - // change the mapping, the client must not desync. - // Therefore, in lieu of more complicated solutions, - // disconnects that will be a surprise for the disconnected - // user (i.e. not a disconnect request or the user - // disconnecting) must be scheduled for the far future. + // Note about disconnects: Imagine a single player plus spectators. The + // client should not have to wait for the server for each frame. However, + // if the server decides to change the mapping, the client must not desync. + // Therefore, in lieu of more complicated solutions, disconnects should be + // requested rather than forced. std::pair m_device_map[IOSync::Class::NumClasses][IOSync::Class::MaxDeviceIndex]; + std::vector m_players; unsigned m_num_players; diff --git a/Source/Core/DolphinWX/Src/FrameTools.cpp b/Source/Core/DolphinWX/Src/FrameTools.cpp index fb16cff22654..676c821b5360 100644 --- a/Source/Core/DolphinWX/Src/FrameTools.cpp +++ b/Source/Core/DolphinWX/Src/FrameTools.cpp @@ -1062,13 +1062,14 @@ void CFrame::DoStop() DoRecordingSave(); if(Movie::IsPlayingInput() || Movie::IsRecordingInput()) Movie::EndPlayInput(false); - NetPlay::StopGame(); wxBeginBusyCursor(); BootManager::Stop(); wxEndBusyCursor(); confirmStop = false; + NetPlay::GameStopped(); + #if defined(HAVE_X11) && HAVE_X11 if (SConfig::GetInstance().m_LocalCoreStartupParameter.bDisableScreenSaver) X11Utils::InhibitScreensaver(X11Utils::XDisplayFromHandle(GetHandle()), diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 13caf362564f..f7eb3fb86f49 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -12,6 +12,7 @@ #include "Frame.h" #include "Core.h" #include "ConfigManager.h" +#include "Host.h" #include #include @@ -21,7 +22,7 @@ #define NETPLAY_TITLEBAR "Dolphin NetPlay" #define INITIAL_PAD_BUFFER_SIZE 20 -static wxString FailureReasonStringForHost(int reason) +static wxString FailureReasonStringForHostLabel(int reason) { switch (reason) { @@ -38,7 +39,7 @@ static wxString FailureReasonStringForHost(int reason) } } -static wxString FailureReasonStringForConnect(int reason) +static wxString FailureReasonStringForDialog(int reason) { switch (reason) { @@ -201,6 +202,15 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const Center(); Show(); + + if (netplay_client) + { + netplay_client->m_state_callback = [=](NetPlayClient* npc) { OnStateChanged(); }; + netplay_client->SetDialog(this); + } + if (netplay_server) + netplay_server->SetDialog(this); + Update(); } const GameListItem* NetPlayDiag::FindISO(const std::string& id) @@ -238,6 +248,8 @@ void NetPlayDiag::UpdateGameName() NetPlayDiag::~NetPlayDiag() { + // We must be truly stopped before killing netplay_client. + main_frame->DoStop(); netplay_client.reset(); netplay_server.reset(); npd = NULL; @@ -279,9 +291,11 @@ void NetPlayDiag::BootGame(const std::string& filename) main_frame->BootGame(filename); } -void NetPlayDiag::StopGame() +void NetPlayDiag::GameStopped() { - main_frame->DoStop(); + if (m_start_btn) + m_start_btn->Enable(); + m_record_chkbox->Enable(); } // NetPlayUI methods called from ---NETPLAY--- thread @@ -308,20 +322,25 @@ void NetPlayDiag::OnMsgChangeGame(const std::string& filename) void NetPlayDiag::OnMsgStartGame() { + // This has to be synchronous because of following messages. + m_game_started_evt.Reset(); wxCommandEvent evt(wxEVT_THREAD, NP_GUI_EVT_START_GAME); GetEventHandler()->AddPendingEvent(evt); - if (m_start_btn) - m_start_btn->Disable(); - m_record_chkbox->Disable(); + m_game_started_evt.Wait(); } void NetPlayDiag::OnMsgStopGame() { - wxCommandEvent evt(wxEVT_THREAD, NP_GUI_EVT_STOP_GAME); - GetEventHandler()->AddPendingEvent(evt); - if (m_start_btn) - m_start_btn->Enable(); - m_record_chkbox->Enable(); + Host_Message(WM_USER_STOP); +} + +void NetPlayDiag::OnStateChanged() +{ + if (netplay_client->m_state == NetPlayClient::Failure) + { + wxCommandEvent evt(wxEVT_THREAD, NP_GUI_EVT_FAILURE); + GetEventHandler()->AddPendingEvent(evt); + } } void NetPlayDiag::OnAdjustBuffer(wxCommandEvent& event) @@ -370,7 +389,7 @@ void NetPlayDiag::UpdateHostLabel() break; case TraversalClient::Failure: m_host_label->SetForegroundColour(*wxBLACK); - m_host_label->SetLabel(FailureReasonStringForHost(g_TraversalClient->m_FailureReason)); + m_host_label->SetLabel(FailureReasonStringForHostLabel(g_TraversalClient->m_FailureReason)); m_host_copy_btn->SetLabel(_("Retry")); m_host_copy_btn->Enable(); m_host_copy_btn_is_retry = true; @@ -454,16 +473,23 @@ void NetPlayDiag::OnThread(wxCommandEvent& event) else { PanicAlertT("The host chose a game that was not found locally."); - netplay_client.reset(); } + if (m_start_btn) + m_start_btn->Disable(); + m_record_chkbox->Disable(); + m_game_started_evt.Set(); } break; - case NP_GUI_EVT_STOP_GAME : - // client stop game + case NP_GUI_EVT_FAILURE: { - netplay_client->StopGame(); + main_frame->DoStop(); + wxString err = FailureReasonStringForDialog(netplay_client->m_failure_reason); + // see other comment about wx bug + auto complain = new wxMessageDialog(this, err); + complain->Bind(wxEVT_WINDOW_MODAL_DIALOG_CLOSED, &NetPlayDiag::OnErrorClosed, this); + complain->ShowWindowModal(); + return; } - break; } // chat messages @@ -476,6 +502,11 @@ void NetPlayDiag::OnThread(wxCommandEvent& event) } } +void NetPlayDiag::OnErrorClosed(wxCommandEvent&) +{ + Destroy(); +} + void NetPlayDiag::OnConfigPads(wxCommandEvent&) { #if 0 @@ -556,7 +587,6 @@ bool ConnectDiag::Validate() if (netplay_client) { // shouldn't be possible, just in case - printf("already a NPC???\n"); return false; } std::string hostSpec = GetHost(); @@ -579,12 +609,13 @@ void ConnectDiag::OnThread(wxCommandEvent& event) { if (netplay_client->m_state == NetPlayClient::Connected) { - netplay_client->SetDialog(new NetPlayDiag(GetParent(), "", false)); + // changes m_state_callback + new NetPlayDiag(GetParent(), "", false); EndModal(0); } else { - wxString err = FailureReasonStringForConnect(netplay_client->m_failure_reason); + wxString err = FailureReasonStringForDialog(netplay_client->m_failure_reason); netplay_client.reset(); // connection failure auto complain = new wxMessageDialog(this, err); @@ -627,7 +658,6 @@ ConnectDiag::~ConnectDiag() { if (netplay_client) { - netplay_client->m_state_callback = nullptr; if (GetReturnCode() != 0) netplay_client.reset(); } @@ -724,10 +754,10 @@ void PadMapDiag::OnAdjust(wxCommandEvent& event) } } -void NetPlay::StopGame() +void NetPlay::GameStopped() { if (netplay_client != NULL) - netplay_client->Stop(); + netplay_client->GameStopped(); } void NetPlay::ShowConnectDialog(wxWindow* parent) @@ -780,6 +810,7 @@ void NetPlay::StartHosting(std::string id, wxWindow* parent) npc->m_state == NetPlayClient::Failure) ev.Set(); })); + ev.Wait(); if (netplay_client->m_state == NetPlayClient::Failure) { PanicAlert("Failed to init netplay client. This shouldn't happen..."); @@ -787,7 +818,6 @@ void NetPlay::StartHosting(std::string id, wxWindow* parent) netplay_server.reset(); return; } - ev.Wait(); if (netplay_client->m_state != NetPlayClient::Connected) { @@ -797,8 +827,5 @@ void NetPlay::StartHosting(std::string id, wxWindow* parent) netplay_server.reset(); return; } - auto diag = new NetPlayDiag(parent, id, true); - netplay_client->SetDialog(diag); - netplay_server->SetDialog(diag); - diag->Update(); + new NetPlayDiag(parent, id, true); } diff --git a/Source/Core/DolphinWX/Src/NetWindow.h b/Source/Core/DolphinWX/Src/NetWindow.h index 738b778d39d9..f7ff76578a93 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.h +++ b/Source/Core/DolphinWX/Src/NetWindow.h @@ -30,6 +30,7 @@ enum NP_GUI_EVT_CHANGE_GAME = 45, NP_GUI_EVT_START_GAME, NP_GUI_EVT_STOP_GAME, + NP_GUI_EVT_FAILURE, }; class NetPlayDiag : public wxFrame, public NetPlayUI @@ -43,19 +44,20 @@ class NetPlayDiag : public wxFrame, public NetPlayUI void OnStart(wxCommandEvent& event); // implementation of NetPlayUI methods - void BootGame(const std::string& filename); - void StopGame(); + virtual void BootGame(const std::string& filename) override; + virtual void GameStopped() override; - void Update(); - void AppendChat(const std::string& msg); + virtual void Update() override; + virtual void AppendChat(const std::string& msg) override; - void OnMsgChangeGame(const std::string& filename); - void OnMsgStartGame(); - void OnMsgStopGame(); + virtual void OnMsgChangeGame(const std::string& filename) override; + virtual void OnMsgStartGame() override; + virtual void OnMsgStopGame() override; + void OnStateChanged(); static NetPlayDiag *&GetInstance() { return npd; }; - bool IsRecording(); + virtual bool IsRecording() override; static const GameListItem* FindISO(const std::string& id); void UpdateGameName(); @@ -73,6 +75,7 @@ class NetPlayDiag : public wxFrame, public NetPlayUI void OnDefocusName(wxFocusEvent& event); void OnCopyIP(wxCommandEvent& event); void GetNetSettings(NetSettings &settings); + void OnErrorClosed(wxCommandEvent& event); wxTextCtrl* m_name_text; wxListBox* m_player_lbox; @@ -89,6 +92,7 @@ class NetPlayDiag : public wxFrame, public NetPlayUI wxStaticText* m_game_label; wxButton* m_start_btn; bool m_is_hosting; + Common::Event m_game_started_evt; std::vector m_playerids; @@ -129,7 +133,7 @@ class PadMapDiag : public wxDialog namespace NetPlay { - void StopGame(); + void GameStopped(); void ShowConnectDialog(wxWindow* parent); void StartHosting(std::string id, wxWindow* parent); } From cf66f4dcd5ce8f1896ae69ec7c21bcd5abc1add4 Mon Sep 17 00:00:00 2001 From: comex Date: Wed, 20 Nov 2013 13:49:52 -0500 Subject: [PATCH 087/202] Fix indentation ;p --- Source/Core/DolphinWX/Src/NetWindow.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/Core/DolphinWX/Src/NetWindow.h b/Source/Core/DolphinWX/Src/NetWindow.h index f7ff76578a93..b451406d7e2c 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.h +++ b/Source/Core/DolphinWX/Src/NetWindow.h @@ -63,7 +63,7 @@ class NetPlayDiag : public wxFrame, public NetPlayUI void UpdateGameName(); private: - DECLARE_EVENT_TABLE() + DECLARE_EVENT_TABLE() void OnChat(wxCommandEvent& event); void OnQuit(wxCommandEvent& event); @@ -72,8 +72,8 @@ class NetPlayDiag : public wxFrame, public NetPlayUI void OnThread(wxCommandEvent& event); void OnAdjustBuffer(wxCommandEvent& event); void OnConfigPads(wxCommandEvent& event); - void OnDefocusName(wxFocusEvent& event); - void OnCopyIP(wxCommandEvent& event); + void OnDefocusName(wxFocusEvent& event); + void OnCopyIP(wxCommandEvent& event); void GetNetSettings(NetSettings &settings); void OnErrorClosed(wxCommandEvent& event); @@ -84,14 +84,14 @@ class NetPlayDiag : public wxFrame, public NetPlayUI wxCheckBox* m_memcard_write; wxCheckBox* m_record_chkbox; wxChoice* m_host_type_choice; - wxStaticText* m_host_label; - wxButton* m_host_copy_btn; - bool m_host_copy_btn_is_retry; + wxStaticText* m_host_label; + wxButton* m_host_copy_btn; + bool m_host_copy_btn_is_retry; std::string m_selected_game; wxStaticText* m_game_label; wxButton* m_start_btn; - bool m_is_hosting; + bool m_is_hosting; Common::Event m_game_started_evt; std::vector m_playerids; @@ -108,7 +108,7 @@ class ConnectDiag : public wxDialog bool Validate(); private: - DECLARE_EVENT_TABLE() + DECLARE_EVENT_TABLE() void OnChange(wxCommandEvent& event); void OnThread(wxCommandEvent& event); From b1ed21690b80fcd8ebb8f357a7dd4dc02621d44e Mon Sep 17 00:00:00 2001 From: comex Date: Wed, 20 Nov 2013 13:58:55 -0500 Subject: [PATCH 088/202] Put in CommonTypes.h to avoid issues with __STDC_FORMAT_MACROS. --- Source/Core/Common/Src/CommonTypes.h | 1 + Source/Core/Common/Src/SDCardUtil.cpp | 1 - Source/Core/Common/Src/SysConf.cpp | 1 - Source/Core/Common/Src/x64Emitter.cpp | 1 - Source/Core/Core/Src/CoreParameter.cpp | 1 - Source/Core/Core/Src/CoreTiming.cpp | 1 - Source/Core/Core/Src/HW/DSPLLE/DSPLLEGlobals.cpp | 1 - Source/Core/Core/Src/HW/GCMemcard.cpp | 1 - Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_DI.cpp | 1 - Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_es.cpp | 4 ---- Source/Core/Core/Src/PowerPC/Interpreter/Interpreter.cpp | 1 - Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp | 1 - Source/Core/Core/Src/PowerPC/JitCommon/JitBackpatch.cpp | 1 - Source/Core/Core/Src/PowerPC/JitILCommon/IR.cpp | 1 - Source/Core/Core/Src/PowerPC/JitInterface.cpp | 1 - Source/Core/Core/Src/PowerPC/PPCTables.cpp | 1 - Source/Core/DiscIO/Src/CompressedBlob.cpp | 1 - Source/Core/DiscIO/Src/DiscScrubber.cpp | 1 - Source/Core/DiscIO/Src/FileSystemGCWii.cpp | 1 - Source/Core/DolphinWX/Src/GameListCtrl.cpp | 1 - Source/Core/DolphinWX/Src/ISOProperties.cpp | 1 - Source/Core/DolphinWX/Src/MemoryCards/WiiSaveCrypted.cpp | 1 - Source/Core/VideoBackends/D3D/Src/Render.cpp | 1 - Source/Core/VideoBackends/OGL/Src/Render.cpp | 1 - 24 files changed, 1 insertion(+), 26 deletions(-) diff --git a/Source/Core/Common/Src/CommonTypes.h b/Source/Core/Common/Src/CommonTypes.h index 3eb131c1a170..a8dd206f3a35 100644 --- a/Source/Core/Common/Src/CommonTypes.h +++ b/Source/Core/Common/Src/CommonTypes.h @@ -10,6 +10,7 @@ #ifndef _COMMONTYPES_H_ #define _COMMONTYPES_H_ +#include #include #include diff --git a/Source/Core/Common/Src/SDCardUtil.cpp b/Source/Core/Common/Src/SDCardUtil.cpp index 5475cf131d34..d2bbcfc3ae7e 100644 --- a/Source/Core/Common/Src/SDCardUtil.cpp +++ b/Source/Core/Common/Src/SDCardUtil.cpp @@ -36,7 +36,6 @@ #include #include #include -#include #ifndef _WIN32 #include // for unlink() diff --git a/Source/Core/Common/Src/SysConf.cpp b/Source/Core/Common/Src/SysConf.cpp index acfb11b0ed5f..29411bb6e8e4 100644 --- a/Source/Core/Common/Src/SysConf.cpp +++ b/Source/Core/Common/Src/SysConf.cpp @@ -5,7 +5,6 @@ #include "FileUtil.h" #include "SysConf.h" -#include SysConf::SysConf() : m_IsValid(false) diff --git a/Source/Core/Common/Src/x64Emitter.cpp b/Source/Core/Common/Src/x64Emitter.cpp index 05540ba4a800..b41a11356a1b 100644 --- a/Source/Core/Common/Src/x64Emitter.cpp +++ b/Source/Core/Common/Src/x64Emitter.cpp @@ -7,7 +7,6 @@ #include "x64ABI.h" #include "CPUDetect.h" -#include namespace Gen { diff --git a/Source/Core/Core/Src/CoreParameter.cpp b/Source/Core/Core/Src/CoreParameter.cpp index e7843fe9973c..3869e1cf79cc 100644 --- a/Source/Core/Core/Src/CoreParameter.cpp +++ b/Source/Core/Core/Src/CoreParameter.cpp @@ -18,7 +18,6 @@ #include "Core.h" // for bWii #include "FifoPlayer/FifoDataFile.h" -#include SCoreStartupParameter::SCoreStartupParameter() : hInstance(0), diff --git a/Source/Core/Core/Src/CoreTiming.cpp b/Source/Core/Core/Src/CoreTiming.cpp index 6dac07e56712..a51a5cd52720 100644 --- a/Source/Core/Core/Src/CoreTiming.cpp +++ b/Source/Core/Core/Src/CoreTiming.cpp @@ -3,7 +3,6 @@ // Refer to the license.txt file included. #include -#include #include "Thread.h" #include "PowerPC/PowerPC.h" diff --git a/Source/Core/Core/Src/HW/DSPLLE/DSPLLEGlobals.cpp b/Source/Core/Core/Src/HW/DSPLLE/DSPLLEGlobals.cpp index c879f5604b13..da5f37d48c1f 100644 --- a/Source/Core/Core/Src/HW/DSPLLE/DSPLLEGlobals.cpp +++ b/Source/Core/Core/Src/HW/DSPLLE/DSPLLEGlobals.cpp @@ -6,7 +6,6 @@ #include "FileUtil.h" #include "DSP/DSPCore.h" #include "DSPLLEGlobals.h" -#include #if PROFILE diff --git a/Source/Core/Core/Src/HW/GCMemcard.cpp b/Source/Core/Core/Src/HW/GCMemcard.cpp index 2421e572ae86..f3c51c6377be 100644 --- a/Source/Core/Core/Src/HW/GCMemcard.cpp +++ b/Source/Core/Core/Src/HW/GCMemcard.cpp @@ -5,7 +5,6 @@ #include "GCMemcard.h" #include "ColorUtil.h" -#include static void ByteSwap(u8 *valueA, u8 *valueB) { diff --git a/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_DI.cpp b/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_DI.cpp index e0106bf273b3..b69ec69febae 100644 --- a/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_DI.cpp +++ b/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_DI.cpp @@ -19,7 +19,6 @@ #include "../../DiscIO/Src/FileMonitor.h" -#include using namespace DVDInterface; diff --git a/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_es.cpp b/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_es.cpp index ea4230f4851c..13e60b1b51f3 100644 --- a/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_es.cpp +++ b/Source/Core/Core/Src/IPC_HLE/WII_IPC_HLE_Device_es.cpp @@ -35,10 +35,6 @@ #include "WII_IPC_HLE_Device_es.h" -// need to include this before polarssl/aes.h, -// otherwise we may not get __STDC_FORMAT_MACROS -#include - #include "../PowerPC/PowerPC.h" #include "../VolumeHandler.h" #include "FileUtil.h" diff --git a/Source/Core/Core/Src/PowerPC/Interpreter/Interpreter.cpp b/Source/Core/Core/Src/PowerPC/Interpreter/Interpreter.cpp index dd71abcaebe8..f2d488d59f83 100644 --- a/Source/Core/Core/Src/PowerPC/Interpreter/Interpreter.cpp +++ b/Source/Core/Core/Src/PowerPC/Interpreter/Interpreter.cpp @@ -14,7 +14,6 @@ #include "../GDBStub.h" #endif -#include namespace { u32 last_pc; diff --git a/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp b/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp index b15ffbaccac2..94447f882619 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp @@ -4,7 +4,6 @@ #include #include -#include #include "Common.h" #include "../../HLE/HLE.h" diff --git a/Source/Core/Core/Src/PowerPC/JitCommon/JitBackpatch.cpp b/Source/Core/Core/Src/PowerPC/JitCommon/JitBackpatch.cpp index b7cc031c99e1..2df9a46c2734 100644 --- a/Source/Core/Core/Src/PowerPC/JitCommon/JitBackpatch.cpp +++ b/Source/Core/Core/Src/PowerPC/JitCommon/JitBackpatch.cpp @@ -3,7 +3,6 @@ // Refer to the license.txt file included. #include -#include #include "Common.h" #include "disasm.h" diff --git a/Source/Core/Core/Src/PowerPC/JitILCommon/IR.cpp b/Source/Core/Core/Src/PowerPC/JitILCommon/IR.cpp index 3a4866f2f804..c904b3e1e956 100644 --- a/Source/Core/Core/Src/PowerPC/JitILCommon/IR.cpp +++ b/Source/Core/Core/Src/PowerPC/JitILCommon/IR.cpp @@ -118,7 +118,6 @@ Fix profiled loads/stores to work safely. On 32-bit, one solution is to #include #include -#include #include #include #include "IR.h" diff --git a/Source/Core/Core/Src/PowerPC/JitInterface.cpp b/Source/Core/Core/Src/PowerPC/JitInterface.cpp index 91d43a779201..ac63ddd3210b 100644 --- a/Source/Core/Core/Src/PowerPC/JitInterface.cpp +++ b/Source/Core/Core/Src/PowerPC/JitInterface.cpp @@ -3,7 +3,6 @@ // Refer to the license.txt file included. #include -#include #ifdef _WIN32 #include diff --git a/Source/Core/Core/Src/PowerPC/PPCTables.cpp b/Source/Core/Core/Src/PowerPC/PPCTables.cpp index 5854f1f0dab9..096aad056146 100644 --- a/Source/Core/Core/Src/PowerPC/PPCTables.cpp +++ b/Source/Core/Core/Src/PowerPC/PPCTables.cpp @@ -4,7 +4,6 @@ #include #include -#include #include "Common.h" #include "PPCTables.h" diff --git a/Source/Core/DiscIO/Src/CompressedBlob.cpp b/Source/Core/DiscIO/Src/CompressedBlob.cpp index c801ea3698a1..98283bd5e6d4 100644 --- a/Source/Core/DiscIO/Src/CompressedBlob.cpp +++ b/Source/Core/DiscIO/Src/CompressedBlob.cpp @@ -9,7 +9,6 @@ #include #endif -#include #include "CompressedBlob.h" #include "DiscScrubber.h" diff --git a/Source/Core/DiscIO/Src/DiscScrubber.cpp b/Source/Core/DiscIO/Src/DiscScrubber.cpp index 928383579e8e..c21a03e2bc1d 100644 --- a/Source/Core/DiscIO/Src/DiscScrubber.cpp +++ b/Source/Core/DiscIO/Src/DiscScrubber.cpp @@ -7,7 +7,6 @@ #include "FileUtil.h" #include "DiscScrubber.h" -#include namespace DiscIO { diff --git a/Source/Core/DiscIO/Src/FileSystemGCWii.cpp b/Source/Core/DiscIO/Src/FileSystemGCWii.cpp index cc09138f3ee8..c94d8640210a 100644 --- a/Source/Core/DiscIO/Src/FileSystemGCWii.cpp +++ b/Source/Core/DiscIO/Src/FileSystemGCWii.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include "FileSystemGCWii.h" #include "StringUtil.h" diff --git a/Source/Core/DolphinWX/Src/GameListCtrl.cpp b/Source/Core/DolphinWX/Src/GameListCtrl.cpp index c020c878c6a6..e1dfa354d8b7 100644 --- a/Source/Core/DolphinWX/Src/GameListCtrl.cpp +++ b/Source/Core/DolphinWX/Src/GameListCtrl.cpp @@ -10,7 +10,6 @@ #include #include -#include #include #include "FileSearch.h" diff --git a/Source/Core/DolphinWX/Src/ISOProperties.cpp b/Source/Core/DolphinWX/Src/ISOProperties.cpp index fed258695f51..20bc7acf0af2 100644 --- a/Source/Core/DolphinWX/Src/ISOProperties.cpp +++ b/Source/Core/DolphinWX/Src/ISOProperties.cpp @@ -7,7 +7,6 @@ #endif #include -#include #include "Common.h" #include "CommonPaths.h" diff --git a/Source/Core/DolphinWX/Src/MemoryCards/WiiSaveCrypted.cpp b/Source/Core/DolphinWX/Src/MemoryCards/WiiSaveCrypted.cpp index 2ccc23639584..104f2d703da3 100644 --- a/Source/Core/DolphinWX/Src/MemoryCards/WiiSaveCrypted.cpp +++ b/Source/Core/DolphinWX/Src/MemoryCards/WiiSaveCrypted.cpp @@ -8,7 +8,6 @@ // http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt #include -#include #include "WiiSaveCrypted.h" #include "FileUtil.h" diff --git a/Source/Core/VideoBackends/D3D/Src/Render.cpp b/Source/Core/VideoBackends/D3D/Src/Render.cpp index d2938e66008c..3cc34caa52a8 100644 --- a/Source/Core/VideoBackends/D3D/Src/Render.cpp +++ b/Source/Core/VideoBackends/D3D/Src/Render.cpp @@ -2,7 +2,6 @@ // Licensed under GPLv2 // Refer to the license.txt file included. -#include #include #include "Timer.h" diff --git a/Source/Core/VideoBackends/OGL/Src/Render.cpp b/Source/Core/VideoBackends/OGL/Src/Render.cpp index b9a3fa4fce22..e239e3d4432c 100644 --- a/Source/Core/VideoBackends/OGL/Src/Render.cpp +++ b/Source/Core/VideoBackends/OGL/Src/Render.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include "GLUtil.h" #if defined(HAVE_WX) && HAVE_WX From bcefa880e4d062121df9ab8b831170ec3a689a50 Mon Sep 17 00:00:00 2001 From: Tillmann Karras Date: Wed, 20 Nov 2013 21:30:49 +0100 Subject: [PATCH 089/202] Jit64: fix fmrx regression Revision ddaf29e039d1736be1ca48c1c0b6af1c8d4ec47d introduced a register corruption bug (#6825). Since fmrx/MOVSD only modifies ps0 but we save both ps0 and ps1 in one xmm register, not loading the previous value when binding to a x64 register trashed ps1. But hey, a good opportunity to shave off one more instruction ;) --- .../Core/Core/Src/PowerPC/Jit64/Jit_FloatingPoint.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit_FloatingPoint.cpp b/Source/Core/Core/Src/PowerPC/Jit64/Jit_FloatingPoint.cpp index b0d0ab485363..65f186e802a8 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit_FloatingPoint.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit_FloatingPoint.cpp @@ -229,11 +229,12 @@ void Jit64::fmrx(UGeckoInstruction inst) } int d = inst.FD; int b = inst.FB; - fpr.Lock(b, d); - fpr.BindToRegister(d, d == b, true); - MOVSD(XMM0, fpr.R(b)); - MOVSD(fpr.R(d), XMM0); - fpr.UnlockAll(); + if (d != b) { + fpr.Lock(b, d); + fpr.BindToRegister(d); + MOVSD(fpr.RX(d), fpr.R(b)); + fpr.UnlockAll(); + } } void Jit64::fcmpx(UGeckoInstruction inst) From 286b6110f1c358ec76205dc931e7c7dd2669ebc4 Mon Sep 17 00:00:00 2001 From: Tony Wasserka Date: Wed, 20 Nov 2013 22:53:10 +0100 Subject: [PATCH 090/202] Revert "Handle BP mask register better to avoid useless BP writes (causing flushes)" This reverts commit 954be9e2d9bfca8b33ac1cb36d3226bf2555f2c1. Fixes issue 6826. --- Source/Core/VideoCommon/Src/BPMemory.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Source/Core/VideoCommon/Src/BPMemory.cpp b/Source/Core/VideoCommon/Src/BPMemory.cpp index 7bc976a943a6..7272ed099303 100644 --- a/Source/Core/VideoCommon/Src/BPMemory.cpp +++ b/Source/Core/VideoCommon/Src/BPMemory.cpp @@ -19,18 +19,15 @@ void LoadBPReg(u32 value0) int opcode = value0 >> 24; int oldval = ((u32*)&bpmem)[opcode]; int newval = (oldval & ~bpmem.bpMask) | (value0 & bpmem.bpMask); + int changes = (oldval ^ newval) & 0xFFFFFF; + BPCmd bp = {opcode, changes, newval}; + + //reset the mask register if (opcode != 0xFE) - { - //reset the mask register bpmem.bpMask = 0xFFFFFF; - int changes = (oldval ^ newval) & 0xFFFFFF; - BPCmd bp = {opcode, changes, newval}; - BPWritten(bp); - } - else - bpmem.bpMask = newval; + BPWritten(bp); } void GetBPRegInfo(const u8* data, char* name, size_t name_size, char* desc, size_t desc_size) From 4c74c8678956652e895333c807cadfcee013354e Mon Sep 17 00:00:00 2001 From: comex Date: Wed, 20 Nov 2013 17:02:27 -0500 Subject: [PATCH 091/202] Fix behavior when offline --- Source/Core/Common/Src/TraversalClient.cpp | 13 ++++++------- Source/Core/Common/Src/TraversalClient.h | 4 ++-- Source/Core/DolphinWX/Src/NetWindow.cpp | 10 +++++++--- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index bde7440d98ad..1bfa48d6e068 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -16,7 +16,6 @@ TraversalClient::TraversalClient(NetHost* netHost, const std::string& server) { m_NetHost = netHost; m_Server = server; - m_State = InitFailure; m_Client = NULL; m_NetHost->m_TraversalClient = this; @@ -41,7 +40,13 @@ void TraversalClient::ReconnectToServer() { m_Server = "vps.qoid.us"; // XXX if (enet_address_set_host(&m_ServerAddress, m_Server.c_str())) + { + m_NetHost->RunOnThread([=]() { + ASSUME_ON(NET); + OnFailure(BadHost); + }); return; + } m_ServerAddress.port = 6262; m_State = Connecting; @@ -289,12 +294,6 @@ bool EnsureTraversalClient(const std::string& server, u16 port) } g_TraversalClient.reset(new TraversalClient(g_MainNetHost.get(), server)); - if (g_TraversalClient->m_State == TraversalClient::InitFailure) - { - g_TraversalClient.reset(); - g_MainNetHost.reset(); - return false; - } } return true; } diff --git a/Source/Core/Common/Src/TraversalClient.h b/Source/Core/Common/Src/TraversalClient.h index a3b6a625ea3f..f9d88affe3ae 100644 --- a/Source/Core/Common/Src/TraversalClient.h +++ b/Source/Core/Common/Src/TraversalClient.h @@ -23,7 +23,6 @@ class TraversalClient public: enum State { - InitFailure, Connecting, Connected, Failure @@ -31,7 +30,8 @@ class TraversalClient enum FailureReason { - VersionTooOld = 0x300, + BadHost = 0x300, + VersionTooOld, ServerForgotAboutUs, SocketSendError, ResendTimeout, diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index f7eb3fb86f49..ea4bb036dac1 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -26,6 +26,8 @@ static wxString FailureReasonStringForHostLabel(int reason) { switch (reason) { + case TraversalClient::BadHost: + return _("(Error: Bad host)"); case TraversalClient::VersionTooOld: return _("(Error: Dolphin too old)"); case TraversalClient::ServerForgotAboutUs: @@ -43,6 +45,11 @@ static wxString FailureReasonStringForDialog(int reason) { switch (reason) { + case TraversalClient::BadHost: + { + auto server = StrToWxStr(SConfig::GetInstance().m_LocalCoreStartupParameter.strNetPlayCentralServer); + return wxString::Format(_("Couldn't look up central server %s"), server); + } case TraversalClient::VersionTooOld: return _("Dolphin too old for traversal server"); case TraversalClient::ServerForgotAboutUs: @@ -394,9 +401,6 @@ void NetPlayDiag::UpdateHostLabel() m_host_copy_btn->Enable(); m_host_copy_btn_is_retry = true; break; - case TraversalClient::InitFailure: - // can't happen - break; } } else if (sel != wxNOT_FOUND) // wxNOT_FOUND shouldn't generally happen From 011fe86d01ebec800ed1018e99a364a620d051bd Mon Sep 17 00:00:00 2001 From: degasus Date: Thu, 21 Nov 2013 05:16:58 +0100 Subject: [PATCH 092/202] jit64: add regcache option IsBound Lots of x86 instructions are different on memory vs registers. So to generate code, we often have to check if a ppc register is already bound to a x86 register. --- Source/Core/Core/Src/PowerPC/Jit64/JitRegCache.cpp | 2 +- Source/Core/Core/Src/PowerPC/Jit64/JitRegCache.h | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Source/Core/Core/Src/PowerPC/Jit64/JitRegCache.cpp b/Source/Core/Core/Src/PowerPC/Jit64/JitRegCache.cpp index b2317c9394e9..1be04a0d5d8f 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/JitRegCache.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64/JitRegCache.cpp @@ -166,7 +166,7 @@ int RegCache::SanityCheck() const void RegCache::DiscardRegContentsIfCached(int preg) { - if (regs[preg].away && regs[preg].location.IsSimpleReg()) + if (IsBound(preg)) { X64Reg xr = regs[preg].location.GetSimpleReg(); xregs[xr].free = true; diff --git a/Source/Core/Core/Src/PowerPC/Jit64/JitRegCache.h b/Source/Core/Core/Src/PowerPC/Jit64/JitRegCache.h index cacd1e0e4e56..f27dcae91c45 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/JitRegCache.h +++ b/Source/Core/Core/Src/PowerPC/Jit64/JitRegCache.h @@ -93,7 +93,7 @@ class RegCache const OpArg &R(int preg) const {return regs[preg].location;} X64Reg RX(int preg) const { - if (regs[preg].away && regs[preg].location.IsSimpleReg()) + if (IsBound(preg)) return regs[preg].location.GetSimpleReg(); PanicAlert("Not so simple - %i", preg); return (X64Reg)-1; @@ -111,6 +111,11 @@ class RegCache return xregs[xreg].free && !xlocks[xreg]; } + bool IsBound(int preg) const + { + return regs[preg].away && regs[preg].location.IsSimpleReg(); + } + X64Reg GetFreeXReg(); From ff917897731d400a8e71c1480425733e651126ee Mon Sep 17 00:00:00 2001 From: Tillmann Karras Date: Thu, 21 Nov 2013 05:31:55 +0100 Subject: [PATCH 093/202] Jit64: really fix fmrx regression This is more tricky than I thought! --- .../Src/PowerPC/Jit64/Jit_FloatingPoint.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit_FloatingPoint.cpp b/Source/Core/Core/Src/PowerPC/Jit64/Jit_FloatingPoint.cpp index 65f186e802a8..7f77112ae96a 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit_FloatingPoint.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit_FloatingPoint.cpp @@ -224,15 +224,26 @@ void Jit64::fmrx(UGeckoInstruction inst) { INSTRUCTION_START JITDISABLE(bJITFloatingPointOff) - if (inst.Rc) { + if (inst.Rc) + { Default(inst); return; } int d = inst.FD; int b = inst.FB; - if (d != b) { + if (d != b) + { fpr.Lock(b, d); - fpr.BindToRegister(d); - MOVSD(fpr.RX(d), fpr.R(b)); + + // we don't need to load d, but if it already is, it must be marked as dirty + if (fpr.IsBound(d)) + { + fpr.BindToRegister(d); + } + fpr.BindToRegister(b, true, false); + + // caveat: the order of ModRM:r/m, ModRM:reg is deliberate! + // "MOVSD reg, mem" zeros out the upper half of the destination register + MOVSD(fpr.R(d), fpr.RX(b)); fpr.UnlockAll(); } } From df74103100b626d8fa6a62bb913a26c8fd6fa243 Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 21 Nov 2013 18:04:17 -0500 Subject: [PATCH 094/202] Stubbed out UI for "configure pads". --- Source/Core/Core/Src/IOSync.cpp | 2 + Source/Core/Core/Src/IOSync.h | 13 +- Source/Core/Core/Src/IOSyncBackends.cpp | 8 +- Source/Core/Core/Src/NetPlayClient.h | 1 + Source/Core/Core/Src/NetPlayServer.cpp | 10 ++ Source/Core/Core/Src/NetPlayServer.h | 22 ++- Source/Core/DolphinWX/Src/NetWindow.cpp | 201 ++++++++++++++---------- Source/Core/DolphinWX/Src/NetWindow.h | 21 ++- 8 files changed, 171 insertions(+), 107 deletions(-) diff --git a/Source/Core/Core/Src/IOSync.cpp b/Source/Core/Core/Src/IOSync.cpp index 4611e27d2769..acb215ddfecc 100644 --- a/Source/Core/Core/Src/IOSync.cpp +++ b/Source/Core/Core/Src/IOSync.cpp @@ -74,3 +74,5 @@ std::unique_ptr g_Backend; Class* g_Classes[Class::NumClasses]; } + +EXISyncClass g_EXISyncClass; diff --git a/Source/Core/Core/Src/IOSync.h b/Source/Core/Core/Src/IOSync.h index 04b3bf9ef4e7..82c27ac37272 100644 --- a/Source/Core/Core/Src/IOSync.h +++ b/Source/Core/Core/Src/IOSync.h @@ -38,6 +38,7 @@ class Class { // These are part of the binary format and should not be changed. ClassSI, + ClassEXI, NumClasses }; @@ -177,9 +178,19 @@ class Class int m_ClassId; }; - void Init(); void ResetBackend(); void DoState(PointerWrap& p); } + +// temporary +class EXISyncClass : public IOSync::Class +{ +public: + EXISyncClass() : IOSync::Class(ClassEXI) {} + virtual void OnConnected(int index, PWBuffer&& subtype) override {}; + virtual void OnDisconnected(int index) override {}; + virtual int GetMaxDeviceIndex() override { return 2; } +}; +extern EXISyncClass g_EXISyncClass; diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index ee1c771b4aca..8fdb878c8462 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -80,9 +80,7 @@ void BackendNetPlay::ConnectLocalDevice(int classId, int localIndex, PWBuffer&& pac.W((u8) classId); pac.W((u8) localIndex); pac.W((u8) 0); // flags - pac.W((PlayerId) 0); // dummy - pac.W((u8) 0); // dummy - pac.vec->append(buf); + pac.W(std::move(buf)); m_Client->SendPacket(std::move(pac)); } @@ -213,8 +211,10 @@ void BackendNetPlay::ProcessPacket(Packet&& p) { PlayerId localPlayer; u8 localIndex; + PWBuffer subtype; p.Do(localPlayer); p.Do(localIndex); + p.Do(subtype); if (localIndex >= g_Classes[classId]->GetMaxDeviceIndex()) { OnPacketError(); @@ -224,7 +224,7 @@ void BackendNetPlay::ProcessPacket(Packet&& p) auto& di = m_DeviceInfo[classId][index] = DeviceInfo(); di.m_LastSentSubframeId = di.m_SubframeId = m_SubframeId; g_Classes[classId]->SetIndex(index, localPlayer == m_Client->m_pid ? localIndex : -1); - g_Classes[classId]->OnConnected(index, PWBuffer(p.vec->data() + p.readOff, p.vec->size() - p.readOff)); + g_Classes[classId]->OnConnected(index, std::move(subtype)); break; } case NP_MSG_DISCONNECT_DEVICE: diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 223cf9f958b4..be051823c1ff 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -41,6 +41,7 @@ class NetPlayUI virtual void OnMsgChangeGame(const std::string& filename) = 0; virtual void OnMsgStartGame() = 0; virtual void OnMsgStopGame() = 0; + virtual void UpdateDevices() = 0; virtual bool IsRecording() = 0; }; diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 78e276849e39..5db76eed2e4b 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -115,6 +115,11 @@ void NetPlayServer::OnTraversalStateChanged() m_dialog->Update(); } +void NetPlayServer::SetDesiredDeviceMapping(int classId, int index, PlayerId pid, int localIndex) ON(NET) +{ + WARN_LOG(NETPLAY, "SetDesiredDeviceMapping stub: class %d index %d -> player %d index %d", classId, index, pid, localIndex); +} + #if defined(__APPLE__) std::string CFStrToStr(CFStringRef cfstr) { @@ -435,6 +440,8 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) return OnDisconnect(pid); WARN_LOG(NETPLAY, "Server: received CONNECT_DEVICE (%u/%u) from client %u", classId, localIndex, pid); + // stub + player.devices_present[classId | (localIndex << 8)] = PWBuffer(); int i; for (i = 0; i < limit; i++) { @@ -458,6 +465,7 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) else // DISCONNECT { WARN_LOG(NETPLAY, "Server: received DISCONNECT_DEVICE (%u/%u) from client %u", classId, localIndex, pid); + player.devices_present.erase(classId | (localIndex << 8)); for (int i = 0; i < IOSync::Class::MaxDeviceIndex; i++) { if (map[i].first == pid && map[i].second == localIndex) @@ -469,6 +477,8 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) } } } + if (m_dialog) + m_dialog->UpdateDevices(); break; } diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 4387fd672dd2..d78ff0e2eeb7 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -43,10 +43,11 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient virtual void OnConnectReady(ENetAddress addr) override {} virtual void OnConnectFailed(u8 reason) override ON(NET) {} + void SetDesiredDeviceMapping(int classId, int index, PlayerId pid, int localIndex) ON(NET); + std::unordered_set GetInterfaceSet(); std::string GetInterfaceHost(std::string interface); -private: class Client { public: @@ -57,11 +58,17 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient u32 ping; u32 current_game; bool connected; - bool in_game; - //bool m_devices_present[IOSync::Class::NumClasses][IOSync::Class::MaxDeviceIndex]; + std::map devices_present; //bool is_localhost; }; + std::vector m_players; + + std::pair m_device_map[IOSync::Class::NumClasses][IOSync::Class::MaxDeviceIndex]; + std::pair m_desired_device_map[IOSync::Class::NumClasses][IOSync::Class::MaxDeviceIndex]; + +private: + void SendToClients(Packet&& packet, const PlayerId skip_pid = -1) NOT_ON(NET); void SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid = -1) ON(NET); MessageId OnConnect(PlayerId pid, Packet& hello) ON(NET); @@ -80,16 +87,7 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient u32 m_current_game; u32 m_target_buffer_size; - // Note about disconnects: Imagine a single player plus spectators. The - // client should not have to wait for the server for each frame. However, - // if the server decides to change the mapping, the client must not desync. - // Therefore, in lieu of more complicated solutions, disconnects should be - // requested rather than forced. - - std::pair m_device_map[IOSync::Class::NumClasses][IOSync::Class::MaxDeviceIndex]; - - std::vector m_players; unsigned m_num_players; // only protects m_selected_game diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index ea4bb036dac1..2e12039ffbef 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -79,6 +79,19 @@ static wxString FailureReasonStringForDialog(int reason) } } +static wxString ClassNameString(IOSync::Class::ClassID cls) +{ + switch (cls) + { + case IOSync::Class::ClassSI: + return _("Controllers"); + case IOSync::Class::ClassEXI: + return _("Memory Cards"); + default: + abort(); + } +} + BEGIN_EVENT_TABLE(NetPlayDiag, wxFrame) EVT_COMMAND(wxID_ANY, wxEVT_THREAD, NetPlayDiag::OnThread) END_EVENT_TABLE() @@ -100,6 +113,7 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const { npd = this; wxPanel* const panel = new wxPanel(this); + m_device_map_diag = NULL; // top crap m_game_label = new wxStaticText(panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize); @@ -255,6 +269,8 @@ void NetPlayDiag::UpdateGameName() NetPlayDiag::~NetPlayDiag() { + if (m_device_map_diag) + m_device_map_diag->Destroy(); // We must be truly stopped before killing netplay_client. main_frame->DoStop(); netplay_client.reset(); @@ -341,6 +357,12 @@ void NetPlayDiag::OnMsgStopGame() Host_Message(WM_USER_STOP); } +void NetPlayDiag::UpdateDevices() +{ + wxCommandEvent evt(wxEVT_THREAD, NP_GUI_EVT_UPDATE_DEVICES); + GetEventHandler()->AddPendingEvent(evt); +} + void NetPlayDiag::OnStateChanged() { if (netplay_client->m_state == NetPlayClient::Failure) @@ -494,6 +516,14 @@ void NetPlayDiag::OnThread(wxCommandEvent& event) complain->ShowWindowModal(); return; } + case NP_GUI_EVT_UPDATE_DEVICES: + { + if (m_device_map_diag && netplay_server) + g_MainNetHost->RunOnThreadSync([&]() { + m_device_map_diag->UpdateDeviceMap(); + }); + } + break; } // chat messages @@ -513,18 +543,25 @@ void NetPlayDiag::OnErrorClosed(wxCommandEvent&) void NetPlayDiag::OnConfigPads(wxCommandEvent&) { -#if 0 - PadMapping mapping[4]; - PadMapping wiimotemapping[4]; - std::vector player_list; - netplay_server->GetPadMapping(mapping); - netplay_server->GetWiimoteMapping(wiimotemapping); - netplay_client->GetPlayers(player_list); - PadMapDiag pmd(this, mapping, wiimotemapping, player_list); - pmd.ShowModal(); - netplay_server->SetPadMapping(mapping); - netplay_server->SetWiimoteMapping(wiimotemapping); -#endif + if (m_device_map_diag) + { + m_device_map_diag->SetFocus(); + } + else + { + m_device_map_diag = new DeviceMapDiag(this, netplay_server.get()); + m_device_map_diag->Bind(wxEVT_SHOW, &NetPlayDiag::OnShowDeviceMapDiag, this); + UpdateDevices(); + } +} + +void NetPlayDiag::OnShowDeviceMapDiag(wxShowEvent& event) +{ + if (!event.IsShown()) + { + m_device_map_diag->Destroy(); + m_device_map_diag = NULL; + } } void NetPlayDiag::OnDefocusName(wxFocusEvent&) @@ -669,93 +706,89 @@ ConnectDiag::~ConnectDiag() SConfig::GetInstance().SaveSettings(); } -PadMapDiag::PadMapDiag(wxWindow* const parent, PadMapping map[], PadMapping wiimotemap[], std::vector& player_list) - : wxDialog(parent, wxID_ANY, _("Configure Pads"), wxDefaultPosition, wxDefaultSize) - , m_mapping(map) - , m_wiimapping (wiimotemap) - , m_player_list(player_list) +DeviceMapDiag::DeviceMapDiag(wxWindow* parent, NetPlayServer* server) + : wxDialog(parent, wxID_ANY, _("Configure Pads"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxTAB_TRAVERSAL) { - wxBoxSizer* const h_szr = new wxBoxSizer(wxHORIZONTAL); - h_szr->AddSpacer(10); + m_server = server; + SetFocus(); +} - wxArrayString player_names; - player_names.Add(_("None")); - for (auto& player : m_player_list) - player_names.Add(player->name); +void DeviceMapDiag::UpdateDeviceMap() +{ + DestroyChildren(); + m_choice_to_cls_idx.clear(); - wxString wiimote_names[5]; - wiimote_names[0] = _("None"); - for (unsigned int i=1; i < 5; ++i) - wiimote_names[i] = wxString(_("Wiimote ")) + (wxChar)(wxT('0')+i); + auto main_szr = new wxBoxSizer(wxVERTICAL); - for (unsigned int i=0; i<4; ++i) + for (int classId = 0; classId < IOSync::Class::NumClasses; classId++) { - wxBoxSizer* const v_szr = new wxBoxSizer(wxVERTICAL); - v_szr->Add(new wxStaticText(this, wxID_ANY, (wxString(_("Pad ")) + (wxChar)(wxT('0')+i))), - 1, wxALIGN_CENTER_HORIZONTAL); - - m_map_cbox[i] = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, player_names); - m_map_cbox[i]->Bind(wxEVT_COMMAND_CHOICE_SELECTED, &PadMapDiag::OnAdjust, this); - if (m_mapping[i] == -1) - m_map_cbox[i]->Select(0); - else - for (unsigned int j = 0; j < m_player_list.size(); j++) - if (m_mapping[i] == m_player_list[j]->pid) - m_map_cbox[i]->Select(j + 1); - - v_szr->Add(m_map_cbox[i], 1); - - h_szr->Add(v_szr, 1, wxTOP | wxEXPAND, 20); - h_szr->AddSpacer(10); - } + auto class_szr = new wxStaticBoxSizer(wxHORIZONTAL, this, ClassNameString((IOSync::Class::ClassID) classId)); + main_szr->AddSpacer(10); + main_szr->Add(class_szr, 0, wxEXPAND | wxLEFT | wxRIGHT, 5); + int max = IOSync::g_Classes[classId]->GetMaxDeviceIndex(); - for (unsigned int i=0; i<4; ++i) - { - wxBoxSizer* const v_szr = new wxBoxSizer(wxVERTICAL); - v_szr->Add(new wxStaticText(this, wxID_ANY, (wxString(_("Wiimote ")) + (wxChar)(wxT('0')+i))), - 1, wxALIGN_CENTER_HORIZONTAL); + std::unordered_map local_idx_to_pos; - m_map_cbox[i+4] = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, player_names); - m_map_cbox[i+4]->Bind(wxEVT_COMMAND_CHOICE_SELECTED, &PadMapDiag::OnAdjust, this); - if (m_wiimapping[i] == -1) - m_map_cbox[i+4]->Select(0); - else - for (unsigned int j = 0; j < m_player_list.size(); j++) - if (m_wiimapping[i] == m_player_list[j]->pid) - m_map_cbox[i+4]->Select(j + 1); + wxArrayString options; + PlayerId pid = 0; + m_pos_to_pid_local_idx[classId].clear(); + options.Add(_("None")); + m_pos_to_pid_local_idx[classId].push_back(std::make_pair(255, 255)); + for (auto& player : m_server->m_players) + { + if (!player.connected) + continue; + for (const auto& p : player.devices_present) + { + int itsClass = p.first & 0xff, local_idx = p.first >> 8; + if (itsClass != classId) continue; + local_idx_to_pos[(pid << 8) | local_idx] = (int) options.Count(); + m_pos_to_pid_local_idx[classId].push_back(std::make_pair(pid, local_idx)); + options.Add(wxString::Format("%s:%u", StrToWxStr(player.name), local_idx)); + } + pid++; + } - v_szr->Add(m_map_cbox[i+4], 1); + for (int idx = 0; idx < max; idx++) + { + auto v_szr = new wxBoxSizer(wxVERTICAL); + wxChoice* choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, options); + choice->Bind(wxEVT_COMMAND_CHOICE_SELECTED, &DeviceMapDiag::OnAdjust, this); + m_choice_to_cls_idx[choice] = std::make_pair(classId, idx); + auto cur = m_server->m_device_map[classId][idx]; + if (cur.first == 255) + choice->Select(0); + else + choice->Select(local_idx_to_pos[(cur.first << 8) | cur.second]); + v_szr->Add(new wxStaticText(this, wxID_ANY, wxString::Format(_("Slot %d"), idx))); + v_szr->Add(choice, 0); + class_szr->Add(v_szr, 0, wxEXPAND); + class_szr->AddSpacer(10); + } - h_szr->Add(v_szr, 1, wxTOP | wxEXPAND, 20); - h_szr->AddSpacer(10); } - wxBoxSizer* const main_szr = new wxBoxSizer(wxVERTICAL); - main_szr->Add(h_szr); main_szr->AddSpacer(5); main_szr->Add(CreateButtonSizer(wxOK), 0, wxEXPAND | wxLEFT | wxRIGHT, 20); main_szr->AddSpacer(5); SetSizerAndFit(main_szr); - SetFocus(); -} - -void PadMapDiag::OnAdjust(wxCommandEvent& event) -{ - (void)event; - for (unsigned int i = 0; i < 4; i++) - { - int player_idx = m_map_cbox[i]->GetSelection(); - if (player_idx > 0) - m_mapping[i] = m_player_list[player_idx - 1]->pid; - else - m_mapping[i] = -1; - - player_idx = m_map_cbox[i+4]->GetSelection(); - if (player_idx > 0) - m_wiimapping[i] = m_player_list[player_idx - 1]->pid; - else - m_wiimapping[i] = -1; - } + Show(); + // Why is this required? + Layout(); +} + +void DeviceMapDiag::OnAdjust(wxCommandEvent& event) +{ + auto p = m_choice_to_cls_idx[(wxChoice*) event.GetEventObject()]; + int classId = p.first, index = p.second; + int pos = event.GetSelection(); + auto q = m_pos_to_pid_local_idx[classId][pos]; + PlayerId pid = q.first; + int local_index = q.second; + g_MainNetHost->RunOnThreadSync([=]() { + ASSUME_ON(NET); + m_server->SetDesiredDeviceMapping(classId, index, pid, local_index); + }); } void NetPlay::GameStopped() diff --git a/Source/Core/DolphinWX/Src/NetWindow.h b/Source/Core/DolphinWX/Src/NetWindow.h index b451406d7e2c..1b747f7f3a01 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.h +++ b/Source/Core/DolphinWX/Src/NetWindow.h @@ -24,6 +24,9 @@ #include "FifoQueue.h" #include "NetPlayClient.h" +#include "NetPlayServer.h" +#include "IOSync.h" +#include enum { @@ -31,8 +34,11 @@ enum NP_GUI_EVT_START_GAME, NP_GUI_EVT_STOP_GAME, NP_GUI_EVT_FAILURE, + NP_GUI_EVT_UPDATE_DEVICES }; +class DeviceMapDiag; + class NetPlayDiag : public wxFrame, public NetPlayUI { public: @@ -53,6 +59,7 @@ class NetPlayDiag : public wxFrame, public NetPlayUI virtual void OnMsgChangeGame(const std::string& filename) override; virtual void OnMsgStartGame() override; virtual void OnMsgStopGame() override; + virtual void UpdateDevices() override; void OnStateChanged(); static NetPlayDiag *&GetInstance() { return npd; }; @@ -72,6 +79,7 @@ class NetPlayDiag : public wxFrame, public NetPlayUI void OnThread(wxCommandEvent& event); void OnAdjustBuffer(wxCommandEvent& event); void OnConfigPads(wxCommandEvent& event); + void OnShowDeviceMapDiag(wxShowEvent& event); void OnDefocusName(wxFocusEvent& event); void OnCopyIP(wxCommandEvent& event); void GetNetSettings(NetSettings &settings); @@ -93,6 +101,7 @@ class NetPlayDiag : public wxFrame, public NetPlayUI wxButton* m_start_btn; bool m_is_hosting; Common::Event m_game_started_evt; + DeviceMapDiag* m_device_map_diag; std::vector m_playerids; @@ -117,18 +126,18 @@ class ConnectDiag : public wxDialog wxButton* m_ConnectBtn; }; -class PadMapDiag : public wxDialog +class DeviceMapDiag : public wxDialog { public: - PadMapDiag(wxWindow* const parent, PadMapping map[], PadMapping wiimotemap[], std::vector& player_list); + DeviceMapDiag(wxWindow* parent, NetPlayServer* server); + void UpdateDeviceMap(); private: void OnAdjust(wxCommandEvent& event); - wxChoice* m_map_cbox[8]; - PadMapping* const m_mapping; - PadMapping* const m_wiimapping; - std::vector& m_player_list; + std::unordered_map> m_choice_to_cls_idx; + std::vector> m_pos_to_pid_local_idx[IOSync::Class::NumClasses]; + NetPlayServer* m_server; }; namespace NetPlay From b0a83c9aaa62488958508337c7a28f7966cb6324 Mon Sep 17 00:00:00 2001 From: degasus Date: Wed, 6 Nov 2013 22:19:37 +0100 Subject: [PATCH 095/202] VideoCommon: don't read alpha from efb which don't have alpha This fixes issue 6788 --- .../Core/VideoCommon/Src/TextureCacheBase.cpp | 54 +++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/Source/Core/VideoCommon/Src/TextureCacheBase.cpp b/Source/Core/VideoCommon/Src/TextureCacheBase.cpp index 31ba3d5e6130..9908ec7c826b 100644 --- a/Source/Core/VideoCommon/Src/TextureCacheBase.cpp +++ b/Source/Core/VideoCommon/Src/TextureCacheBase.cpp @@ -616,6 +616,7 @@ void TextureCache::CopyRenderTargetToTexture(u32 dstAddr, unsigned int dstFormat ColorMask[0] = ColorMask[1] = ColorMask[2] = ColorMask[3] = 255.0f; ColorMask[4] = ColorMask[5] = ColorMask[6] = ColorMask[7] = 1.0f / 255.0f; unsigned int cbufid = -1; + bool efbHasAlpha = bpmem.zcontrol.pixel_format == PIXELFMT_RGBA6_Z24; if (srcFormat == PIXELFMT_Z24) { @@ -744,35 +745,56 @@ void TextureCache::CopyRenderTargetToTexture(u32 dstAddr, unsigned int dstFormat colmat[0] = colmat[4] = colmat[8] = colmat[15] = 1.0f; ColorMask[0] = ColorMask[3] = 15.0f; ColorMask[4] = ColorMask[7] = 1.0f / 15.0f; + cbufid = 14; + if(!efbHasAlpha) { + ColorMask[3] = 0.0f; + fConstAdd[3] = 1.0f; + cbufid = 15; + } break; case 3: // RA8 colmat[0] = colmat[4] = colmat[8] = colmat[15] = 1.0f; - cbufid = 15; + + cbufid = 16; + if(!efbHasAlpha) { + ColorMask[3] = 0.0f; + fConstAdd[3] = 1.0f; + cbufid = 17; + } break; case 7: // A8 colmat[3] = colmat[7] = colmat[11] = colmat[15] = 1.0f; - cbufid = 16; + + cbufid = 18; + if(!efbHasAlpha) { + ColorMask[3] = 0.0f; + fConstAdd[0] = 1.0f; + fConstAdd[1] = 1.0f; + fConstAdd[2] = 1.0f; + fConstAdd[3] = 1.0f; + cbufid = 19; + } break; case 9: // G8 colmat[1] = colmat[5] = colmat[9] = colmat[13] = 1.0f; - cbufid = 17; + cbufid = 20; break; case 10: // B8 colmat[2] = colmat[6] = colmat[10] = colmat[14] = 1.0f; - cbufid = 18; + cbufid = 21; break; case 11: // RG8 colmat[0] = colmat[4] = colmat[8] = colmat[13] = 1.0f; - cbufid = 19; + cbufid = 22; break; case 12: // GB8 colmat[1] = colmat[5] = colmat[9] = colmat[14] = 1.0f; - cbufid = 20; + cbufid = 23; break; case 4: // RGB565 @@ -782,7 +804,7 @@ void TextureCache::CopyRenderTargetToTexture(u32 dstAddr, unsigned int dstFormat ColorMask[1] = 63.0f; ColorMask[5] = 1.0f / 63.0f; fConstAdd[3] = 1.0f; // set alpha to 1 - cbufid = 21; + cbufid = 24; break; case 5: // RGB5A3 @@ -791,17 +813,29 @@ void TextureCache::CopyRenderTargetToTexture(u32 dstAddr, unsigned int dstFormat ColorMask[4] = ColorMask[5] = ColorMask[6] = 1.0f / 31.0f; ColorMask[3] = 7.0f; ColorMask[7] = 1.0f / 7.0f; - cbufid = 22; + + cbufid = 25; + if(!efbHasAlpha) { + ColorMask[3] = 0.0f; + fConstAdd[3] = 1.0f; + cbufid = 26; + } break; case 6: // RGBA8 colmat[0] = colmat[5] = colmat[10] = colmat[15] = 1.0f; - cbufid = 23; + + cbufid = 27; + if(!efbHasAlpha) { + ColorMask[3] = 0.0f; + fConstAdd[3] = 1.0f; + cbufid = 28; + } break; default: ERROR_LOG(VIDEO, "Unknown copy color format: 0x%x", dstFormat); colmat[0] = colmat[5] = colmat[10] = colmat[15] = 1.0f; - cbufid = 23; + cbufid = 29; break; } } From efeb0096c995ee4d0b8e6d98977c842c4fa7ee74 Mon Sep 17 00:00:00 2001 From: skidau Date: Fri, 22 Nov 2013 14:55:25 +1100 Subject: [PATCH 096/202] Changed the DSP ROM warning from a panic alert to an on-screen message. --- Source/Core/Core/Src/DSP/DSPCore.cpp | 14 +++++++++----- Source/Core/Core/Src/DSP/DSPHost.h | 1 + Source/Core/Core/Src/HW/DSPLLE/DSPHost.cpp | 6 ++++++ Source/DSPTool/Src/DSPTool.cpp | 1 + 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Source/Core/Core/Src/DSP/DSPCore.cpp b/Source/Core/Core/Src/DSP/DSPCore.cpp index 377e21fba879..ff05dcb3fa8a 100644 --- a/Source/Core/Core/Src/DSP/DSPCore.cpp +++ b/Source/Core/Core/Src/DSP/DSPCore.cpp @@ -113,13 +113,17 @@ static bool VerifyRoms(const char *irom_filename, const char *coef_filename) } if (rom_idx == 1) - PanicAlertT("You are using an old free DSP ROM made by the Dolphin Team.\n" - "Only games using the Zelda UCode will work correctly.\n"); + { + DSPHost_OSD_AddMessage("You are using an old free DSP ROM made by the Dolphin Team.", 6000); + DSPHost_OSD_AddMessage("Only games using the Zelda UCode will work correctly.", 6000); + } if (rom_idx == 2) - PanicAlertT("You are using a free DSP ROM made by the Dolphin Team.\n" - "All Wii games will work correctly, and most GC games should " - "also work fine, but the GBA/IPL/CARD UCodes will not work.\n"); + { + DSPHost_OSD_AddMessage("You are using a free DSP ROM made by the Dolphin Team.", 8000); + DSPHost_OSD_AddMessage("All Wii games will work correctly, and most GC games should ", 8000); + DSPHost_OSD_AddMessage("also work fine, but the GBA/IPL/CARD UCodes will not work.\n", 8000); + } return true; } diff --git a/Source/Core/Core/Src/DSP/DSPHost.h b/Source/Core/Core/Src/DSP/DSPHost.h index cef329a7ec08..18ba5ec447eb 100644 --- a/Source/Core/Core/Src/DSP/DSPHost.h +++ b/Source/Core/Core/Src/DSP/DSPHost.h @@ -12,6 +12,7 @@ u8 DSPHost_ReadHostMemory(u32 addr); void DSPHost_WriteHostMemory(u8 value, u32 addr); +void DSPHost_OSD_AddMessage(const std::string& str, u32 ms); bool DSPHost_OnThread(); bool DSPHost_Wii(); void DSPHost_InterruptRequest(); diff --git a/Source/Core/Core/Src/HW/DSPLLE/DSPHost.cpp b/Source/Core/Core/Src/HW/DSPLLE/DSPHost.cpp index 4b25f4ba204b..5b2dc1f56e2e 100644 --- a/Source/Core/Core/Src/HW/DSPLLE/DSPHost.cpp +++ b/Source/Core/Core/Src/HW/DSPLLE/DSPHost.cpp @@ -13,6 +13,7 @@ #include "../../ConfigManager.h" #include "../../PowerPC/PowerPC.h" #include "Host.h" +#include "OnScreenDisplay.h" // The user of the DSPCore library must supply a few functions so that the // emulation core can access the environment it runs in. If the emulation @@ -29,6 +30,11 @@ void DSPHost_WriteHostMemory(u8 value, u32 addr) DSP::WriteARAM(value, addr); } +void DSPHost_OSD_AddMessage(const std::string& str, u32 ms) +{ + OSD::AddMessage(str, ms); +} + bool DSPHost_OnThread() { const SCoreStartupParameter& _CoreParameter = SConfig::GetInstance().m_LocalCoreStartupParameter; diff --git a/Source/DSPTool/Src/DSPTool.cpp b/Source/DSPTool/Src/DSPTool.cpp index 9f38b1347b5f..5a776be55874 100644 --- a/Source/DSPTool/Src/DSPTool.cpp +++ b/Source/DSPTool/Src/DSPTool.cpp @@ -11,6 +11,7 @@ // Stub out the dsplib host stuff, since this is just a simple cmdline tools. u8 DSPHost_ReadHostMemory(u32 addr) { return 0; } void DSPHost_WriteHostMemory(u8 value, u32 addr) {} +void DSPHost_OSD_AddMessage(const std::string& str, u32 ms) {} bool DSPHost_OnThread() { return false; } bool DSPHost_Wii() { return false; } void DSPHost_CodeLoaded(const u8 *ptr, int size) {} From 1763dc20764cc77f2d9d2fdb88e595fc12db1a3f Mon Sep 17 00:00:00 2001 From: "kostamarino@hotmail.com" Date: Fri, 22 Nov 2013 14:03:12 +0200 Subject: [PATCH 097/202] Gameini database update. Beyond Good and Evil (gc), Hunter: The Reckoning (gc), MARIO SUPERSTAR BASEBALL (gc), Heavy Fire Special Operations (wiiware), Tiger Woods PGA TOUR 2005 (gc), Tiger Woods PGA TOUR 06 (gc), Midway Arcade Treasures (gc), Midway Arcade Treasures 2 (gc), Midway Arcade Treasures 3 (gc), FINAL FANTASY Crystal Chronicles (gc), Freestyle Metal X (gc), Hunter: The Reckoning (gc), Finding Nemo (gc), Tony Hawk's Pro Skater 4 (gc), Yogi Bear (wii), Ghost Mansion Party (wiiware) are updated/added. Fixes issue 6773. Fixes issue 6774. Thanks nash67 for his help. --- Data/Sys/GameSettings/G5TE69.ini | 34 ++++++++++++++++++++++++++++++++ Data/Sys/GameSettings/G5TP69.ini | 34 ++++++++++++++++++++++++++++++++ Data/Sys/GameSettings/G6WE69.ini | 31 +++++++++++++++++++++++++++++ Data/Sys/GameSettings/G6WP69.ini | 31 +++++++++++++++++++++++++++++ Data/Sys/GameSettings/GAKE5D.ini | 23 +++++++++++++++++++++ Data/Sys/GameSettings/GAYE5D.ini | 24 ++++++++++++++++++++++ Data/Sys/GameSettings/GCCJ01.ini | 33 +++++++++++++++++++++++++++++++ Data/Sys/GameSettings/GCCJGC.ini | 32 ++++++++++++++++++++++++++++++ Data/Sys/GameSettings/GE3E5D.ini | 31 +++++++++++++++++++++++++++++ Data/Sys/GameSettings/GFXE5D.ini | 19 ++++++++++++++++++ Data/Sys/GameSettings/GGEE41.ini | 3 ++- Data/Sys/GameSettings/GGEP41.ini | 3 ++- Data/Sys/GameSettings/GGEY41.ini | 3 ++- Data/Sys/GameSettings/GHNE71.ini | 5 ++++- Data/Sys/GameSettings/GHNX71.ini | 22 +++++++++++++++++++++ Data/Sys/GameSettings/GNED78.ini | 22 +++++++++++++++++++++ Data/Sys/GameSettings/GNEE78.ini | 22 +++++++++++++++++++++ Data/Sys/GameSettings/GNEF78.ini | 22 +++++++++++++++++++++ Data/Sys/GameSettings/GNEP78.ini | 22 +++++++++++++++++++++ Data/Sys/GameSettings/GNES78.ini | 22 +++++++++++++++++++++ Data/Sys/GameSettings/GT4D52.ini | 22 +++++++++++++++++++++ Data/Sys/GameSettings/GT4E52.ini | 22 +++++++++++++++++++++ Data/Sys/GameSettings/GT4F52.ini | 22 +++++++++++++++++++++ Data/Sys/GameSettings/GT4P52.ini | 22 +++++++++++++++++++++ Data/Sys/GameSettings/GYQE01.ini | 33 +++++++++++++++++++++++++++++++ Data/Sys/GameSettings/GYQJ01.ini | 15 ++++++++++++++ Data/Sys/GameSettings/GYQP01.ini | 15 ++++++++++++++ Data/Sys/GameSettings/SG8EG9.ini | 34 ++++++++++++++++++++++++++++++++ Data/Sys/GameSettings/SG8PAF.ini | 34 ++++++++++++++++++++++++++++++++ Data/Sys/GameSettings/WHFETY.ini | 5 ++++- Data/Sys/GameSettings/WHUEGL.ini | 30 ++++++++++++++++++++++++++++ 31 files changed, 687 insertions(+), 5 deletions(-) create mode 100644 Data/Sys/GameSettings/G5TE69.ini create mode 100644 Data/Sys/GameSettings/G5TP69.ini create mode 100644 Data/Sys/GameSettings/G6WE69.ini create mode 100644 Data/Sys/GameSettings/G6WP69.ini create mode 100644 Data/Sys/GameSettings/GAKE5D.ini create mode 100644 Data/Sys/GameSettings/GAYE5D.ini create mode 100644 Data/Sys/GameSettings/GCCJ01.ini create mode 100644 Data/Sys/GameSettings/GCCJGC.ini create mode 100644 Data/Sys/GameSettings/GE3E5D.ini create mode 100644 Data/Sys/GameSettings/GFXE5D.ini create mode 100644 Data/Sys/GameSettings/GHNX71.ini create mode 100644 Data/Sys/GameSettings/GNED78.ini create mode 100644 Data/Sys/GameSettings/GNEE78.ini create mode 100644 Data/Sys/GameSettings/GNEF78.ini create mode 100644 Data/Sys/GameSettings/GNEP78.ini create mode 100644 Data/Sys/GameSettings/GNES78.ini create mode 100644 Data/Sys/GameSettings/GT4D52.ini create mode 100644 Data/Sys/GameSettings/GT4E52.ini create mode 100644 Data/Sys/GameSettings/GT4F52.ini create mode 100644 Data/Sys/GameSettings/GT4P52.ini create mode 100644 Data/Sys/GameSettings/GYQE01.ini create mode 100644 Data/Sys/GameSettings/SG8EG9.ini create mode 100644 Data/Sys/GameSettings/SG8PAF.ini create mode 100644 Data/Sys/GameSettings/WHUEGL.ini diff --git a/Data/Sys/GameSettings/G5TE69.ini b/Data/Sys/GameSettings/G5TE69.ini new file mode 100644 index 000000000000..58b572482149 --- /dev/null +++ b/Data/Sys/GameSettings/G5TE69.ini @@ -0,0 +1,34 @@ +# G5TE69 - Tiger Woods PGA TOUR 2005 + +[Core] +# Values set here will override the main dolphin settings. +TLBHack = 1 + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationIssues = +EmulationStateId = 4 + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video] +ProjectionHack = 0 +PH_SZNear = 0 +PH_SZFar = 0 +PH_ExtraParam = 0 +PH_ZNear = +PH_ZFar = + +[Video_Hacks] +EFBCopyEnable = True +EFBToTextureEnable = False + +[Video_Settings] +SafeTextureCacheColorSamples = 512 diff --git a/Data/Sys/GameSettings/G5TP69.ini b/Data/Sys/GameSettings/G5TP69.ini new file mode 100644 index 000000000000..0f6c6542f5e6 --- /dev/null +++ b/Data/Sys/GameSettings/G5TP69.ini @@ -0,0 +1,34 @@ +# G5TP69 - Tiger Woods PGA TOUR 2005 + +[Core] +# Values set here will override the main dolphin settings. +TLBHack = 1 + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationIssues = +EmulationStateId = 4 + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video] +ProjectionHack = 0 +PH_SZNear = 0 +PH_SZFar = 0 +PH_ExtraParam = 0 +PH_ZNear = +PH_ZFar = + +[Video_Hacks] +EFBCopyEnable = True +EFBToTextureEnable = False + +[Video_Settings] +SafeTextureCacheColorSamples = 512 diff --git a/Data/Sys/GameSettings/G6WE69.ini b/Data/Sys/GameSettings/G6WE69.ini new file mode 100644 index 000000000000..f621c8367f6c --- /dev/null +++ b/Data/Sys/GameSettings/G6WE69.ini @@ -0,0 +1,31 @@ +# G6WE69 - Tiger Woods PGA TOUR 06 + +[Core] +# Values set here will override the main dolphin settings. +TLBHack = 1 + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationIssues = +EmulationStateId = 4 + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video] +ProjectionHack = 0 +PH_SZNear = 0 +PH_SZFar = 0 +PH_ExtraParam = 0 +PH_ZNear = +PH_ZFar = + +[Video_Hacks] +EFBCopyEnable = True +EFBToTextureEnable = False diff --git a/Data/Sys/GameSettings/G6WP69.ini b/Data/Sys/GameSettings/G6WP69.ini new file mode 100644 index 000000000000..179f8cdd0d1d --- /dev/null +++ b/Data/Sys/GameSettings/G6WP69.ini @@ -0,0 +1,31 @@ +# G6WP69 - Tiger Woods PGA TOUR 06 + +[Core] +# Values set here will override the main dolphin settings. +TLBHack = 1 + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationIssues = +EmulationStateId = 4 + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video] +ProjectionHack = 0 +PH_SZNear = 0 +PH_SZFar = 0 +PH_ExtraParam = 0 +PH_ZNear = +PH_ZFar = + +[Video_Hacks] +EFBCopyEnable = True +EFBToTextureEnable = False diff --git a/Data/Sys/GameSettings/GAKE5D.ini b/Data/Sys/GameSettings/GAKE5D.ini new file mode 100644 index 000000000000..cb4e4fe176c0 --- /dev/null +++ b/Data/Sys/GameSettings/GAKE5D.ini @@ -0,0 +1,23 @@ +# GAKE5D - Midway Arcade Treasures + +[Core] +# Values set here will override the main dolphin settings. +TLBHack = 1 + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video_Settings] +SafeTextureCacheColorSamples = 0 + diff --git a/Data/Sys/GameSettings/GAYE5D.ini b/Data/Sys/GameSettings/GAYE5D.ini new file mode 100644 index 000000000000..2574e01f7c07 --- /dev/null +++ b/Data/Sys/GameSettings/GAYE5D.ini @@ -0,0 +1,24 @@ +# GAYE5D - Midway Arcade Treasures 2 + +[Core] +# Values set here will override the main dolphin settings. +TLBHack = 1 + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video_Settings] +SafeTextureCacheColorSamples = 0 +UseXFB = True +UseRealXFB = True diff --git a/Data/Sys/GameSettings/GCCJ01.ini b/Data/Sys/GameSettings/GCCJ01.ini new file mode 100644 index 000000000000..b755e512755f --- /dev/null +++ b/Data/Sys/GameSettings/GCCJ01.ini @@ -0,0 +1,33 @@ +# GCCJ01 - FINAL FANTASY Crystal Chronicles + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video] +ProjectionHack = 0 +PH_SZNear = 0 +PH_SZFar = 0 +PH_ExtraParam = 0 +PH_ZNear = +PH_ZFar = + +[Video_Settings] +SafeTextureCacheColorSamples = 512 + +[Video_Hacks] +EFBEmulateFormatChanges = True + diff --git a/Data/Sys/GameSettings/GCCJGC.ini b/Data/Sys/GameSettings/GCCJGC.ini new file mode 100644 index 000000000000..99b8a2ee9de3 --- /dev/null +++ b/Data/Sys/GameSettings/GCCJGC.ini @@ -0,0 +1,32 @@ +# GCCJGC - FINAL FANTASY Crystal Chronicles + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video] +ProjectionHack = 0 +PH_SZNear = 0 +PH_SZFar = 0 +PH_ExtraParam = 0 +PH_ZNear = +PH_ZFar = + +[Video_Settings] +SafeTextureCacheColorSamples = 512 + +[Video_Hacks] +EFBEmulateFormatChanges = True diff --git a/Data/Sys/GameSettings/GE3E5D.ini b/Data/Sys/GameSettings/GE3E5D.ini new file mode 100644 index 000000000000..f518a0b5af04 --- /dev/null +++ b/Data/Sys/GameSettings/GE3E5D.ini @@ -0,0 +1,31 @@ +# GE3E5D - Midway Arcade Treasures 3 + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video] +ProjectionHack = 0 +PH_SZNear = 0 +PH_SZFar = 0 +PH_ExtraParam = 0 +PH_ZNear = +PH_ZFar = + +[Video_Settings] +SafeTextureCacheColorSamples = 0 +UseXFB = True +UseRealXFB = False diff --git a/Data/Sys/GameSettings/GFXE5D.ini b/Data/Sys/GameSettings/GFXE5D.ini new file mode 100644 index 000000000000..5a007199352b --- /dev/null +++ b/Data/Sys/GameSettings/GFXE5D.ini @@ -0,0 +1,19 @@ +# GFXE5D - Freestyle Metal X + +[Core] +# Values set here will override the main dolphin settings. +TLBHack = 1 + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + diff --git a/Data/Sys/GameSettings/GGEE41.ini b/Data/Sys/GameSettings/GGEE41.ini index 4f3fe0e90ae6..313ae3cdf326 100644 --- a/Data/Sys/GameSettings/GGEE41.ini +++ b/Data/Sys/GameSettings/GGEE41.ini @@ -26,7 +26,8 @@ PH_ZNear = PH_ZFar = [Video_Settings] -VSync = False +UseXFB = True +UseRealXFB = False [Video_Hacks] DlistCachingEnable = False diff --git a/Data/Sys/GameSettings/GGEP41.ini b/Data/Sys/GameSettings/GGEP41.ini index c00f5cc92a5b..6ef2027d2a15 100644 --- a/Data/Sys/GameSettings/GGEP41.ini +++ b/Data/Sys/GameSettings/GGEP41.ini @@ -26,7 +26,8 @@ PH_ZNear = PH_ZFar = [Video_Settings] -VSync = False +UseXFB = True +UseRealXFB = False [Video_Hacks] DlistCachingEnable = False diff --git a/Data/Sys/GameSettings/GGEY41.ini b/Data/Sys/GameSettings/GGEY41.ini index 550c18e83214..4f46c1c19d59 100644 --- a/Data/Sys/GameSettings/GGEY41.ini +++ b/Data/Sys/GameSettings/GGEY41.ini @@ -26,7 +26,8 @@ PH_ZNear = PH_ZFar = [Video_Settings] -VSync = False +UseXFB = True +UseRealXFB = False [Video_Hacks] DlistCachingEnable = False diff --git a/Data/Sys/GameSettings/GHNE71.ini b/Data/Sys/GameSettings/GHNE71.ini index 1901f25d2cde..fa603507dd40 100644 --- a/Data/Sys/GameSettings/GHNE71.ini +++ b/Data/Sys/GameSettings/GHNE71.ini @@ -6,7 +6,7 @@ [EmuState] # The Emulation State. 1 is worst, 5 is best, 0 is not set. EmulationStateId = 4 -EmulationIssues = Slow and cutscenes are black +EmulationIssues = Needs real xfb for the videos to show up. [OnLoad] # Add memory patches to be loaded once on boot here. @@ -17,3 +17,6 @@ EmulationIssues = Slow and cutscenes are black [ActionReplay] # Add action replay cheats here. +[Video_Settings] +UseXFB = True +UseRealXFB = True diff --git a/Data/Sys/GameSettings/GHNX71.ini b/Data/Sys/GameSettings/GHNX71.ini new file mode 100644 index 000000000000..33084f79e9e7 --- /dev/null +++ b/Data/Sys/GameSettings/GHNX71.ini @@ -0,0 +1,22 @@ +# GHNX71 - Hunter: The Reckoning + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = Needs real xfb for the videos to show up. + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video_Settings] +UseXFB = True +UseRealXFB = True diff --git a/Data/Sys/GameSettings/GNED78.ini b/Data/Sys/GameSettings/GNED78.ini new file mode 100644 index 000000000000..317761c1272b --- /dev/null +++ b/Data/Sys/GameSettings/GNED78.ini @@ -0,0 +1,22 @@ +# GNED78 - Finding Nemo + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = Needs real xfb for the videos to show up. + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video_Settings] +UseXFB = True +UseRealXFB = True diff --git a/Data/Sys/GameSettings/GNEE78.ini b/Data/Sys/GameSettings/GNEE78.ini new file mode 100644 index 000000000000..af49d7e6642e --- /dev/null +++ b/Data/Sys/GameSettings/GNEE78.ini @@ -0,0 +1,22 @@ +# GNEE78 - Finding Nemo + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = Needs real xfb for the videos to show up. + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video_Settings] +UseXFB = True +UseRealXFB = True diff --git a/Data/Sys/GameSettings/GNEF78.ini b/Data/Sys/GameSettings/GNEF78.ini new file mode 100644 index 000000000000..b0303ea9cf78 --- /dev/null +++ b/Data/Sys/GameSettings/GNEF78.ini @@ -0,0 +1,22 @@ +# GNEF78 - Finding Nemo + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = Needs real xfb for the videos to show up. + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video_Settings] +UseXFB = True +UseRealXFB = True diff --git a/Data/Sys/GameSettings/GNEP78.ini b/Data/Sys/GameSettings/GNEP78.ini new file mode 100644 index 000000000000..5bf28ebd07cb --- /dev/null +++ b/Data/Sys/GameSettings/GNEP78.ini @@ -0,0 +1,22 @@ +# GNEP78 - Finding Nemo + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = Needs real xfb for the videos to show up. + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video_Settings] +UseXFB = True +UseRealXFB = True diff --git a/Data/Sys/GameSettings/GNES78.ini b/Data/Sys/GameSettings/GNES78.ini new file mode 100644 index 000000000000..b29379ab2e82 --- /dev/null +++ b/Data/Sys/GameSettings/GNES78.ini @@ -0,0 +1,22 @@ +# GNES78 - Finding Nemo + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = Needs real xfb for the videos to show up. + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video_Settings] +UseXFB = True +UseRealXFB = True diff --git a/Data/Sys/GameSettings/GT4D52.ini b/Data/Sys/GameSettings/GT4D52.ini new file mode 100644 index 000000000000..25e55b1d9895 --- /dev/null +++ b/Data/Sys/GameSettings/GT4D52.ini @@ -0,0 +1,22 @@ +# GT4D52 - Tony Hawk's Pro Skater 4 + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video_Hacks] +EFBCopyEnable = True +EFBToTextureEnable = False diff --git a/Data/Sys/GameSettings/GT4E52.ini b/Data/Sys/GameSettings/GT4E52.ini new file mode 100644 index 000000000000..4c3d483359c9 --- /dev/null +++ b/Data/Sys/GameSettings/GT4E52.ini @@ -0,0 +1,22 @@ +# GT4E52 - Tony Hawk's Pro Skater 4 + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video_Hacks] +EFBCopyEnable = True +EFBToTextureEnable = False diff --git a/Data/Sys/GameSettings/GT4F52.ini b/Data/Sys/GameSettings/GT4F52.ini new file mode 100644 index 000000000000..c30970c8f672 --- /dev/null +++ b/Data/Sys/GameSettings/GT4F52.ini @@ -0,0 +1,22 @@ +# GT4F52 - Tony Hawk's Pro Skater 4 + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video_Hacks] +EFBCopyEnable = True +EFBToTextureEnable = False diff --git a/Data/Sys/GameSettings/GT4P52.ini b/Data/Sys/GameSettings/GT4P52.ini new file mode 100644 index 000000000000..2a7e1958f1b8 --- /dev/null +++ b/Data/Sys/GameSettings/GT4P52.ini @@ -0,0 +1,22 @@ +# GT4P52 - Tony Hawk's Pro Skater 4 + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video_Hacks] +EFBCopyEnable = True +EFBToTextureEnable = False diff --git a/Data/Sys/GameSettings/GYQE01.ini b/Data/Sys/GameSettings/GYQE01.ini new file mode 100644 index 000000000000..f2eeac9819cb --- /dev/null +++ b/Data/Sys/GameSettings/GYQE01.ini @@ -0,0 +1,33 @@ +# GYQE01 - Mario Superstar Baseball + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = Player's shadow needs efb to ram and texture cache set to safe to appear properly. + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video] +ProjectionHack = 0 +PH_SZNear = 0 +PH_SZFar = 0 +PH_ExtraParam = 0 +PH_ZNear = +PH_ZFar = + +[Video_Settings] +SafeTextureCacheColorSamples = 0 + +[Video_Hacks] +EFBToTextureEnable = False +EFBCopyEnable = True diff --git a/Data/Sys/GameSettings/GYQJ01.ini b/Data/Sys/GameSettings/GYQJ01.ini index 414e4fed0a06..2241ed691fc8 100644 --- a/Data/Sys/GameSettings/GYQJ01.ini +++ b/Data/Sys/GameSettings/GYQJ01.ini @@ -6,6 +6,7 @@ [EmuState] # The Emulation State. 1 is worst, 5 is best, 0 is not set. EmulationStateId = 4 +EmulationIssues = Player's shadow needs efb to ram and texture cache set to safe to appear properly. [OnLoad] # Add memory patches to be loaded once on boot here. @@ -16,3 +17,17 @@ EmulationStateId = 4 [ActionReplay] # Add action replay cheats here. +[Video] +ProjectionHack = 0 +PH_SZNear = 0 +PH_SZFar = 0 +PH_ExtraParam = 0 +PH_ZNear = +PH_ZFar = + +[Video_Settings] +SafeTextureCacheColorSamples = 0 + +[Video_Hacks] +EFBToTextureEnable = False +EFBCopyEnable = True diff --git a/Data/Sys/GameSettings/GYQP01.ini b/Data/Sys/GameSettings/GYQP01.ini index 89f76c202c24..bbbe591b099a 100644 --- a/Data/Sys/GameSettings/GYQP01.ini +++ b/Data/Sys/GameSettings/GYQP01.ini @@ -6,6 +6,7 @@ [EmuState] # The Emulation State. 1 is worst, 5 is best, 0 is not set. EmulationStateId = 4 +EmulationIssues = Player's shadow needs efb to ram and texture cache set to safe to appear properly. [OnLoad] # Add memory patches to be loaded once on boot here. @@ -16,3 +17,17 @@ EmulationStateId = 4 [ActionReplay] # Add action replay cheats here. +[Video] +ProjectionHack = 0 +PH_SZNear = 0 +PH_SZFar = 0 +PH_ExtraParam = 0 +PH_ZNear = +PH_ZFar = + +[Video_Settings] +SafeTextureCacheColorSamples = 0 + +[Video_Hacks] +EFBToTextureEnable = False +EFBCopyEnable = True diff --git a/Data/Sys/GameSettings/SG8EG9.ini b/Data/Sys/GameSettings/SG8EG9.ini new file mode 100644 index 000000000000..53dc72291527 --- /dev/null +++ b/Data/Sys/GameSettings/SG8EG9.ini @@ -0,0 +1,34 @@ +# SG8EG9 - Yogi Bear + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationIssues = +EmulationStateId = 4 + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video] +ProjectionHack = 0 +PH_SZNear = 0 +PH_SZFar = 0 +PH_ExtraParam = 0 +PH_ZNear = +PH_ZFar = + +[Video_Settings] +UseXFB = True +UseRealXFB = False + +[Video_Hacks] +EFBToTextureEnable = False +EFBCopyEnable = True diff --git a/Data/Sys/GameSettings/SG8PAF.ini b/Data/Sys/GameSettings/SG8PAF.ini new file mode 100644 index 000000000000..93f5bd5f52b8 --- /dev/null +++ b/Data/Sys/GameSettings/SG8PAF.ini @@ -0,0 +1,34 @@ +# SG8PAF - Yogi Bear + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationIssues = +EmulationStateId = 4 + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video] +ProjectionHack = 0 +PH_SZNear = 0 +PH_SZFar = 0 +PH_ExtraParam = 0 +PH_ZNear = +PH_ZFar = + +[Video_Settings] +UseXFB = True +UseRealXFB = False + +[Video_Hacks] +EFBToTextureEnable = False +EFBCopyEnable = True diff --git a/Data/Sys/GameSettings/WHFETY.ini b/Data/Sys/GameSettings/WHFETY.ini index 242d398951fc..bc351b7767bf 100644 --- a/Data/Sys/GameSettings/WHFETY.ini +++ b/Data/Sys/GameSettings/WHFETY.ini @@ -6,7 +6,7 @@ [EmuState] # The Emulation State. 1 is worst, 5 is best, 0 is not set. EmulationStateId = 4 -EmulationIssues = +EmulationIssues = [OnLoad] # Add memory patches to be loaded once on boot here. @@ -20,3 +20,6 @@ EmulationIssues = [Video] ProjectionHack = 0 +[Video_Settings] +UseXFB = True +UseRealXFB = False diff --git a/Data/Sys/GameSettings/WHUEGL.ini b/Data/Sys/GameSettings/WHUEGL.ini new file mode 100644 index 000000000000..5422ff12a6a4 --- /dev/null +++ b/Data/Sys/GameSettings/WHUEGL.ini @@ -0,0 +1,30 @@ +# WHUEGL - Ghost Mansion Party + +[Core] +# Values set here will override the main dolphin settings. + +[EmuState] +# The Emulation State. 1 is worst, 5 is best, 0 is not set. +EmulationStateId = 4 +EmulationIssues = + +[OnLoad] +# Add memory patches to be loaded once on boot here. + +[OnFrame] +# Add memory patches to be applied every frame here. + +[ActionReplay] +# Add action replay cheats here. + +[Video] +ProjectionHack = 0 +PH_SZNear = 0 +PH_SZFar = 0 +PH_ExtraParam = 0 +PH_ZNear = +PH_ZFar = + +[Video_Settings] +UseXFB = True +UseRealXFB = False From 672fa65ee76c0addb31b8c8cacc129a76926331d Mon Sep 17 00:00:00 2001 From: Tony Wasserka Date: Fri, 22 Nov 2013 17:45:24 +0100 Subject: [PATCH 098/202] OpenGL: Enable pinned memory even for index buffers (works for me). Big-ish speedup on AMD GPUs for streaming intensive games. --- Source/Core/VideoBackends/OGL/Src/StreamBuffer.cpp | 2 +- Source/Core/VideoCommon/Src/DriverDetails.cpp | 1 - Source/Core/VideoCommon/Src/DriverDetails.h | 3 ++- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/StreamBuffer.cpp b/Source/Core/VideoBackends/OGL/Src/StreamBuffer.cpp index 15135f362182..f6630c0d0975 100644 --- a/Source/Core/VideoBackends/OGL/Src/StreamBuffer.cpp +++ b/Source/Core/VideoBackends/OGL/Src/StreamBuffer.cpp @@ -40,7 +40,7 @@ StreamBuffer::StreamBuffer(u32 type, size_t size, StreamType uploadType) m_uploadtype = BUFFERDATA; else if(g_ogl_config.bSupportsGLSync && g_ActiveConfig.bHackedBufferUpload && (m_uploadtype & MAP_AND_RISK)) m_uploadtype = MAP_AND_RISK; - else if(g_ogl_config.bSupportsGLSync && g_ogl_config.bSupportsGLPinnedMemory && (!DriverDetails::HasBug(DriverDetails::BUG_BROKENPINNEDMEMORY) || type != GL_ELEMENT_ARRAY_BUFFER) && (m_uploadtype & PINNED_MEMORY)) + else if(g_ogl_config.bSupportsGLSync && g_ogl_config.bSupportsGLPinnedMemory && !(DriverDetails::HasBug(DriverDetails::BUG_BROKENPINNEDMEMORY) && type == GL_ELEMENT_ARRAY_BUFFER) && (m_uploadtype & PINNED_MEMORY)) m_uploadtype = PINNED_MEMORY; else if(nvidia && (m_uploadtype & BUFFERSUBDATA)) m_uploadtype = BUFFERSUBDATA; diff --git a/Source/Core/VideoCommon/Src/DriverDetails.cpp b/Source/Core/VideoCommon/Src/DriverDetails.cpp index 1a9a56f532ba..d2e8f2a059fc 100644 --- a/Source/Core/VideoCommon/Src/DriverDetails.cpp +++ b/Source/Core/VideoCommon/Src/DriverDetails.cpp @@ -39,7 +39,6 @@ namespace DriverDetails {VENDOR_MESA, DRIVER_I965, BUG_BROKENUBO, 900, 920, true}, {VENDOR_ATI, DRIVER_ATI, BUG_BROKENHACKEDBUFFER, -1.0, -1.0, true}, {VENDOR_MESA, DRIVER_NOUVEAU, BUG_BROKENHACKEDBUFFER, -1.0, -1.0, true}, - {VENDOR_ATI, DRIVER_ATI, BUG_BROKENPINNEDMEMORY, -1.0, -1.0, true}, {VENDOR_TEGRA, DRIVER_NVIDIA, BUG_ISTEGRA, -1.0, -1.0, true}, {VENDOR_IMGTEC, DRIVER_IMGTEC, BUG_ISPOWERVR, -1.0, -1.0, true}, }; diff --git a/Source/Core/VideoCommon/Src/DriverDetails.h b/Source/Core/VideoCommon/Src/DriverDetails.h index 2a5fb3ce22fe..49af92084e97 100644 --- a/Source/Core/VideoCommon/Src/DriverDetails.h +++ b/Source/Core/VideoCommon/Src/DriverDetails.h @@ -94,10 +94,11 @@ namespace DriverDetails // Bug: The pinned memory extension isn't working for index buffers // Affected devices: AMD as they are the only vendor providing this extension // Started Version: ? - // Ended Version: -1 + // Ended Version: 13.9 working for me (neobrain). // Pinned memory is disabled for index buffer as the amd driver (the only one with pinned memory support) seems // to be broken. We just get flickering/black rendering when using pinned memory here -- degasus - 2013/08/20 // Please see issue #6105 on google code. Let's hope buffer storage solves this issues. + // TODO: Detect broken drivers. BUG_BROKENPINNEDMEMORY, // Bug: Entirely broken UBOs // Affected devices: Qualcomm/Adreno From d987b2c0b88283d12bdacfa985d4f66fbef0fb9e Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 21 Nov 2013 18:45:55 -0500 Subject: [PATCH 099/202] Forbid starting netplay while in a game and starting a game from the main window while in netplay. This is clearly wrong behavior for the user, and fixing it lets me not worry about IOSync races. --- Source/Core/Core/Src/Core.h | 1 + Source/Core/DolphinWX/Src/Frame.cpp | 3 +- Source/Core/DolphinWX/Src/Frame.h | 4 +- Source/Core/DolphinWX/Src/FrameTools.cpp | 76 ++++++++++------------ Source/Core/DolphinWX/Src/GameListCtrl.cpp | 3 + Source/Core/DolphinWX/Src/GameListCtrl.h | 2 +- Source/Core/DolphinWX/Src/NetWindow.cpp | 7 +- Source/Core/DolphinWX/Src/NetWindow.h | 1 - 8 files changed, 48 insertions(+), 49 deletions(-) diff --git a/Source/Core/Core/Src/Core.h b/Source/Core/Core/Src/Core.h index 2b966ea1dba6..6724393cb718 100644 --- a/Source/Core/Core/Src/Core.h +++ b/Source/Core/Core/Src/Core.h @@ -42,6 +42,7 @@ void Stop(); std::string StopMessage(bool, std::string); +bool IsInitialized(); // bool IsRunning(); bool IsRunningAndStarted(); // is running and the cpu loop has been entered bool IsRunningInCurrentThread(); // this tells us whether we are running in the cpu thread. diff --git a/Source/Core/DolphinWX/Src/Frame.cpp b/Source/Core/DolphinWX/Src/Frame.cpp index ae012214c232..a27da501960c 100644 --- a/Source/Core/DolphinWX/Src/Frame.cpp +++ b/Source/Core/DolphinWX/Src/Frame.cpp @@ -276,7 +276,7 @@ CFrame::CFrame(wxFrame* parent, , m_LogWindow(NULL), m_LogConfigWindow(NULL) , m_FifoPlayerDlg(NULL), UseDebugger(_UseDebugger) , m_bBatchMode(_BatchMode), m_bEdit(false), m_bTabSplit(false), m_bNoDocking(false) - , m_bGameLoading(false) + , m_bGameLoading(false), m_bInDestructor(false) { for (int i = 0; i <= IDM_CODEWINDOW - IDM_LOGWINDOW; i++) bFloatWindow[i] = false; @@ -395,6 +395,7 @@ CFrame::CFrame(wxFrame* parent, // Destructor CFrame::~CFrame() { + m_bInDestructor = true; drives.clear(); #if defined(HAVE_XRANDR) && HAVE_XRANDR diff --git a/Source/Core/DolphinWX/Src/Frame.h b/Source/Core/DolphinWX/Src/Frame.h index 25ef52c9bc60..408c02ebb172 100644 --- a/Source/Core/DolphinWX/Src/Frame.h +++ b/Source/Core/DolphinWX/Src/Frame.h @@ -126,7 +126,7 @@ class CFrame : public CRenderFrame void ClearStatusBar(); void GetRenderWindowSize(int& x, int& y, int& width, int& height); void OnRenderWindowSizeRequest(int width, int height); - void BootGame(const std::string& filename); + void BootGame(const std::string& filename, bool is_netplay = false); void OnRenderParentClose(wxCloseEvent& event); void OnRenderParentMove(wxMoveEvent& event); bool RendererHasFocus(); @@ -134,6 +134,7 @@ class CFrame : public CRenderFrame void ToggleDisplayMode (bool bFullscreen); void UpdateWiiMenuChoice(wxMenuItem *WiiMenuItem=NULL); static void ConnectWiimote(int wm_idx, bool connect); + bool IsGameRunning(); #ifdef __WXGTK__ Common::Event panic_event; @@ -178,6 +179,7 @@ class CFrame : public CRenderFrame bool m_bTabSplit; bool m_bNoDocking; bool m_bGameLoading; + bool m_bInDestructor; std::vector drives; diff --git a/Source/Core/DolphinWX/Src/FrameTools.cpp b/Source/Core/DolphinWX/Src/FrameTools.cpp index 676c821b5360..dcba7cdc4dce 100644 --- a/Source/Core/DolphinWX/Src/FrameTools.cpp +++ b/Source/Core/DolphinWX/Src/FrameTools.cpp @@ -564,8 +564,11 @@ void CFrame::InitBitmaps() // 1. Show the game list and boot the selected game. // 2. Default ISO // 3. Boot last selected game -void CFrame::BootGame(const std::string& filename) +void CFrame::BootGame(const std::string& filename, bool is_netplay) { + if (NetPlayDiag::GetInstance() != NULL && !is_netplay) + return; + std::string bootfile = filename; SCoreStartupParameter& StartUp = SConfig::GetInstance().m_LocalCoreStartupParameter; @@ -1308,6 +1311,7 @@ void CFrame::StatusBarMessage(const char * Text, ...) void CFrame::OnNetPlay(wxCommandEvent& WXUNUSED (event)) { NetPlay::ShowConnectDialog(this); + UpdateGUI(); } void CFrame::OnMemcard(wxCommandEvent& WXUNUSED (event)) @@ -1558,6 +1562,10 @@ void CFrame::OnFrameSkip(wxCommandEvent& event) // Update the enabled/disabled status void CFrame::UpdateGUI() { + // ...Can't we just kill the process? + if (m_bInDestructor) + return; + // Save status bool Initialized = Core::IsRunning(); bool Running = Core::GetState() == Core::CORE_RUN; @@ -1612,6 +1620,7 @@ void CFrame::UpdateGUI() // Tools GetMenuBar()->FindItem(IDM_CHEATS)->Enable(SConfig::GetInstance().m_LocalCoreStartupParameter.bEnableCheats); + GetMenuBar()->FindItem(IDM_NETPLAY)->Enable(!IsGameRunning()); GetMenuBar()->FindItem(IDM_CONNECT_WIIMOTE1)->Enable(RunningWii); GetMenuBar()->FindItem(IDM_CONNECT_WIIMOTE2)->Enable(RunningWii); @@ -1654,54 +1663,35 @@ void CFrame::UpdateGUI() GetMenuBar()->FindItem(IDM_RECORDREADONLY)->Enable(Running || Paused); - if (!Initialized && !m_bGameLoading) + if (!IsGameRunning()) { - if (m_GameListCtrl->IsEnabled()) - { - // Prepare to load Default ISO, enable play button - if (!SConfig::GetInstance().m_LocalCoreStartupParameter.m_strDefaultGCM.empty()) - { - if (m_ToolBar) - m_ToolBar->EnableTool(IDM_PLAY, true); - GetMenuBar()->FindItem(IDM_PLAY)->Enable(true); - GetMenuBar()->FindItem(IDM_RECORD)->Enable(true); - GetMenuBar()->FindItem(IDM_PLAYRECORD)->Enable(true); - } - // Prepare to load last selected file, enable play button - else if (!SConfig::GetInstance().m_LastFilename.empty() - && wxFileExists(wxSafeConvertMB2WX(SConfig::GetInstance().m_LastFilename.c_str()))) - { - if (m_ToolBar) - m_ToolBar->EnableTool(IDM_PLAY, true); - GetMenuBar()->FindItem(IDM_PLAY)->Enable(true); - GetMenuBar()->FindItem(IDM_RECORD)->Enable(true); - GetMenuBar()->FindItem(IDM_PLAYRECORD)->Enable(true); - } - else - { - // No game has been selected yet, disable play button - if (m_ToolBar) - m_ToolBar->EnableTool(IDM_PLAY, false); - GetMenuBar()->FindItem(IDM_PLAY)->Enable(false); - GetMenuBar()->FindItem(IDM_RECORD)->Enable(false); - GetMenuBar()->FindItem(IDM_PLAYRECORD)->Enable(false); - } - } - // Game has not started, show game list if (!m_GameListCtrl->IsShown()) { m_GameListCtrl->Enable(); m_GameListCtrl->Show(); } - // Game has been selected but not started, enable play button - if (m_GameListCtrl->GetSelectedISO() != NULL && m_GameListCtrl->IsEnabled()) + if (m_GameListCtrl->IsEnabled()) { + bool enabled = + ( + // Prepare to load Default ISO, enable play button + !SConfig::GetInstance().m_LocalCoreStartupParameter.m_strDefaultGCM.empty() || + // Prepare to load last selected file, enable play button + (!SConfig::GetInstance().m_LastFilename.empty() + && wxFileExists(wxSafeConvertMB2WX(SConfig::GetInstance().m_LastFilename.c_str()))) || + // Game has been selected but not started, enable play button + (m_GameListCtrl->GetSelectedISO() != NULL && m_GameListCtrl->IsEnabled()) + ) && + // No starting while NetPlay is active except through that + NetPlayDiag::GetInstance() == NULL; + + if (m_ToolBar) - m_ToolBar->EnableTool(IDM_PLAY, true); - GetMenuBar()->FindItem(IDM_PLAY)->Enable(true); - GetMenuBar()->FindItem(IDM_RECORD)->Enable(true); - GetMenuBar()->FindItem(IDM_PLAYRECORD)->Enable(true); + m_ToolBar->EnableTool(IDM_PLAY, enabled); + GetMenuBar()->FindItem(IDM_PLAY)->Enable(enabled); + GetMenuBar()->FindItem(IDM_RECORD)->Enable(enabled); + GetMenuBar()->FindItem(IDM_PLAYRECORD)->Enable(enabled); } } else if (Initialized) @@ -1732,6 +1722,12 @@ void CFrame::UpdateGUI() else g_CheatsWindow->Close(); } + m_GameListCtrl->UpdateFileMenu(); +} + +bool CFrame::IsGameRunning() +{ + return m_bGameLoading || Core::IsRunning(); } void CFrame::UpdateGameList() diff --git a/Source/Core/DolphinWX/Src/GameListCtrl.cpp b/Source/Core/DolphinWX/Src/GameListCtrl.cpp index e1dfa354d8b7..a5c8ebf1c3bc 100644 --- a/Source/Core/DolphinWX/Src/GameListCtrl.cpp +++ b/Source/Core/DolphinWX/Src/GameListCtrl.cpp @@ -872,6 +872,8 @@ void CGameListCtrl::AppendContextMenuOptions(wxMenu* menu) menu->Append(IDM_HOSTNETPLAY, _("Change &Netplay Game")); else menu->Append(IDM_HOSTNETPLAY, _("Host &Netplay Game")); + if (main_frame && main_frame->IsGameRunning()) + menu->Enable(IDM_HOSTNETPLAY, false); menu->AppendSeparator(); if (selected_iso && selected_iso->GetPlatform() != GameListItem::GAMECUBE_DISC) @@ -1079,6 +1081,7 @@ void CGameListCtrl::OnHostNetplay(wxCommandEvent& WXUNUSED (event)) if (iso) { NetPlay::StartHosting(iso->GetRevisionSpecificUniqueID(), this); + main_frame->UpdateGUI(); } } diff --git a/Source/Core/DolphinWX/Src/GameListCtrl.h b/Source/Core/DolphinWX/Src/GameListCtrl.h index f28838d7682d..97db2e431b65 100644 --- a/Source/Core/DolphinWX/Src/GameListCtrl.h +++ b/Source/Core/DolphinWX/Src/GameListCtrl.h @@ -40,6 +40,7 @@ class CGameListCtrl : public wxListCtrl static const GameListItem *GetISO(size_t index); void SetFileMenu(wxMenu* fileMenu); + void UpdateFileMenu(); enum { @@ -110,7 +111,6 @@ class CGameListCtrl : public wxListCtrl void CompressSelection(bool _compress); void AutomaticColumnWidth(); void UnselectAll(); - void UpdateFileMenu(); static size_t m_currentItem; static std::string m_currentFilename; diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 2e12039ffbef..fb4134b41371 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -276,6 +276,7 @@ NetPlayDiag::~NetPlayDiag() netplay_client.reset(); netplay_server.reset(); npd = NULL; + main_frame->UpdateGUI(); } void NetPlayDiag::OnChat(wxCommandEvent&) @@ -311,7 +312,7 @@ void NetPlayDiag::OnStart(wxCommandEvent&) void NetPlayDiag::BootGame(const std::string& filename) { - main_frame->BootGame(filename); + main_frame->BootGame(filename, /*is_netplay=*/true); } void NetPlayDiag::GameStopped() @@ -345,11 +346,8 @@ void NetPlayDiag::OnMsgChangeGame(const std::string& filename) void NetPlayDiag::OnMsgStartGame() { - // This has to be synchronous because of following messages. - m_game_started_evt.Reset(); wxCommandEvent evt(wxEVT_THREAD, NP_GUI_EVT_START_GAME); GetEventHandler()->AddPendingEvent(evt); - m_game_started_evt.Wait(); } void NetPlayDiag::OnMsgStopGame() @@ -503,7 +501,6 @@ void NetPlayDiag::OnThread(wxCommandEvent& event) if (m_start_btn) m_start_btn->Disable(); m_record_chkbox->Disable(); - m_game_started_evt.Set(); } break; case NP_GUI_EVT_FAILURE: diff --git a/Source/Core/DolphinWX/Src/NetWindow.h b/Source/Core/DolphinWX/Src/NetWindow.h index 1b747f7f3a01..55b2d43665aa 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.h +++ b/Source/Core/DolphinWX/Src/NetWindow.h @@ -100,7 +100,6 @@ class NetPlayDiag : public wxFrame, public NetPlayUI wxStaticText* m_game_label; wxButton* m_start_btn; bool m_is_hosting; - Common::Event m_game_started_evt; DeviceMapDiag* m_device_map_diag; std::vector m_playerids; From f5483ffdaffbeef2d1db17fb75a2dddd1a06e99b Mon Sep 17 00:00:00 2001 From: comex Date: Fri, 22 Nov 2013 14:55:00 -0500 Subject: [PATCH 100/202] Disable stats. --- Source/Core/Common/Src/NetHost.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/Common/Src/NetHost.cpp b/Source/Core/Common/Src/NetHost.cpp index a5c06b3a00d4..0d1d3719a977 100644 --- a/Source/Core/Common/Src/NetHost.cpp +++ b/Source/Core/Common/Src/NetHost.cpp @@ -244,7 +244,7 @@ void NetHost::ProcessPacketQueue() void NetHost::PrintStats() { -#if 1 +#if 0 if (m_StatsTimer.GetTimeDifference() > 5000) { m_StatsTimer.Update(); From 0a312559438a15eb90cc4faf073f9d2a26a0943b Mon Sep 17 00:00:00 2001 From: Matthew Parlane Date: Sun, 24 Nov 2013 11:33:43 +1300 Subject: [PATCH 101/202] Unused arguments removed from XFBSource::Draw Thanks neo. --- Source/Core/VideoBackends/D3D/Src/FramebufferManager.cpp | 2 +- Source/Core/VideoBackends/D3D/Src/FramebufferManager.h | 2 +- Source/Core/VideoBackends/D3D/Src/Render.cpp | 2 +- Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp | 2 +- Source/Core/VideoBackends/OGL/Src/FramebufferManager.h | 2 +- Source/Core/VideoBackends/OGL/Src/Render.cpp | 2 +- Source/Core/VideoCommon/Src/FramebufferManagerBase.h | 3 +-- 7 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Source/Core/VideoBackends/D3D/Src/FramebufferManager.cpp b/Source/Core/VideoBackends/D3D/Src/FramebufferManager.cpp index 973cf3818185..e010068bce53 100644 --- a/Source/Core/VideoBackends/D3D/Src/FramebufferManager.cpp +++ b/Source/Core/VideoBackends/D3D/Src/FramebufferManager.cpp @@ -180,7 +180,7 @@ void FramebufferManager::GetTargetSize(unsigned int *width, unsigned int *height } void XFBSource::Draw(const MathUtil::Rectangle &sourcerc, - const MathUtil::Rectangle &drawrc, int width, int height) const + const MathUtil::Rectangle &drawrc) const { D3D::drawShadedTexSubQuad(tex->GetSRV(), &sourcerc, texWidth, texHeight, &drawrc, PixelShaderCache::GetColorCopyProgram(false), diff --git a/Source/Core/VideoBackends/D3D/Src/FramebufferManager.h b/Source/Core/VideoBackends/D3D/Src/FramebufferManager.h index 2b14ec2b66e8..abd2b27368c8 100644 --- a/Source/Core/VideoBackends/D3D/Src/FramebufferManager.h +++ b/Source/Core/VideoBackends/D3D/Src/FramebufferManager.h @@ -51,7 +51,7 @@ struct XFBSource : public XFBSourceBase ~XFBSource() { tex->Release(); } void Draw(const MathUtil::Rectangle &sourcerc, - const MathUtil::Rectangle &drawrc, int width, int height) const; + const MathUtil::Rectangle &drawrc) const; void DecodeToTexture(u32 xfbAddr, u32 fbWidth, u32 fbHeight); void CopyEFB(float Gamma); diff --git a/Source/Core/VideoBackends/D3D/Src/Render.cpp b/Source/Core/VideoBackends/D3D/Src/Render.cpp index d2938e66008c..a84010c70f0f 100644 --- a/Source/Core/VideoBackends/D3D/Src/Render.cpp +++ b/Source/Core/VideoBackends/D3D/Src/Render.cpp @@ -832,7 +832,7 @@ void Renderer::Swap(u32 xfbAddr, u32 fbWidth, u32 fbHeight,const EFBRectangle& r //drawRc.right *= hScale; } - xfbSource->Draw(sourceRc, drawRc, 0, 0); + xfbSource->Draw(sourceRc, drawRc); } } else diff --git a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp index 081c5e71d025..e2e4d873ddcb 100644 --- a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp +++ b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp @@ -433,7 +433,7 @@ XFBSource::~XFBSource() void XFBSource::Draw(const MathUtil::Rectangle &sourcerc, - const MathUtil::Rectangle &drawrc, int width, int height) const + const MathUtil::Rectangle &drawrc) const { // Texture map xfbSource->texture onto the main buffer glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); diff --git a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.h b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.h index 47c8b2048d75..7b93b239ae88 100644 --- a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.h +++ b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.h @@ -52,7 +52,7 @@ struct XFBSource : public XFBSourceBase void CopyEFB(float Gamma) override; void DecodeToTexture(u32 xfbAddr, u32 fbWidth, u32 fbHeight) override; void Draw(const MathUtil::Rectangle &sourcerc, - const MathUtil::Rectangle &drawrc, int width, int height) const override; + const MathUtil::Rectangle &drawrc) const override; const GLuint texture; }; diff --git a/Source/Core/VideoBackends/OGL/Src/Render.cpp b/Source/Core/VideoBackends/OGL/Src/Render.cpp index b9a3fa4fce22..d75ac311598a 100644 --- a/Source/Core/VideoBackends/OGL/Src/Render.cpp +++ b/Source/Core/VideoBackends/OGL/Src/Render.cpp @@ -1379,7 +1379,7 @@ void Renderer::Swap(u32 xfbAddr, u32 fbWidth, u32 fbHeight,const EFBRectangle& r sourceRc.top = xfbSource->sourceRc.top; sourceRc.bottom = xfbSource->sourceRc.bottom; - xfbSource->Draw(sourceRc, drawRc, 0, 0); + xfbSource->Draw(sourceRc, drawRc); } } else diff --git a/Source/Core/VideoCommon/Src/FramebufferManagerBase.h b/Source/Core/VideoCommon/Src/FramebufferManagerBase.h index e529bb1a3977..2680f0ced406 100644 --- a/Source/Core/VideoCommon/Src/FramebufferManagerBase.h +++ b/Source/Core/VideoCommon/Src/FramebufferManagerBase.h @@ -15,9 +15,8 @@ struct XFBSourceBase { virtual ~XFBSourceBase() {} - // TODO: only DX9 uses the width/height params virtual void Draw(const MathUtil::Rectangle &sourcerc, - const MathUtil::Rectangle &drawrc, int width, int height) const = 0; + const MathUtil::Rectangle &drawrc) const = 0; virtual void DecodeToTexture(u32 xfbAddr, u32 fbWidth, u32 fbHeight) = 0; From 2bf7379a7f171a1c80490951978d7ee3b2154365 Mon Sep 17 00:00:00 2001 From: degasus Date: Sun, 24 Nov 2013 04:00:12 +0100 Subject: [PATCH 102/202] D3D: also fix MAX_COPY_BUFFERS the D3D backend caches the colmat buffers. As we've created more different colmats, the maximum of this matrices must also be updated. --- Source/Core/VideoBackends/D3D/Src/TextureCache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/VideoBackends/D3D/Src/TextureCache.cpp b/Source/Core/VideoBackends/D3D/Src/TextureCache.cpp index 22e644b2d899..14a20f4aadfd 100644 --- a/Source/Core/VideoBackends/D3D/Src/TextureCache.cpp +++ b/Source/Core/VideoBackends/D3D/Src/TextureCache.cpp @@ -20,7 +20,7 @@ namespace DX11 { static TextureEncoder* g_encoder = NULL; -const size_t MAX_COPY_BUFFERS = 25; +const size_t MAX_COPY_BUFFERS = 30; ID3D11Buffer* efbcopycbuf[MAX_COPY_BUFFERS] = { 0 }; TextureCache::TCacheEntry::~TCacheEntry() From 09f4439d0c06ddd422b99e9aba26334e97c5bed9 Mon Sep 17 00:00:00 2001 From: degasus Date: Sun, 24 Nov 2013 04:43:54 +0100 Subject: [PATCH 103/202] VideoCommon: reorder cbufid in orderer. We've used once two times --- .../VideoBackends/D3D/Src/TextureCache.cpp | 2 +- .../Core/VideoCommon/Src/TextureCacheBase.cpp | 60 +++++++++---------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Source/Core/VideoBackends/D3D/Src/TextureCache.cpp b/Source/Core/VideoBackends/D3D/Src/TextureCache.cpp index 14a20f4aadfd..27bee7240e83 100644 --- a/Source/Core/VideoBackends/D3D/Src/TextureCache.cpp +++ b/Source/Core/VideoBackends/D3D/Src/TextureCache.cpp @@ -20,7 +20,7 @@ namespace DX11 { static TextureEncoder* g_encoder = NULL; -const size_t MAX_COPY_BUFFERS = 30; +const size_t MAX_COPY_BUFFERS = 32; ID3D11Buffer* efbcopycbuf[MAX_COPY_BUFFERS] = { 0 }; TextureCache::TCacheEntry::~TCacheEntry() diff --git a/Source/Core/VideoCommon/Src/TextureCacheBase.cpp b/Source/Core/VideoCommon/Src/TextureCacheBase.cpp index 9908ec7c826b..8555bd736d03 100644 --- a/Source/Core/VideoCommon/Src/TextureCacheBase.cpp +++ b/Source/Core/VideoCommon/Src/TextureCacheBase.cpp @@ -634,40 +634,40 @@ void TextureCache::CopyRenderTargetToTexture(u32 dstAddr, unsigned int dstFormat case 3: // Z16 colmat[1] = colmat[5] = colmat[9] = colmat[12] = 1.0f; - cbufid = 24; + cbufid = 2; break; case 11: // Z16 (reverse order) colmat[0] = colmat[4] = colmat[8] = colmat[13] = 1.0f; - cbufid = 2; + cbufid = 3; break; case 6: // Z24X8 colmat[0] = colmat[5] = colmat[10] = 1.0f; - cbufid = 3; + cbufid = 4; break; case 9: // Z8M colmat[1] = colmat[5] = colmat[9] = colmat[13] = 1.0f; - cbufid = 4; + cbufid = 5; break; case 10: // Z8L colmat[2] = colmat[6] = colmat[10] = colmat[14] = 1.0f; - cbufid = 5; + cbufid = 6; break; case 12: // Z16L - copy lower 16 depth bits // expected to be used as an IA8 texture (upper 8 bits stored as intensity, lower 8 bits stored as alpha) // Used e.g. in Zelda: Skyward Sword colmat[1] = colmat[5] = colmat[9] = colmat[14] = 1.0f; - cbufid = 6; + cbufid = 7; break; default: ERROR_LOG(VIDEO, "Unknown copy zbuf format: 0x%x", dstFormat); colmat[2] = colmat[5] = colmat[8] = 1.0f; - cbufid = 7; + cbufid = 8; break; } } @@ -694,11 +694,11 @@ void TextureCache::CopyRenderTargetToTexture(u32 dstAddr, unsigned int dstFormat { ColorMask[0] = ColorMask[1] = ColorMask[2] = 15.0f; ColorMask[4] = ColorMask[5] = ColorMask[6] = 1.0f / 15.0f; - cbufid = 8; + cbufid = 9; } else { - cbufid = 9; + cbufid = 10; } } else// alpha @@ -708,11 +708,11 @@ void TextureCache::CopyRenderTargetToTexture(u32 dstAddr, unsigned int dstFormat { ColorMask[0] = ColorMask[1] = ColorMask[2] = ColorMask[3] = 15.0f; ColorMask[4] = ColorMask[5] = ColorMask[6] = ColorMask[7] = 1.0f / 15.0f; - cbufid = 10; + cbufid = 11; } else { - cbufid = 11; + cbufid = 12; } } @@ -721,7 +721,7 @@ void TextureCache::CopyRenderTargetToTexture(u32 dstAddr, unsigned int dstFormat default: ERROR_LOG(VIDEO, "Unknown copy intensity format: 0x%x", dstFormat); colmat[0] = colmat[5] = colmat[10] = colmat[15] = 1.0f; - cbufid = 23; + cbufid = 13; break; } } @@ -733,12 +733,12 @@ void TextureCache::CopyRenderTargetToTexture(u32 dstAddr, unsigned int dstFormat colmat[0] = colmat[4] = colmat[8] = colmat[12] = 1; ColorMask[0] = 15.0f; ColorMask[4] = 1.0f / 15.0f; - cbufid = 12; + cbufid = 14; break; case 1: // R8 case 8: // R8 colmat[0] = colmat[4] = colmat[8] = colmat[12] = 1; - cbufid = 13; + cbufid = 15; break; case 2: // RA4 @@ -746,55 +746,55 @@ void TextureCache::CopyRenderTargetToTexture(u32 dstAddr, unsigned int dstFormat ColorMask[0] = ColorMask[3] = 15.0f; ColorMask[4] = ColorMask[7] = 1.0f / 15.0f; - cbufid = 14; + cbufid = 16; if(!efbHasAlpha) { ColorMask[3] = 0.0f; fConstAdd[3] = 1.0f; - cbufid = 15; + cbufid = 17; } break; case 3: // RA8 colmat[0] = colmat[4] = colmat[8] = colmat[15] = 1.0f; - cbufid = 16; + cbufid = 18; if(!efbHasAlpha) { ColorMask[3] = 0.0f; fConstAdd[3] = 1.0f; - cbufid = 17; + cbufid = 19; } break; case 7: // A8 colmat[3] = colmat[7] = colmat[11] = colmat[15] = 1.0f; - cbufid = 18; + cbufid = 20; if(!efbHasAlpha) { ColorMask[3] = 0.0f; fConstAdd[0] = 1.0f; fConstAdd[1] = 1.0f; fConstAdd[2] = 1.0f; fConstAdd[3] = 1.0f; - cbufid = 19; + cbufid = 21; } break; case 9: // G8 colmat[1] = colmat[5] = colmat[9] = colmat[13] = 1.0f; - cbufid = 20; + cbufid = 22; break; case 10: // B8 colmat[2] = colmat[6] = colmat[10] = colmat[14] = 1.0f; - cbufid = 21; + cbufid = 23; break; case 11: // RG8 colmat[0] = colmat[4] = colmat[8] = colmat[13] = 1.0f; - cbufid = 22; + cbufid = 24; break; case 12: // GB8 colmat[1] = colmat[5] = colmat[9] = colmat[14] = 1.0f; - cbufid = 23; + cbufid = 25; break; case 4: // RGB565 @@ -804,7 +804,7 @@ void TextureCache::CopyRenderTargetToTexture(u32 dstAddr, unsigned int dstFormat ColorMask[1] = 63.0f; ColorMask[5] = 1.0f / 63.0f; fConstAdd[3] = 1.0f; // set alpha to 1 - cbufid = 24; + cbufid = 26; break; case 5: // RGB5A3 @@ -814,28 +814,28 @@ void TextureCache::CopyRenderTargetToTexture(u32 dstAddr, unsigned int dstFormat ColorMask[3] = 7.0f; ColorMask[7] = 1.0f / 7.0f; - cbufid = 25; + cbufid = 27; if(!efbHasAlpha) { ColorMask[3] = 0.0f; fConstAdd[3] = 1.0f; - cbufid = 26; + cbufid = 28; } break; case 6: // RGBA8 colmat[0] = colmat[5] = colmat[10] = colmat[15] = 1.0f; - cbufid = 27; + cbufid = 29; if(!efbHasAlpha) { ColorMask[3] = 0.0f; fConstAdd[3] = 1.0f; - cbufid = 28; + cbufid = 30; } break; default: ERROR_LOG(VIDEO, "Unknown copy color format: 0x%x", dstFormat); colmat[0] = colmat[5] = colmat[10] = colmat[15] = 1.0f; - cbufid = 29; + cbufid = 31; break; } } From eef2cddfd766cb4dfe7f5fea4dc388f34e65be02 Mon Sep 17 00:00:00 2001 From: Scott Mansell Date: Sun, 24 Nov 2013 17:15:11 +1300 Subject: [PATCH 104/202] Opengl: Fix opengl realxfb "macroblocking"/bluring issue. YUYV textures should NEVER be interpolated/filtered in RGB colour space. Use TexelFetch to always fetch an actual texture sample. issue 6503 --- Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index 0c1800381ea8..1bbb3d535ab8 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -85,7 +85,7 @@ void CreatePrograms() "COLOROUT(ocol0)\n" "void main()\n" "{\n" - " vec4 c0 = texture2DRect(samp9, uv0).rgba;\n" + " vec4 c0 = texelFetch(samp9, ivec2(uv0));\n" " float f = step(0.5, fract(uv0.x));\n" " float y = mix(c0.b, c0.r, f);\n" " float yComp = 1.164 * (y - 0.0625);\n" From 994426b3dc0e98c0bfdec727bcb3acbef90c95ef Mon Sep 17 00:00:00 2001 From: degasus Date: Sun, 24 Nov 2013 05:39:40 +0100 Subject: [PATCH 105/202] Opengl: fix real XFB sample positions (0,0) and (1,0) aren't accurate xfb sample positions. This fixes the image shift to the left and some blocking on higher IR. --- Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index 1bbb3d535ab8..3446a8ee196b 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -69,8 +69,8 @@ void CreatePrograms() "COLOROUT(ocol0)\n" "void main()\n" "{\n" - " vec3 c0 = texture2DRect(samp9, uv0).rgb;\n" - " vec3 c1 = texture2DRect(samp9, uv0 + vec2(1.0, 0.0)).rgb;\n" + " vec3 c0 = texture2DRect(samp9, uv0 - dFdx(uv0) * 0.25).rgb;\n" + " vec3 c1 = texture2DRect(samp9, uv0 + dFdx(uv0) * 0.25).rgb;\n" " vec3 c01 = (c0 + c1) * 0.5;\n" " vec3 y_const = vec3(0.257,0.504,0.098);\n" " vec3 u_const = vec3(-0.148,-0.291,0.439);\n" From 531f840720b90ecb22e813d4aa46a593c2669488 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Sun, 24 Nov 2013 07:45:42 +0000 Subject: [PATCH 106/202] Fix OpenGL ES 3 in the recent changes. texelFetch doesn't require the lod argument in desktop GLSL versions, but in GLSL ES 3 it is a required argument. --- Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index 3446a8ee196b..cb7fb92e9c75 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -85,7 +85,7 @@ void CreatePrograms() "COLOROUT(ocol0)\n" "void main()\n" "{\n" - " vec4 c0 = texelFetch(samp9, ivec2(uv0));\n" + " vec4 c0 = texelFetch(samp9, ivec2(uv0), 0);\n" " float f = step(0.5, fract(uv0.x));\n" " float y = mix(c0.b, c0.r, f);\n" " float yComp = 1.164 * (y - 0.0625);\n" From e6b35642dff3f78548120653a6836c671520f736 Mon Sep 17 00:00:00 2001 From: Scott Mansell Date: Sun, 24 Nov 2013 22:48:10 +1300 Subject: [PATCH 107/202] Fix Desktop GLSL versions in the recent changes. Seems OpenGL ES 3 Requires you must have an lod argument, while Desktop versions require you must not have a lod argument if you are using a Sampler2DRect (which doesn't do Mipmapping). --- Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index cb7fb92e9c75..98114feeca78 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -85,7 +85,11 @@ void CreatePrograms() "COLOROUT(ocol0)\n" "void main()\n" "{\n" +#ifdef USE_GLES3 " vec4 c0 = texelFetch(samp9, ivec2(uv0), 0);\n" +#else + " vec4 c0 = texelFetch(samp9, ivec2(uv0));\n" +#endif " float f = step(0.5, fract(uv0.x));\n" " float y = mix(c0.b, c0.r, f);\n" " float yComp = 1.164 * (y - 0.0625);\n" From 6f73162df404c13476f3815b075d493f5a72b4ab Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Sun, 24 Nov 2013 10:50:59 +0000 Subject: [PATCH 108/202] [ARM] Implement the Acid test in the JIT core. This test is currently broken in JIT64 since it uses cr instead of cr_fast. --- .../Core/Src/PowerPC/JitArm32/JitArm_Branch.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_Branch.cpp b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_Branch.cpp index c70a2b2f3de9..eac488a5b060 100644 --- a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_Branch.cpp +++ b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_Branch.cpp @@ -135,9 +135,11 @@ void JitArm::bx(UGeckoInstruction inst) else destination = js.compilerPC + SignExt26(inst.LI << 2); #ifdef ACID_TEST - // TODO: Not implemented yet. - //if (inst.LK) - //AND(32, M(&PowerPC::ppcState.cr), Imm32(~(0xFF000000))); + if (inst.LK) + { + MOV(R14, 0); + STRB(R14, R9, PPCSTATE_OFF(cr_fast[0])); + } #endif if (destination == js.compilerPC) { @@ -329,8 +331,11 @@ void JitArm::bclrx(UGeckoInstruction inst) // This below line can be used to prove that blr "eats flags" in practice. // This observation will let us do a lot of fun observations. #ifdef ACID_TEST - // TODO: Not yet implemented - // AND(32, M(&PowerPC::ppcState.cr), Imm32(~(0xFF000000))); + if (inst.LK) + { + MOV(R14, 0); + STRB(R14, R9, PPCSTATE_OFF(cr_fast[0])); + } #endif //MOV(32, R(EAX), M(&LR)); From b5bd2ba847562d1cc7494941899e3acf4d7e007b Mon Sep 17 00:00:00 2001 From: Scott Mansell Date: Mon, 25 Nov 2013 01:11:42 +1300 Subject: [PATCH 109/202] OpenGL: Enable filtering for EFB to Real XFB copies. This fixes Real XFB Jaggies in OpenGL on games which use yscaling, such as most PAL games. This fixes the last of the "Real XFB Macroblocking" issues for opengl, see issue #6503 --- Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index 98114feeca78..71ad6f42236a 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -351,7 +351,10 @@ void EncodeToRamYUYV(GLuint srcTexture, const TargetRectangle& sourceRc, u8* des s_rgbToYuyvProgram.Bind(); - EncodeToRamUsingShader(srcTexture, sourceRc, destAddr, dstWidth / 2, dstHeight, 0, false, false); + // We enable linear filtering, because the gamecube does filtering in the vertical direction when + // yscale is enabled. + // Otherwise we get jaggies when a game uses yscaling (most PAL games) + EncodeToRamUsingShader(srcTexture, sourceRc, destAddr, dstWidth / 2, dstHeight, 0, false, true); FramebufferManager::SetFramebuffer(0); TextureCache::DisableStage(0); g_renderer->RestoreAPIState(); From 12741f6406899f7e184cd2139c83e215c43a90f8 Mon Sep 17 00:00:00 2001 From: Scott Mansell Date: Mon, 25 Nov 2013 01:30:53 +1300 Subject: [PATCH 110/202] Add comments for anybody attempting accuracy improvements in the future. --- .../OGL/Src/TextureConverter.cpp | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index 71ad6f42236a..b51672b76360 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -62,6 +62,22 @@ static const char *VProgram = void CreatePrograms() { + /* TODO: Accuracy Improvements + * + * This shader doesn't really match what the gamecube does interally in the + * copy pipeline. + * 1. It uses Opengl's built in filtering when yscaling, someone could work + * out how the copypipeline does it's filtering and implement it correctly + * in this shader. + * 2. Deflickering isn't implemented, a futher filtering over 3 lines. + * Isn't really needed on non-interlaced monitors (and would lower quality; + * But hey, accuracy!) + * 3. Flipper's YUYV conversion implements a 3 pixel horozontal blur on the + * UV channels, centering the U channel on the Left pixel and the V channel + * on the Right pixel. + * The current implementation Centers both UV channels at the same place + * inbetween the two Pixels, and only blurs over these two pixels. + */ // Output is BGRA because that is slightly faster than RGBA. const char *FProgramRgbToYuyv = "uniform sampler2DRect samp9;\n" @@ -79,6 +95,13 @@ void CreatePrograms() " ocol0 = vec4(dot(c1,y_const),dot(c01,u_const),dot(c0,y_const),dot(c01, v_const)) + const3;\n" "}\n"; + /* TODO: Accuracy Improvements + * + * The YVYU to RGB conversion here matches the RGB to YUYV done above, but + * if a game modifies or adds images to the XFB then it should be using the + * same algorithm as the flipper, and could result in slight colour inaccuracies + * when run back through this shader. + */ const char *FProgramYuyvToRgb = "uniform sampler2DRect samp9;\n" "VARYIN vec2 uv0;\n" From d410fe7c96f63fe994fb18160c0b66e2ca4b3324 Mon Sep 17 00:00:00 2001 From: degasus Date: Sun, 24 Nov 2013 15:56:50 +0100 Subject: [PATCH 111/202] OpenGL: cleanup yuv2rgb (real xfb) workflow We neither scale nor render from subimages, so we by using gl_Position, we don't have to generate _any_ vertices for this converting. Also remove the glTexSubImage optimization as every driver does it when needed. But there are some workflows (eg on APU) where it's better to realloc this texture instead of a second memcpy or stall. --- .../OGL/Src/TextureConverter.cpp | 76 ++++--------------- 1 file changed, 15 insertions(+), 61 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index b51672b76360..56c1f2e484d9 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -28,8 +28,6 @@ using OGL::TextureCache; static GLuint s_texConvFrameBuffer = 0; static GLuint s_srcTexture = 0; // for decoding from RAM -static GLuint s_srcTextureWidth = 0; -static GLuint s_srcTextureHeight = 0; static GLuint s_dstTexture = 0; // for encoding to RAM const int renderBufferWidth = 1024; @@ -44,11 +42,8 @@ static SHADER s_encodingPrograms[NUM_ENCODING_PROGRAMS]; static GLuint s_encode_VBO = 0; static GLuint s_encode_VAO = 0; -static GLuint s_decode_VBO = 0; static GLuint s_decode_VAO = 0; static TargetRectangle s_cached_sourceRc; -static int s_cached_srcWidth = 0; -static int s_cached_srcHeight = 0; static const char *VProgram = "ATTRIN vec2 rawpos;\n" @@ -102,19 +97,28 @@ void CreatePrograms() * same algorithm as the flipper, and could result in slight colour inaccuracies * when run back through this shader. */ + const char *VProgramYuyvToRgb = + "void main()\n" + "{\n" + " vec2 rawpos = vec2(gl_VertexID&1, gl_VertexID&2);\n" + " gl_Position = vec4(rawpos*2.0-1.0, 0.0, 1.0);\n" + "}\n"; const char *FProgramYuyvToRgb = "uniform sampler2DRect samp9;\n" "VARYIN vec2 uv0;\n" "COLOROUT(ocol0)\n" "void main()\n" "{\n" + " ivec2 uv = ivec2(gl_FragCoord.xy);\n" #ifdef USE_GLES3 - " vec4 c0 = texelFetch(samp9, ivec2(uv0), 0);\n" + // We switch top/bottom here. TODO: move this to screen blit. + " ivec2 ts = textureSize(samp9, 0);\n" + " vec4 c0 = texelFetch(samp9, ivec2(uv.x/2, ts.y-uv.y-1), 0);\n" #else - " vec4 c0 = texelFetch(samp9, ivec2(uv0));\n" + " ivec2 ts = textureSize(samp9);\n" + " vec4 c0 = texelFetch(samp9, ivec2(uv.x/2, ts.y-uv.y-1));\n" #endif - " float f = step(0.5, fract(uv0.x));\n" - " float y = mix(c0.b, c0.r, f);\n" + " float y = mix(c0.b, c0.r, uv.x & 1);\n" " float yComp = 1.164 * (y - 0.0625);\n" " float uComp = c0.g - 0.5;\n" " float vComp = c0.a - 0.5;\n" @@ -125,7 +129,7 @@ void CreatePrograms() "}\n"; ProgramShaderCache::CompileShader(s_rgbToYuyvProgram, VProgram, FProgramRgbToYuyv); - ProgramShaderCache::CompileShader(s_yuyvToRgbProgram, VProgram, FProgramYuyvToRgb); + ProgramShaderCache::CompileShader(s_yuyvToRgbProgram, VProgramYuyvToRgb, FProgramYuyvToRgb); } SHADER &GetOrCreateEncodingShader(u32 format) @@ -173,19 +177,8 @@ void Init() s_cached_sourceRc.left = -1; s_cached_sourceRc.right = -1; - glGenBuffers(1, &s_decode_VBO ); glGenVertexArrays(1, &s_decode_VAO ); - glBindBuffer(GL_ARRAY_BUFFER, s_decode_VBO ); glBindVertexArray( s_decode_VAO ); - s_cached_srcWidth = -1; - s_cached_srcHeight = -1; - glEnableVertexAttribArray(SHADER_POSITION_ATTRIB); - glVertexAttribPointer(SHADER_POSITION_ATTRIB, 2, GL_FLOAT, 0, sizeof(GLfloat)*4, (GLfloat*)NULL); - glEnableVertexAttribArray(SHADER_TEXTURE0_ATTRIB); - glVertexAttribPointer(SHADER_TEXTURE0_ATTRIB, 2, GL_FLOAT, 0, sizeof(GLfloat)*4, (GLfloat*)NULL+2); - - s_srcTextureWidth = 0; - s_srcTextureHeight = 0; glActiveTexture(GL_TEXTURE0 + 9); glGenTextures(1, &s_srcTexture); @@ -207,7 +200,6 @@ void Shutdown() glDeleteFramebuffers(1, &s_texConvFrameBuffer); glDeleteBuffers(1, &s_encode_VBO ); glDeleteVertexArrays(1, &s_encode_VAO ); - glDeleteBuffers(1, &s_decode_VBO ); glDeleteVertexArrays(1, &s_decode_VAO ); s_rgbToYuyvProgram.Destroy(); @@ -395,8 +387,6 @@ void DecodeToTexture(u32 xfbAddr, int srcWidth, int srcHeight, GLuint destTextur return; } - int srcFmtWidth = srcWidth / 2; - g_renderer->ResetAPIState(); // reset any game specific settings // switch to texture converter frame buffer @@ -410,50 +400,14 @@ void DecodeToTexture(u32 xfbAddr, int srcWidth, int srcHeight, GLuint destTextur // set srcAddr as data for source texture glActiveTexture(GL_TEXTURE0+9); glBindTexture(getFbType(), s_srcTexture); - - // TODO: make this less slow. (How?) - if ((GLsizei)s_srcTextureWidth == (GLsizei)srcFmtWidth && (GLsizei)s_srcTextureHeight == (GLsizei)srcHeight) - { - glTexSubImage2D(getFbType(), 0,0,0,s_srcTextureWidth, s_srcTextureHeight, - GL_BGRA, GL_UNSIGNED_BYTE, srcAddr); - } - else - { - glTexImage2D(getFbType(), 0, GL_RGBA, (GLsizei)srcFmtWidth, (GLsizei)srcHeight, - 0, GL_BGRA, GL_UNSIGNED_BYTE, srcAddr); - s_srcTextureWidth = (GLsizei)srcFmtWidth; - s_srcTextureHeight = (GLsizei)srcHeight; - } + glTexImage2D(getFbType(), 0, GL_RGBA, srcWidth / 2, srcHeight, 0, GL_BGRA, GL_UNSIGNED_BYTE, srcAddr); glViewport(0, 0, srcWidth, srcHeight); s_yuyvToRgbProgram.Bind(); - GL_REPORT_ERRORD(); - - if(s_cached_srcHeight != srcHeight || s_cached_srcWidth != srcWidth) { - GLfloat vertices[] = { - 1.f, -1.f, - (float)srcFmtWidth, (float)srcHeight, - 1.f, 1.f, - (float)srcFmtWidth, 0.f, - -1.f, -1.f, - 0.f, (float)srcHeight, - -1.f, 1.f, - 0.f, 0.f - }; - - glBindBuffer(GL_ARRAY_BUFFER, s_decode_VBO ); - glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*4*4, vertices, GL_STREAM_DRAW); - - s_cached_srcHeight = srcHeight; - s_cached_srcWidth = srcWidth; - } - glBindVertexArray( s_decode_VAO ); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - GL_REPORT_ERRORD(); - FramebufferManager::SetFramebuffer(0); g_renderer->RestoreAPIState(); From f6f2b1fc60067b19068f2d0edbbfbfe5e73a0806 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Sun, 24 Nov 2013 15:04:53 -0600 Subject: [PATCH 112/202] [Android-overlay] Support multiple gamepads with touch screen controls. --- Source/Android/assets/GCPadNew.ini | 75 +++++++++++++++++++ .../dolphinemu/dolphinemu/NativeLibrary.java | 10 ++- .../emulation/overlay/InputOverlay.java | 8 +- .../DolphinWX/Src/Android/ButtonManager.cpp | 67 +++++++++-------- .../DolphinWX/Src/Android/ButtonManager.h | 8 +- Source/Core/DolphinWX/Src/MainAndroid.cpp | 8 +- .../ControllerInterface/Android/Android.cpp | 49 ++++++------ .../Src/ControllerInterface/Android/Android.h | 16 ++-- 8 files changed, 164 insertions(+), 77 deletions(-) diff --git a/Source/Android/assets/GCPadNew.ini b/Source/Android/assets/GCPadNew.ini index 7e770f60d33c..77e790d0fa6d 100644 --- a/Source/Android/assets/GCPadNew.ini +++ b/Source/Android/assets/GCPadNew.ini @@ -25,5 +25,80 @@ D-Pad/Down = `Button 7` D-Pad/Left = `Button 8` D-Pad/Right = `Button 9` [GCPad2] +Device = Android/1/Touchscreen +Buttons/A = `Button 0` +Buttons/B = `Button 1` +Buttons/X = `Button 3` +Buttons/Y = `Button 4` +Buttons/Z = `Button 5` +Buttons/Start = `Button 2` +Main Stick/Up = `Axis 11` +Main Stick/Down = `Axis 12` +Main Stick/Left = `Axis 13` +Main Stick/Right = `Axis 14` +Main Stick/Modifier = Shift_L +Main Stick/Modifier/Range = 50.000000 +C-Stick/Up = `Axis 15` +C-Stick/Down = `Axis 16` +C-Stick/Left = `Axis 17` +C-Stick/Right = `Axis 18` +C-Stick/Modifier = Control_L +C-Stick/Modifier/Range = 50.000000 +Triggers/L = `Axis 18` +Triggers/R = `Axis 19` +D-Pad/Up = `Button 6` +D-Pad/Down = `Button 7` +D-Pad/Left = `Button 8` +D-Pad/Right = `Button 9` [GCPad3] +Device = Android/2/Touchscreen +Buttons/A = `Button 0` +Buttons/B = `Button 1` +Buttons/X = `Button 3` +Buttons/Y = `Button 4` +Buttons/Z = `Button 5` +Buttons/Start = `Button 2` +Main Stick/Up = `Axis 11` +Main Stick/Down = `Axis 12` +Main Stick/Left = `Axis 13` +Main Stick/Right = `Axis 14` +Main Stick/Modifier = Shift_L +Main Stick/Modifier/Range = 50.000000 +C-Stick/Up = `Axis 15` +C-Stick/Down = `Axis 16` +C-Stick/Left = `Axis 17` +C-Stick/Right = `Axis 18` +C-Stick/Modifier = Control_L +C-Stick/Modifier/Range = 50.000000 +Triggers/L = `Axis 18` +Triggers/R = `Axis 19` +D-Pad/Up = `Button 6` +D-Pad/Down = `Button 7` +D-Pad/Left = `Button 8` +D-Pad/Right = `Button 9` [GCPad4] +Device = Android/3/Touchscreen +Buttons/A = `Button 0` +Buttons/B = `Button 1` +Buttons/X = `Button 3` +Buttons/Y = `Button 4` +Buttons/Z = `Button 5` +Buttons/Start = `Button 2` +Main Stick/Up = `Axis 11` +Main Stick/Down = `Axis 12` +Main Stick/Left = `Axis 13` +Main Stick/Right = `Axis 14` +Main Stick/Modifier = Shift_L +Main Stick/Modifier/Range = 50.000000 +C-Stick/Up = `Axis 15` +C-Stick/Down = `Axis 16` +C-Stick/Left = `Axis 17` +C-Stick/Right = `Axis 18` +C-Stick/Modifier = Control_L +C-Stick/Modifier/Range = 50.000000 +Triggers/L = `Axis 18` +Triggers/R = `Axis 19` +D-Pad/Up = `Button 6` +D-Pad/Down = `Button 7` +D-Pad/Left = `Button 8` +D-Pad/Right = `Button 9` diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java index 7745deadbac6..ac6275301d88 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/NativeLibrary.java @@ -56,19 +56,21 @@ public static final class ButtonState /** * Handles touch events. * - * @param Button Key code identifying which button was pressed. + * @param padID Identifier for which GCpad 0-3, + * @param Button Key code identifying which button was pressed, * @param Action Mask for the action being performed. */ - public static native void onTouchEvent(int Button, int Action); + public static native void onTouchEvent(int padID, int Button, int Action); /** * Handles axis-related touch events. * - * @param Axis Axis ID for the type of axis being altered. (Example: Main stick up, down, left, right, etc). + * @param padID Identifier for which GCpad 0-3, + * @param Axis Axis ID for the type of axis being altered. (Example: Main stick up, down, left, right, etc), * @param force How 'far down' the joystick is pushed down. 0.0f indicates center (or no force), * 1.0f indicates max force (or joystick pushed all the way down in any arbitrary direction). */ - public static native void onTouchAxisEvent(int Axis, float force); + public static native void onTouchAxisEvent(int padID, int Axis, float force); /** * Handles button press events for a gamepad. diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java index 26f628ed46ce..4f34df7dc64d 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlay.java @@ -96,15 +96,15 @@ public boolean onTouch(View v, MotionEvent event) switch (button.getId()) { case ButtonType.BUTTON_A: - NativeLibrary.onTouchEvent(ButtonType.BUTTON_A, buttonState); + NativeLibrary.onTouchEvent(0, ButtonType.BUTTON_A, buttonState); break; case ButtonType.BUTTON_B: - NativeLibrary.onTouchEvent(ButtonType.BUTTON_B, buttonState); + NativeLibrary.onTouchEvent(0, ButtonType.BUTTON_B, buttonState); break; case ButtonType.BUTTON_START: - NativeLibrary.onTouchEvent(ButtonType.BUTTON_START, buttonState); + NativeLibrary.onTouchEvent(0, ButtonType.BUTTON_START, buttonState); break; default: @@ -121,7 +121,7 @@ public boolean onTouch(View v, MotionEvent event) for (int a = 0; a < 4; ++a) { - NativeLibrary.onTouchAxisEvent(axisIDs[a], axises[a]); + NativeLibrary.onTouchAxisEvent(0, axisIDs[a], axises[a]); } } diff --git a/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp b/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp index 314cdb8c8375..b33148338d43 100644 --- a/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp +++ b/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp @@ -24,8 +24,9 @@ extern void DrawButton(GLuint tex, float *coords); namespace ButtonManager { - std::unordered_map m_buttons; - std::unordered_map m_axises; + // Pair key is padID, BUTTONTYPE + std::map, Button*> m_buttons; + std::map, Axis*> m_axises; std::unordered_map m_controllers; const char *configStrings[] = { "InputA", "InputB", @@ -64,28 +65,30 @@ namespace ButtonManager void Init() { // Initialize our touchscreen buttons - m_buttons[BUTTON_A] = new Button(); - m_buttons[BUTTON_B] = new Button(); - m_buttons[BUTTON_START] = new Button(); - m_buttons[BUTTON_X] = new Button(); - m_buttons[BUTTON_Y] = new Button(); - m_buttons[BUTTON_Z] = new Button(); - m_buttons[BUTTON_UP] = new Button(); - m_buttons[BUTTON_DOWN] = new Button(); - m_buttons[BUTTON_LEFT] = new Button(); - m_buttons[BUTTON_RIGHT] = new Button(); - - m_axises[STICK_MAIN_UP] = new Axis(); - m_axises[STICK_MAIN_DOWN] = new Axis(); - m_axises[STICK_MAIN_LEFT] = new Axis(); - m_axises[STICK_MAIN_RIGHT] = new Axis(); - m_axises[STICK_C_UP] = new Axis(); - m_axises[STICK_C_DOWN] = new Axis(); - m_axises[STICK_C_LEFT] = new Axis(); - m_axises[STICK_C_RIGHT] = new Axis(); - m_axises[TRIGGER_L] = new Axis(); - m_axises[TRIGGER_R] = new Axis(); - + for (int a = 0; a < 4; ++a) + { + m_buttons[std::make_pair(a, BUTTON_A)] = new Button(); + m_buttons[std::make_pair(a, BUTTON_B)] = new Button(); + m_buttons[std::make_pair(a, BUTTON_START)] = new Button(); + m_buttons[std::make_pair(a, BUTTON_X)] = new Button(); + m_buttons[std::make_pair(a, BUTTON_Y)] = new Button(); + m_buttons[std::make_pair(a, BUTTON_Z)] = new Button(); + m_buttons[std::make_pair(a, BUTTON_UP)] = new Button(); + m_buttons[std::make_pair(a, BUTTON_DOWN)] = new Button(); + m_buttons[std::make_pair(a, BUTTON_LEFT)] = new Button(); + m_buttons[std::make_pair(a, BUTTON_RIGHT)] = new Button(); + + m_axises[std::make_pair(a, STICK_MAIN_UP)] = new Axis(); + m_axises[std::make_pair(a, STICK_MAIN_DOWN)] = new Axis(); + m_axises[std::make_pair(a, STICK_MAIN_LEFT)] = new Axis(); + m_axises[std::make_pair(a, STICK_MAIN_RIGHT)] = new Axis(); + m_axises[std::make_pair(a, STICK_C_UP)] = new Axis(); + m_axises[std::make_pair(a, STICK_C_DOWN)] = new Axis(); + m_axises[std::make_pair(a, STICK_C_LEFT)] = new Axis(); + m_axises[std::make_pair(a, STICK_C_RIGHT)] = new Axis(); + m_axises[std::make_pair(a, TRIGGER_L)] = new Axis(); + m_axises[std::make_pair(a, TRIGGER_R)] = new Axis(); + } // Init our controller bindings IniFile ini; ini.Load(File::GetUserPath(D_CONFIG_IDX) + std::string("Dolphin.ini")); @@ -117,33 +120,33 @@ namespace ButtonManager } } - bool GetButtonPressed(ButtonType button) + bool GetButtonPressed(int padID, ButtonType button) { bool pressed = false; - pressed = m_buttons[button]->Pressed(); + pressed = m_buttons[std::make_pair(padID, button)]->Pressed(); for (auto it = m_controllers.begin(); it != m_controllers.end(); ++it) pressed |= it->second->ButtonValue(button); return pressed; } - float GetAxisValue(ButtonType axis) + float GetAxisValue(int padID, ButtonType axis) { float value = 0.0f; - value = m_axises[axis]->AxisValue(); + value = m_axises[std::make_pair(padID, axis)]->AxisValue(); auto it = m_controllers.begin(); if (it == m_controllers.end()) return value; return it->second->AxisValue(axis); } - void TouchEvent(int button, int action) + void TouchEvent(int padID, int button, int action) { - m_buttons[button]->SetState(action ? BUTTON_PRESSED : BUTTON_RELEASED); + m_buttons[std::make_pair(padID, button)]->SetState(action ? BUTTON_PRESSED : BUTTON_RELEASED); } - void TouchAxisEvent(int axis, float value) + void TouchAxisEvent(int padID, int axis, float value) { - m_axises[axis]->SetValue(value); + m_axises[std::make_pair(padID, axis)]->SetValue(value); } void GamepadEvent(std::string dev, int button, int action) { diff --git a/Source/Core/DolphinWX/Src/Android/ButtonManager.h b/Source/Core/DolphinWX/Src/Android/ButtonManager.h index 383e52f4455b..f94b3596bcf6 100644 --- a/Source/Core/DolphinWX/Src/Android/ButtonManager.h +++ b/Source/Core/DolphinWX/Src/Android/ButtonManager.h @@ -120,10 +120,10 @@ namespace ButtonManager }; void Init(); - bool GetButtonPressed(ButtonType button); - float GetAxisValue(ButtonType axis); - void TouchEvent(int button, int action); - void TouchAxisEvent(int axis, float value); + bool GetButtonPressed(int padID, ButtonType button); + float GetAxisValue(int padID, ButtonType axis); + void TouchEvent(int padID, int button, int action); + void TouchAxisEvent(int padID, int axis, float value); void GamepadEvent(std::string dev, int button, int action); void GamepadAxisEvent(std::string dev, int axis, float value); void Shutdown(); diff --git a/Source/Core/DolphinWX/Src/MainAndroid.cpp b/Source/Core/DolphinWX/Src/MainAndroid.cpp index 0d75d91e2d99..7382aba219b5 100644 --- a/Source/Core/DolphinWX/Src/MainAndroid.cpp +++ b/Source/Core/DolphinWX/Src/MainAndroid.cpp @@ -237,13 +237,13 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_StopEmulatio Core::Stop(); updateMainFrameEvent.Set(); // Kick the waiting event } -JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onTouchEvent(JNIEnv *env, jobject obj, jint Button, jint Action) +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onTouchEvent(JNIEnv *env, jobject obj, jint padID, jint Button, jint Action) { - ButtonManager::TouchEvent(Button, Action); + ButtonManager::TouchEvent(padID, Button, Action); } -JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onTouchAxisEvent(JNIEnv *env, jobject obj, jint Button, jfloat Action) +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onTouchAxisEvent(JNIEnv *env, jobject obj, jint padID, jint Button, jfloat Action) { - ButtonManager::TouchAxisEvent(Button, Action); + ButtonManager::TouchAxisEvent(padID, Button, Action); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onGamePadEvent(JNIEnv *env, jobject obj, jstring jDevice, jint Button, jint Action) { diff --git a/Source/Core/InputCommon/Src/ControllerInterface/Android/Android.cpp b/Source/Core/InputCommon/Src/ControllerInterface/Android/Android.cpp index ef95f9b36367..ec7a7a2a5654 100644 --- a/Source/Core/InputCommon/Src/ControllerInterface/Android/Android.cpp +++ b/Source/Core/InputCommon/Src/ControllerInterface/Android/Android.cpp @@ -25,7 +25,10 @@ namespace Android void Init( std::vector& devices ) { - devices.push_back(new Touchscreen()); + devices.push_back(new Touchscreen(0)); + devices.push_back(new Touchscreen(1)); + devices.push_back(new Touchscreen(2)); + devices.push_back(new Touchscreen(3)); } // Touchscreens and stuff @@ -43,49 +46,49 @@ int Touchscreen::GetId() const { return 0; } -Touchscreen::Touchscreen() +Touchscreen::Touchscreen(int padID) + : _padID(padID) { - AddInput(new Button(ButtonManager::BUTTON_A)); - AddInput(new Button(ButtonManager::BUTTON_B)); - AddInput(new Button(ButtonManager::BUTTON_START)); - AddInput(new Button(ButtonManager::BUTTON_X)); - AddInput(new Button(ButtonManager::BUTTON_Y)); - AddInput(new Button(ButtonManager::BUTTON_Z)); - AddInput(new Button(ButtonManager::BUTTON_UP)); - AddInput(new Button(ButtonManager::BUTTON_DOWN)); - AddInput(new Button(ButtonManager::BUTTON_LEFT)); - AddInput(new Button(ButtonManager::BUTTON_RIGHT)); - AddAnalogInputs(new Axis(ButtonManager::STICK_MAIN_LEFT), new Axis(ButtonManager::STICK_MAIN_RIGHT)); - AddAnalogInputs(new Axis(ButtonManager::STICK_MAIN_UP), new Axis(ButtonManager::STICK_MAIN_DOWN)); - AddAnalogInputs(new Axis(ButtonManager::STICK_C_UP), new Axis(ButtonManager::STICK_C_DOWN)); - AddAnalogInputs(new Axis(ButtonManager::STICK_C_LEFT), new Axis(ButtonManager::STICK_C_RIGHT)); - AddAnalogInputs(new Axis(ButtonManager::TRIGGER_L), new Axis(ButtonManager::TRIGGER_L)); - AddAnalogInputs(new Axis(ButtonManager::TRIGGER_R), new Axis(ButtonManager::TRIGGER_R)); - + AddInput(new Button(_padID, ButtonManager::BUTTON_A)); + AddInput(new Button(_padID, ButtonManager::BUTTON_B)); + AddInput(new Button(_padID, ButtonManager::BUTTON_START)); + AddInput(new Button(_padID, ButtonManager::BUTTON_X)); + AddInput(new Button(_padID, ButtonManager::BUTTON_Y)); + AddInput(new Button(_padID, ButtonManager::BUTTON_Z)); + AddInput(new Button(_padID, ButtonManager::BUTTON_UP)); + AddInput(new Button(_padID, ButtonManager::BUTTON_DOWN)); + AddInput(new Button(_padID, ButtonManager::BUTTON_LEFT)); + AddInput(new Button(_padID, ButtonManager::BUTTON_RIGHT)); + AddAnalogInputs(new Axis(_padID, ButtonManager::STICK_MAIN_LEFT), new Axis(_padID, ButtonManager::STICK_MAIN_RIGHT)); + AddAnalogInputs(new Axis(_padID, ButtonManager::STICK_MAIN_UP), new Axis(_padID, ButtonManager::STICK_MAIN_DOWN)); + AddAnalogInputs(new Axis(_padID, ButtonManager::STICK_C_UP), new Axis(_padID, ButtonManager::STICK_C_DOWN)); + AddAnalogInputs(new Axis(_padID, ButtonManager::STICK_C_LEFT), new Axis(_padID, ButtonManager::STICK_C_RIGHT)); + AddAnalogInputs(new Axis(_padID, ButtonManager::TRIGGER_L), new Axis(_padID, ButtonManager::TRIGGER_L)); + AddAnalogInputs(new Axis(_padID, ButtonManager::TRIGGER_R), new Axis(_padID, ButtonManager::TRIGGER_R)); } // Buttons and stuff std::string Touchscreen::Button::GetName() const { std::ostringstream ss; - ss << "Button " << (int)m_index; + ss << "Button " << (int)_index; return ss.str(); } ControlState Touchscreen::Button::GetState() const { - return ButtonManager::GetButtonPressed(m_index); + return ButtonManager::GetButtonPressed(_padID, _index); } std::string Touchscreen::Axis::GetName() const { std::ostringstream ss; - ss << "Axis " << (int)m_index; + ss << "Axis " << (int)_index; return ss.str(); } ControlState Touchscreen::Axis::GetState() const { - return ButtonManager::GetAxisValue(m_index); + return ButtonManager::GetAxisValue(_padID, _index); } } diff --git a/Source/Core/InputCommon/Src/ControllerInterface/Android/Android.h b/Source/Core/InputCommon/Src/ControllerInterface/Android/Android.h index e38c6a221a78..8b1fd99c23bb 100644 --- a/Source/Core/InputCommon/Src/ControllerInterface/Android/Android.h +++ b/Source/Core/InputCommon/Src/ControllerInterface/Android/Android.h @@ -33,31 +33,35 @@ class Touchscreen : public Core::Device { public: std::string GetName() const; - Button(ButtonManager::ButtonType index) : m_index(index) {} + Button(int padID, ButtonManager::ButtonType index) : _padID(padID), _index(index) {} ControlState GetState() const; private: - const ButtonManager::ButtonType m_index; + const int _padID; + const ButtonManager::ButtonType _index; }; class Axis : public Input { public: std::string GetName() const; - Axis(ButtonManager::ButtonType index) : m_index(index) {} + Axis(int padID, ButtonManager::ButtonType index) : _padID(padID), _index(index) {} ControlState GetState() const; private: - const ButtonManager::ButtonType m_index; + const int _padID; + const ButtonManager::ButtonType _index; }; - + public: bool UpdateInput() { return true; } bool UpdateOutput() { return true; } - Touchscreen(); + Touchscreen(int padID); ~Touchscreen() {} std::string GetName() const; int GetId() const; std::string GetSource() const; +private: + const int _padID; }; } From f292819ff5d9017094c8d2debca2e4a93add448c Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Sun, 24 Nov 2013 15:49:23 -0600 Subject: [PATCH 113/202] [Android] Due to recent changes in code breaking Tegra 4 support, and also the upcoming code which will be breaking GLES2 support entirely. Taking the initiative to drop the remaining support code from the codebase in preparation for the upcoming changes. For a look at how Dolphin on Tegra 4 looked like prior and would not have been able to be fixed at all due to Tegra 4 not supporting the precision we need in our shaders; Look at this Youtube video http://youtu.be/Ga7Jc_Ote7U --- .../settings/VideoSettingsFragment.java | 8 -- .../OGL/Src/FramebufferManager.cpp | 35 +------- .../VideoBackends/OGL/Src/GLFunctions.cpp | 79 +++++++------------ .../OGL/Src/ProgramShaderCache.cpp | 40 +++------- Source/Core/VideoBackends/OGL/Src/Render.cpp | 5 +- Source/Core/VideoBackends/OGL/Src/Render.h | 1 - .../VideoBackends/OGL/Src/SamplerCache.cpp | 5 +- .../VideoBackends/OGL/Src/VertexManager.cpp | 18 ----- Source/Core/VideoCommon/Src/DriverDetails.cpp | 2 - Source/Core/VideoCommon/Src/DriverDetails.h | 14 ---- 10 files changed, 42 insertions(+), 165 deletions(-) diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/VideoSettingsFragment.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/VideoSettingsFragment.java index b88ea07afe7e..e181fc32bc9a 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/VideoSettingsFragment.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/VideoSettingsFragment.java @@ -182,14 +182,6 @@ public static boolean SupportsGLES3() mSupportsGLES3 = true; } } - if (!mSupportsGLES3 && - m_GLVendor != null && m_GLVendor.equals("NVIDIA Corporation") && - m_GLRenderer != null && m_GLRenderer.equals("NVIDIA Tegra") && - m_GLExtensions != null && m_GLExtensions.contains("GL_OES_depth24")) - { - // Is a Tegra 4 since it supports 24bit depth - mSupportsGLES3 = true; - } return mSupportsGLES3; } diff --git a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp index e2e4d873ddcb..15300163364e 100644 --- a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp +++ b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp @@ -66,15 +66,6 @@ FramebufferManager::FramebufferManager(int targetWidth, int targetHeight, int ms // alpha channel should be ignored if the EFB does not have one. // Create EFB target. - u32 depthType; - if (DriverDetails::HasBug(DriverDetails::BUG_ISTEGRA)) - { - depthType = GL_DEPTH_COMPONENT; - } - else - { - depthType = GL_DEPTH_COMPONENT24; - } glGenFramebuffers(1, &m_efbFramebuffer); glActiveTexture(GL_TEXTURE0 + 9); @@ -94,7 +85,7 @@ FramebufferManager::FramebufferManager(int targetWidth, int targetHeight, int ms glBindTexture(getFbType(), m_efbDepth); glTexParameteri(getFbType(), GL_TEXTURE_MAX_LEVEL, 0); - glTexImage2D(getFbType(), 0, depthType, m_targetWidth, m_targetHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL); + glTexImage2D(getFbType(), 0, GL_DEPTH_COMPONENT24, m_targetWidth, m_targetHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL); glBindTexture(getFbType(), m_resolvedColorTexture); glTexParameteri(getFbType(), GL_TEXTURE_MAX_LEVEL, 0); @@ -159,7 +150,7 @@ FramebufferManager::FramebufferManager(int targetWidth, int targetHeight, int ms glBindTexture(getFbType(), m_resolvedDepthTexture); glTexParameteri(getFbType(), GL_TEXTURE_MAX_LEVEL, 0); - glTexImage2D(getFbType(), 0, depthType, m_targetWidth, m_targetHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL); + glTexImage2D(getFbType(), 0, GL_DEPTH_COMPONENT24, m_targetWidth, m_targetHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL); // Bind resolved textures to resolved framebuffer. @@ -235,13 +226,8 @@ FramebufferManager::FramebufferManager(int targetWidth, int targetHeight, int ms " ocol0 = float4(dst6) / 63.f;\n" "}"; - if(g_ogl_config.eSupportedGLSLVersion != GLSLES2) - { - // HACK: This shaders aren't glsles2 compatible as glsles2 don't support bit operations - // it could be workaround by floor + frac + tons off additions, but I think it isn't worth - ProgramShaderCache::CompileShader(m_pixel_format_shaders[0], vs, ps_rgb8_to_rgba6); - ProgramShaderCache::CompileShader(m_pixel_format_shaders[1], vs, ps_rgba6_to_rgb8); - } + ProgramShaderCache::CompileShader(m_pixel_format_shaders[0], vs, ps_rgb8_to_rgba6); + ProgramShaderCache::CompileShader(m_pixel_format_shaders[1], vs, ps_rgba6_to_rgb8); } FramebufferManager::~FramebufferManager() @@ -372,19 +358,6 @@ GLuint FramebufferManager::ResolveAndGetDepthTarget(const EFBRectangle &source_r void FramebufferManager::ReinterpretPixelData(unsigned int convtype) { - if(g_ogl_config.eSupportedGLSLVersion == GLSLES2) { - // This feature isn't supported by glsles2 - - // TODO: move this to InitBackendInfo - // We have to disable both the active and the stored config. Else we - // would either - // show this line per format change in one frame or - // once per frame. - OSD::AddMessage("Format Change Emulation isn't supported by your GPU.", 10000); - g_ActiveConfig.bEFBEmulateFormatChanges = false; - g_Config.bEFBEmulateFormatChanges = false; - return; - } g_renderer->ResetAPIState(); GLuint src_texture = 0; diff --git a/Source/Core/VideoBackends/OGL/Src/GLFunctions.cpp b/Source/Core/VideoBackends/OGL/Src/GLFunctions.cpp index 6ff264aebe59..c91908ea1161 100644 --- a/Source/Core/VideoBackends/OGL/Src/GLFunctions.cpp +++ b/Source/Core/VideoBackends/OGL/Src/GLFunctions.cpp @@ -72,67 +72,42 @@ namespace GLFunc self = dlopen(NULL, RTLD_LAZY); LoadFunction("glUnmapBuffer", (void**)&glUnmapBuffer); + LoadFunction("glBeginQuery", (void**)&glBeginQuery); + LoadFunction("glEndQuery", (void**)&glEndQuery); + LoadFunction("glGetQueryObjectuiv", (void**)&glGetQueryObjectuiv); + LoadFunction("glDeleteQueries", (void**)&glDeleteQueries); + LoadFunction("glGenQueries", (void**)&glGenQueries); - if (DriverDetails::HasBug(DriverDetails::BUG_ISTEGRA)) - { - LoadFunction("glBeginQueryEXT", (void**)&glBeginQuery); - LoadFunction("glEndQueryEXT", (void**)&glEndQuery); - LoadFunction("glGetQueryObjectuivEXT", (void**)&glGetQueryObjectuiv); - LoadFunction("glDeleteQueriesEXT", (void**)&glDeleteQueries); - LoadFunction("glGenQueriesEXT", (void**)&glGenQueries); - - LoadFunction("glMapBufferRangeNV", (void**)&glMapBufferRange); - LoadFunction("glBindBufferRangeNV", (void**)&glBindBufferRange); - LoadFunction("glBlitFramebufferNV", (void**)&glBlitFramebuffer); - - LoadFunction("glGenVertexArraysOES", (void**)&glGenVertexArrays); - LoadFunction("glDeleteVertexArraysOES", (void**)&glDeleteVertexArrays); - LoadFunction("glBindVertexArrayOES", (void**)&glBindVertexArray); + LoadFunction("glMapBufferRange", (void**)&glMapBufferRange); + LoadFunction("glBindBufferRange", (void**)&glBindBufferRange); + LoadFunction("glBlitFramebuffer", (void**)&glBlitFramebuffer); - LoadFunction("glRenderbufferStorageMultisampleNV", (void**)&glRenderbufferStorageMultisample); + LoadFunction("glGenVertexArrays", (void**)&glGenVertexArrays); + LoadFunction("glDeleteVertexArrays", (void**)&glDeleteVertexArrays); + LoadFunction("glBindVertexArray", (void**)&glBindVertexArray); - LoadFunction("glGetUniformBlockIndexNV", (void**)&glGetUniformBlockIndex); - LoadFunction("glUniformBlockBindingNV", (void**)&glUniformBlockBinding); - } - else - { - LoadFunction("glBeginQuery", (void**)&glBeginQuery); - LoadFunction("glEndQuery", (void**)&glEndQuery); - LoadFunction("glGetQueryObjectuiv", (void**)&glGetQueryObjectuiv); - LoadFunction("glDeleteQueries", (void**)&glDeleteQueries); - LoadFunction("glGenQueries", (void**)&glGenQueries); + LoadFunction("glClientWaitSync", (void**)&glClientWaitSync); + LoadFunction("glDeleteSync", (void**)&glDeleteSync); + LoadFunction("glFenceSync", (void**)&glFenceSync); - LoadFunction("glMapBufferRange", (void**)&glMapBufferRange); - LoadFunction("glBindBufferRange", (void**)&glBindBufferRange); - LoadFunction("glBlitFramebuffer", (void**)&glBlitFramebuffer); + LoadFunction("glSamplerParameterf", (void**)&glSamplerParameterf); + LoadFunction("glSamplerParameteri", (void**)&glSamplerParameteri); + LoadFunction("glSamplerParameterfv", (void**)&glSamplerParameterfv); + LoadFunction("glBindSampler", (void**)&glBindSampler); + LoadFunction("glDeleteSamplers", (void**)&glDeleteSamplers); + LoadFunction("glGenSamplers", (void**)&glGenSamplers); - LoadFunction("glGenVertexArrays", (void**)&glGenVertexArrays); - LoadFunction("glDeleteVertexArrays", (void**)&glDeleteVertexArrays); - LoadFunction("glBindVertexArray", (void**)&glBindVertexArray); + LoadFunction("glGetProgramBinary", (void**)&glGetProgramBinary); + LoadFunction("glProgramBinary", (void**)&glProgramBinary); + LoadFunction("glProgramParameteri", (void**)&glProgramParameteri); - LoadFunction("glClientWaitSync", (void**)&glClientWaitSync); - LoadFunction("glDeleteSync", (void**)&glDeleteSync); - LoadFunction("glFenceSync", (void**)&glFenceSync); + LoadFunction("glDrawRangeElements", (void**)&glDrawRangeElements); - LoadFunction("glSamplerParameterf", (void**)&glSamplerParameterf); - LoadFunction("glSamplerParameteri", (void**)&glSamplerParameteri); - LoadFunction("glSamplerParameterfv", (void**)&glSamplerParameterfv); - LoadFunction("glBindSampler", (void**)&glBindSampler); - LoadFunction("glDeleteSamplers", (void**)&glDeleteSamplers); - LoadFunction("glGenSamplers", (void**)&glGenSamplers); + LoadFunction("glRenderbufferStorageMultisample", (void**)&glRenderbufferStorageMultisample); - LoadFunction("glGetProgramBinary", (void**)&glGetProgramBinary); - LoadFunction("glProgramBinary", (void**)&glProgramBinary); - LoadFunction("glProgramParameteri", (void**)&glProgramParameteri); + LoadFunction("glGetUniformBlockIndex", (void**)&glGetUniformBlockIndex); + LoadFunction("glUniformBlockBinding", (void**)&glUniformBlockBinding); - LoadFunction("glDrawRangeElements", (void**)&glDrawRangeElements); - - LoadFunction("glRenderbufferStorageMultisample", (void**)&glRenderbufferStorageMultisample); - - LoadFunction("glGetUniformBlockIndex", (void**)&glGetUniformBlockIndex); - LoadFunction("glUniformBlockBinding", (void**)&glUniformBlockBinding); - - } dlclose(self); } } diff --git a/Source/Core/VideoBackends/OGL/Src/ProgramShaderCache.cpp b/Source/Core/VideoBackends/OGL/Src/ProgramShaderCache.cpp index cc807b79528e..f998c6ced285 100644 --- a/Source/Core/VideoBackends/OGL/Src/ProgramShaderCache.cpp +++ b/Source/Core/VideoBackends/OGL/Src/ProgramShaderCache.cpp @@ -571,12 +571,12 @@ void ProgramShaderCache::CreateHeader ( void ) "%s\n" // ubo "%s\n" // early-z - // Precision defines for GLSLES2/3 + // Precision defines for GLSLES3 "%s\n" "\n"// A few required defines and ones that will make our lives a lot easier - "#define ATTRIN %s\n" - "#define ATTROUT %s\n" + "#define ATTRIN in\n" + "#define ATTROUT out\n" "#define VARYIN %s\n" "#define VARYOUT %s\n" @@ -594,40 +594,18 @@ void ProgramShaderCache::CreateHeader ( void ) "%s\n" "%s\n" - // GLSLES2 hacks - "%s\n" - "%s\n" - "%s\n" - "%s\n" - "%s\n" - "%s\n" - "%s\n" - "#define COLOROUT(name) %s\n" - - - , v==GLSLES2 ? "" : v==GLSLES3 ? "#version 300 es" : v==GLSL_130 ? "#version 130" : v==GLSL_140 ? "#version 140" : "#version 150" + , v==GLSLES3 ? "#version 300 es" : v==GLSL_130 ? "#version 130" : v==GLSL_140 ? "#version 140" : "#version 150" , g_ActiveConfig.backend_info.bSupportsGLSLUBO && v 0) - { - glDrawElements(triangle_mode, triangle_index_size, GL_UNSIGNED_SHORT, (u8*)NULL+s_offset[0]); - INCSTAT(stats.thisFrame.numIndexedDrawCalls); - } - if (line_index_size > 0) - { - glDrawElements(GL_LINES, line_index_size, GL_UNSIGNED_SHORT, (u8*)NULL+s_offset[1]); - INCSTAT(stats.thisFrame.numIndexedDrawCalls); - } - if (point_index_size > 0) - { - glDrawElements(GL_POINTS, point_index_size, GL_UNSIGNED_SHORT, (u8*)NULL+s_offset[2]); - INCSTAT(stats.thisFrame.numIndexedDrawCalls); - } } else { if (triangle_index_size > 0) { diff --git a/Source/Core/VideoCommon/Src/DriverDetails.cpp b/Source/Core/VideoCommon/Src/DriverDetails.cpp index d2e8f2a059fc..3e975e09172a 100644 --- a/Source/Core/VideoCommon/Src/DriverDetails.cpp +++ b/Source/Core/VideoCommon/Src/DriverDetails.cpp @@ -39,8 +39,6 @@ namespace DriverDetails {VENDOR_MESA, DRIVER_I965, BUG_BROKENUBO, 900, 920, true}, {VENDOR_ATI, DRIVER_ATI, BUG_BROKENHACKEDBUFFER, -1.0, -1.0, true}, {VENDOR_MESA, DRIVER_NOUVEAU, BUG_BROKENHACKEDBUFFER, -1.0, -1.0, true}, - {VENDOR_TEGRA, DRIVER_NVIDIA, BUG_ISTEGRA, -1.0, -1.0, true}, - {VENDOR_IMGTEC, DRIVER_IMGTEC, BUG_ISPOWERVR, -1.0, -1.0, true}, }; std::map m_bugs; diff --git a/Source/Core/VideoCommon/Src/DriverDetails.h b/Source/Core/VideoCommon/Src/DriverDetails.h index 49af92084e97..0ddf097ea1df 100644 --- a/Source/Core/VideoCommon/Src/DriverDetails.h +++ b/Source/Core/VideoCommon/Src/DriverDetails.h @@ -116,20 +116,6 @@ namespace DriverDetails // Drawing on screen text causes the whole screen to swizzle in a terrible fashion // Clearing the framebuffer causes one to never see a frame. BUG_BROKENSWAP, - // Bug: Running on a Tegra 4 device - // Affected devices: Nvidia Tegra - // Started Version: 4 - // Ended Version: 5 - // Tegra 4 hardware limitations don't allow it to support OpenGL ES 3 - // This is fixed in Tegra 5 - BUG_ISTEGRA, - // Bug: Running on a PowerVR5 device - // Affected devices: PowerVR54x - // Started Version: 540 - // Ended Version: 6xxx - // PowerVR 5 hardware limitations don't allow it to support OpenGL ES 3 - // This is fixed in PowerVR6 - BUG_ISPOWERVR, // Bug: glBufferSubData/glMapBufferRange stalls + OOM // Affected devices: Adreno a3xx/Mali-t6xx // Started Version: -1 From 13578dc0b3d8797bc82d69d346c3af0d90345206 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Sun, 24 Nov 2013 16:28:20 -0600 Subject: [PATCH 114/202] [Android] Enable the ability to find OpenMP on Android...which isn't used in the generic texture decoder so no win. --- CMakeLists.txt | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d350fdeb2d30..6ea0510baf99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -332,6 +332,20 @@ add_definitions(-D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE) # externals. include(CheckLib) include(CheckCXXSourceRuns) + +if(OPENMP) + include(FindOpenMP OPTIONAL) + if(OPENMP_FOUND) + message("OpenMP parallelization enabled") + add_definitions("${OpenMP_CXX_FLAGS}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${OpenMP_CXX_FLAGS}") + endif() +endif() +if(NOT OPENMP_FOUND) + add_definitions(-Wno-unknown-pragmas) + message("OpenMP parallelization disabled") +endif() + if(NOT ANDROID) include(FindOpenGL) @@ -339,20 +353,7 @@ if(NOT ANDROID) if(NOT OPENGL_GLU_FOUND) message(FATAL_ERROR "GLU is required but not found") endif() - - if(OPENMP) - include(FindOpenMP OPTIONAL) - if(OPENMP_FOUND) - message("OpenMP parallelization enabled") - add_definitions("${OpenMP_CXX_FLAGS}") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${OpenMP_CXX_FLAGS}") - endif() - endif() - if(NOT OPENMP_FOUND) - add_definitions(-Wno-unknown-pragmas) - message("OpenMP parallelization disabled") - endif() - + include(FindALSA OPTIONAL) if(ALSA_FOUND) add_definitions(-DHAVE_ALSA=1) From 2c09e8fc5a2bb6ea358d779ce523d44e1b7dcdce Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Sun, 24 Nov 2013 16:43:53 -0600 Subject: [PATCH 115/202] [Android] Enable hard-float support. Requires Android NDK r9b. --- CMakeLists.txt | 4 ++++ Source/Core/DolphinWX/CMakeLists.txt | 1 + 2 files changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ea0510baf99..74c254b68b56 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -289,6 +289,10 @@ if(ANDROID) set(USE_WAYLAND 0) set(USE_UPNP 0) set(USE_GLES3 1) + if(ANDROID_NDK_ABI_NAME STREQUAL "armeabi-v7a") + message("Enabling hard-float") + add_definitions(-mhard-float) + endif() endif() # For now GLES and EGL are tied to each other. diff --git a/Source/Core/DolphinWX/CMakeLists.txt b/Source/Core/DolphinWX/CMakeLists.txt index 23fb8d64f7fe..a35f1ba1ce38 100644 --- a/Source/Core/DolphinWX/CMakeLists.txt +++ b/Source/Core/DolphinWX/CMakeLists.txt @@ -187,6 +187,7 @@ if(ANDROID) target_link_libraries(${DOLPHIN_EXE} log android + "-Wl,--no-warn-mismatch" "-Wl,--whole-archive" ${LIBS} "-Wl,--no-whole-archive" From da3eef1019fb9899cdde34d423ce19c4a093bf60 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Mon, 25 Nov 2013 00:06:29 +0000 Subject: [PATCH 116/202] Fix the issue with COLOROUT not being defined anymore. Fix a issue where Mali shader compiler is idiotic in finding an overload for the mix function. --- Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp | 4 ++-- Source/Core/VideoBackends/OGL/Src/RasterFont.cpp | 2 +- Source/Core/VideoBackends/OGL/Src/Render.cpp | 2 +- Source/Core/VideoBackends/OGL/Src/TextureCache.cpp | 4 ++-- Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp | 6 +++--- Source/Core/VideoCommon/Src/PixelShaderGen.cpp | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp index 15300163364e..1ce5e552a247 100644 --- a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp +++ b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp @@ -200,7 +200,7 @@ FramebufferManager::FramebufferManager(int targetWidth, int targetHeight, int ms char ps_rgba6_to_rgb8[] = "uniform sampler2DRect samp9;\n" - "COLOROUT(ocol0)\n" + "out vec4 ocol0;\n" "void main()\n" "{\n" " ivec4 src6 = ivec4(round(texture2DRect(samp9, gl_FragCoord.xy) * 63.f));\n" @@ -214,7 +214,7 @@ FramebufferManager::FramebufferManager(int targetWidth, int targetHeight, int ms char ps_rgb8_to_rgba6[] = "uniform sampler2DRect samp9;\n" - "COLOROUT(ocol0)\n" + "out vec4 ocol0;\n" "void main()\n" "{\n" " ivec4 src8 = ivec4(round(texture2DRect(samp9, gl_FragCoord.xy) * 255.f));\n" diff --git a/Source/Core/VideoBackends/OGL/Src/RasterFont.cpp b/Source/Core/VideoBackends/OGL/Src/RasterFont.cpp index 42b896378491..d3e4272d28f6 100644 --- a/Source/Core/VideoBackends/OGL/Src/RasterFont.cpp +++ b/Source/Core/VideoBackends/OGL/Src/RasterFont.cpp @@ -127,7 +127,7 @@ static const char *s_fragmentShaderSrc = "uniform sampler2D samp8;\n" "uniform vec4 color;\n" "VARYIN vec2 uv0;\n" - "COLOROUT(ocol0)\n" + "out vec4 ocol0;\n" "void main(void) {\n" " ocol0 = texture(samp8,uv0) * color;\n" "}\n"; diff --git a/Source/Core/VideoBackends/OGL/Src/Render.cpp b/Source/Core/VideoBackends/OGL/Src/Render.cpp index 8f95f5575e10..6dd86866fdad 100644 --- a/Source/Core/VideoBackends/OGL/Src/Render.cpp +++ b/Source/Core/VideoBackends/OGL/Src/Render.cpp @@ -665,7 +665,7 @@ void Renderer::Init() " c = vec4(color0, 1.0);\n" "}\n", "VARYIN vec4 c;\n" - "COLOROUT(ocol0)\n" + "out vec4 ocol0;\n" "void main(void) {\n" " ocol0 = c;\n" "}\n"); diff --git a/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp b/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp index eb2198ee7f41..aa197c293c01 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp @@ -406,7 +406,7 @@ TextureCache::TextureCache() "uniform sampler2DRect samp9;\n" "uniform vec4 colmat[7];\n" "VARYIN vec2 uv0;\n" - "COLOROUT(ocol0)\n" + "out vec4 ocol0;\n" "\n" "void main(){\n" " vec4 texcol = texture2DRect(samp9, uv0);\n" @@ -418,7 +418,7 @@ TextureCache::TextureCache() "uniform sampler2DRect samp9;\n" "uniform vec4 colmat[5];\n" "VARYIN vec2 uv0;\n" - "COLOROUT(ocol0)\n" + "out vec4 ocol0;\n" "\n" "void main(){\n" " vec4 texcol = texture2DRect(samp9, uv0);\n" diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index 56c1f2e484d9..f906d2219e4c 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -77,7 +77,7 @@ void CreatePrograms() const char *FProgramRgbToYuyv = "uniform sampler2DRect samp9;\n" "VARYIN vec2 uv0;\n" - "COLOROUT(ocol0)\n" + "out vec4 ocol0;\n" "void main()\n" "{\n" " vec3 c0 = texture2DRect(samp9, uv0 - dFdx(uv0) * 0.25).rgb;\n" @@ -106,7 +106,7 @@ void CreatePrograms() const char *FProgramYuyvToRgb = "uniform sampler2DRect samp9;\n" "VARYIN vec2 uv0;\n" - "COLOROUT(ocol0)\n" + "out vec4 ocol0;\n" "void main()\n" "{\n" " ivec2 uv = ivec2(gl_FragCoord.xy);\n" @@ -118,7 +118,7 @@ void CreatePrograms() " ivec2 ts = textureSize(samp9);\n" " vec4 c0 = texelFetch(samp9, ivec2(uv.x/2, ts.y-uv.y-1));\n" #endif - " float y = mix(c0.b, c0.r, uv.x & 1);\n" + " float y = mix(c0.b, c0.r, (uv.x & 1) == 1);\n" " float yComp = 1.164 * (y - 0.0625);\n" " float uComp = c0.g - 0.5;\n" " float vComp = c0.a - 0.5;\n" diff --git a/Source/Core/VideoCommon/Src/PixelShaderGen.cpp b/Source/Core/VideoCommon/Src/PixelShaderGen.cpp index f107da783e20..26e5cc0eba41 100644 --- a/Source/Core/VideoCommon/Src/PixelShaderGen.cpp +++ b/Source/Core/VideoCommon/Src/PixelShaderGen.cpp @@ -304,7 +304,7 @@ static inline void GeneratePixelShader(T& out, DSTALPHA_MODE dstAlphaMode, API_T if (ApiType == API_OPENGL) { - out.Write("COLOROUT(ocol0)\n"); + out.Write("out vec4 ocol0;\n"); if (dstAlphaMode == DSTALPHA_DUAL_SOURCE_BLEND) out.Write("out vec4 ocol1;\n"); From 7ed8e6a29c5f8a0f619104bc68b8854feda98fac Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Sun, 24 Nov 2013 22:45:48 -0600 Subject: [PATCH 117/202] [Android] Fix the check for the Qualcomm graphics driver version for v53 drivers with the screen being rotated 90 degrees. Initialize the OpenGL information grabbing only once. Check for v14 Qualcomm drivers and spit out an error if the user tries selecting OpenGL ES 3. --- Source/Android/res/values-ja/strings.xml | 1 + Source/Android/res/values/strings.xml | 1 + .../settings/VideoSettingsFragment.java | 56 +++++++++++++++---- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/Source/Android/res/values-ja/strings.xml b/Source/Android/res/values-ja/strings.xml index 77db4fee799c..db9c1854fd95 100644 --- a/Source/Android/res/values-ja/strings.xml +++ b/Source/Android/res/values-ja/strings.xml @@ -29,6 +29,7 @@ デバイスの互換性の警告 この電話は、NEON拡張をサポートしていません。 おそらくDolphinを実行することはできません。\nあなたはとにかくそれを実行してみますか? + デバイスはOpenGLES3のビデオドライバのバグがあります。\nあなたはとにかくそれを使用してみたいのですか? クリックされたファイル: %1$s diff --git a/Source/Android/res/values/strings.xml b/Source/Android/res/values/strings.xml index ca7157fd44cb..7ffc2bcfd205 100644 --- a/Source/Android/res/values/strings.xml +++ b/Source/Android/res/values/strings.xml @@ -29,6 +29,7 @@ Device Compatibility Warning Your phone doesn\'t support NEON which makes it incapable of running Dolphin Mobile?\nDo you want to try anyway? + Your device has known buggy video drivers for OpenGL ES 3.\nDo you want to try anyway? File clicked: %1$s diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/settings/VideoSettingsFragment.java b/Source/Android/src/org/dolphinemu/dolphinemu/settings/VideoSettingsFragment.java index e181fc32bc9a..81d4d2dad78d 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/settings/VideoSettingsFragment.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/settings/VideoSettingsFragment.java @@ -7,6 +7,8 @@ package org.dolphinemu.dolphinemu.settings; import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Bundle; @@ -29,6 +31,7 @@ public final class VideoSettingsFragment extends PreferenceFragment public static String m_GLRenderer; public static String m_GLExtensions; public static float m_QualcommVersion; + public static boolean m_Inited = false; private Activity m_activity; /** @@ -147,20 +150,24 @@ private EGLConfig chooseConfig() */ public static boolean SupportsGLES3() { - VersionCheck mbuffer = new VersionCheck(); - m_GLVersion = mbuffer.getVersion(); - m_GLVendor = mbuffer.getVendor(); - m_GLRenderer = mbuffer.getRenderer(); - m_GLExtensions = mbuffer.getExtensions(); - boolean mSupportsGLES3 = false; + if (!m_Inited) + { + VersionCheck mbuffer = new VersionCheck(); + m_GLVersion = mbuffer.getVersion(); + m_GLVendor = mbuffer.getVendor(); + m_GLRenderer = mbuffer.getRenderer(); + m_GLExtensions = mbuffer.getExtensions(); + m_Inited = true; + } + // Check for OpenGL ES 3 support (General case). if (m_GLVersion != null && m_GLVersion.contains("OpenGL ES 3.0")) mSupportsGLES3 = true; // Checking for OpenGL ES 3 support for certain Qualcomm devices. - if (!mSupportsGLES3 && m_GLVendor != null && m_GLVendor.equals("Qualcomm")) + if (m_GLVendor != null && m_GLVendor.equals("Qualcomm")) { if (m_GLRenderer.contains("Adreno (TM) 3")) { @@ -182,6 +189,7 @@ public static boolean SupportsGLES3() mSupportsGLES3 = true; } } + return mSupportsGLES3; } @@ -253,10 +261,36 @@ public void onSharedPreferenceChanged(SharedPreferences preference, String key) } else if (preference.getString(key, "Software Renderer").equals("OGL")) { - mainScreen.getPreference(0).setEnabled(true); - mainScreen.getPreference(1).setEnabled(true); - mainScreen.getPreference(3).setEnabled(true); - //mainScreen.getPreference(4).setEnabled(false); + // Create an alert telling them that their phone sucks + if (VideoSettingsFragment.SupportsGLES3() + && VideoSettingsFragment.m_GLVendor != null + && VideoSettingsFragment.m_GLVendor.equals("Qualcomm") + && VideoSettingsFragment.m_QualcommVersion == 14.0f) + { + AlertDialog.Builder builder = new AlertDialog.Builder(m_activity); + builder.setTitle(R.string.device_compat_warning); + builder.setMessage(R.string.device_gles3compat_warning_msg); + builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + mainScreen.getPreference(0).setEnabled(true); + mainScreen.getPreference(1).setEnabled(true); + mainScreen.getPreference(3).setEnabled(true); + //mainScreen.getPreference(4).setEnabled(false); + } + }); + builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) + { + // Get an editor. + SharedPreferences.Editor editor = sPrefs.edit(); + editor.putString("gpuPref", "Software Renderer"); + editor.commit(); + videoBackends.setValue("Software Renderer"); + videoBackends.setSummary("Software Renderer"); + } + }); + builder.show(); + } } } } From 230e12ae8c022a8e24ecea3019623a7c0ebcd3c7 Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 07:38:20 +0100 Subject: [PATCH 118/202] OpenGL: also remove VAO from xfb convertion We use attributeless rendering, so officially we have to bind _any_ VAO. As the state of this VAO doesn't matter, we don't have to switch it. Also fix an AMD issue as they don't like to render from an empty VAO. --- Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index f906d2219e4c..f653dfbf019f 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -42,7 +42,6 @@ static SHADER s_encodingPrograms[NUM_ENCODING_PROGRAMS]; static GLuint s_encode_VBO = 0; static GLuint s_encode_VAO = 0; -static GLuint s_decode_VAO = 0; static TargetRectangle s_cached_sourceRc; static const char *VProgram = @@ -177,9 +176,6 @@ void Init() s_cached_sourceRc.left = -1; s_cached_sourceRc.right = -1; - glGenVertexArrays(1, &s_decode_VAO ); - glBindVertexArray( s_decode_VAO ); - glActiveTexture(GL_TEXTURE0 + 9); glGenTextures(1, &s_srcTexture); glBindTexture(getFbType(), s_srcTexture); @@ -200,7 +196,6 @@ void Shutdown() glDeleteFramebuffers(1, &s_texConvFrameBuffer); glDeleteBuffers(1, &s_encode_VBO ); glDeleteVertexArrays(1, &s_encode_VAO ); - glDeleteVertexArrays(1, &s_decode_VAO ); s_rgbToYuyvProgram.Destroy(); s_yuyvToRgbProgram.Destroy(); @@ -405,7 +400,6 @@ void DecodeToTexture(u32 xfbAddr, int srcWidth, int srcHeight, GLuint destTextur glViewport(0, 0, srcWidth, srcHeight); s_yuyvToRgbProgram.Bind(); - glBindVertexArray( s_decode_VAO ); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); FramebufferManager::SetFramebuffer(0); From e8f23af10b8205daff8c348058cbb1abc8ee4c5e Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 08:32:41 +0100 Subject: [PATCH 119/202] OpenGL: always use texture2d as efb --- .../OGL/Src/FramebufferManager.cpp | 42 +++++++++---------- .../OGL/Src/FramebufferManager.h | 8 ---- .../VideoBackends/OGL/Src/TextureCache.cpp | 2 +- .../OGL/Src/TextureConverter.cpp | 18 ++++---- 4 files changed, 31 insertions(+), 39 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp index 1ce5e552a247..af130fc7aa54 100644 --- a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp +++ b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp @@ -79,24 +79,24 @@ FramebufferManager::FramebufferManager(int targetWidth, int targetHeight, int ms m_efbDepth = glObj[1]; m_resolvedColorTexture = glObj[2]; // needed for pixel format convertion - glBindTexture(getFbType(), m_efbColor); - glTexParameteri(getFbType(), GL_TEXTURE_MAX_LEVEL, 0); - glTexImage2D(getFbType(), 0, GL_RGBA, m_targetWidth, m_targetHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glBindTexture(GL_TEXTURE_2D, m_efbColor); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_targetWidth, m_targetHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); - glBindTexture(getFbType(), m_efbDepth); - glTexParameteri(getFbType(), GL_TEXTURE_MAX_LEVEL, 0); - glTexImage2D(getFbType(), 0, GL_DEPTH_COMPONENT24, m_targetWidth, m_targetHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL); + glBindTexture(GL_TEXTURE_2D, m_efbDepth); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, m_targetWidth, m_targetHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL); - glBindTexture(getFbType(), m_resolvedColorTexture); - glTexParameteri(getFbType(), GL_TEXTURE_MAX_LEVEL, 0); - glTexImage2D(getFbType(), 0, GL_RGBA, m_targetWidth, m_targetHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glBindTexture(GL_TEXTURE_2D, m_resolvedColorTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_targetWidth, m_targetHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); // Bind target textures to the EFB framebuffer. glBindFramebuffer(GL_FRAMEBUFFER, m_efbFramebuffer); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, getFbType(), m_efbColor, 0); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, getFbType(), m_efbDepth, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_efbColor, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_efbDepth, 0); GL_REPORT_FBO_ERROR(); } @@ -144,20 +144,20 @@ FramebufferManager::FramebufferManager(int targetWidth, int targetHeight, int ms m_resolvedColorTexture = glObj[0]; m_resolvedDepthTexture = glObj[1]; - glBindTexture(getFbType(), m_resolvedColorTexture); - glTexParameteri(getFbType(), GL_TEXTURE_MAX_LEVEL, 0); - glTexImage2D(getFbType(), 0, GL_RGBA, m_targetWidth, m_targetHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glBindTexture(GL_TEXTURE_2D, m_resolvedColorTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_targetWidth, m_targetHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); - glBindTexture(getFbType(), m_resolvedDepthTexture); - glTexParameteri(getFbType(), GL_TEXTURE_MAX_LEVEL, 0); - glTexImage2D(getFbType(), 0, GL_DEPTH_COMPONENT24, m_targetWidth, m_targetHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL); + glBindTexture(GL_TEXTURE_2D, m_resolvedDepthTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, m_targetWidth, m_targetHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL); // Bind resolved textures to resolved framebuffer. glBindFramebuffer(GL_FRAMEBUFFER, m_resolvedFramebuffer); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, getFbType(), m_resolvedColorTexture, 0); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, getFbType(), m_resolvedDepthTexture, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_resolvedColorTexture, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_resolvedDepthTexture, 0); GL_REPORT_FBO_ERROR(); @@ -386,11 +386,11 @@ void FramebufferManager::ReinterpretPixelData(unsigned int convtype) m_resolvedColorTexture = src_texture; // also switch them on fbo - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, getFbType(), m_efbColor, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_efbColor, 0); } glViewport(0,0, m_targetWidth, m_targetHeight); glActiveTexture(GL_TEXTURE0 + 9); - glBindTexture(getFbType(), src_texture); + glBindTexture(GL_TEXTURE_2D, src_texture); m_pixel_format_shaders[convtype ? 1 : 0].Bind(); glBindVertexArray(m_pixel_format_vao); diff --git a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.h b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.h index 7b93b239ae88..d3e32075deb9 100644 --- a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.h +++ b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.h @@ -57,14 +57,6 @@ struct XFBSource : public XFBSourceBase const GLuint texture; }; -inline GLenum getFbType() -{ -#ifndef USE_GLES3 - return GL_TEXTURE_RECTANGLE; -#endif - return GL_TEXTURE_2D; -} - class FramebufferManager : public FramebufferManagerBase { public: diff --git a/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp b/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp index aa197c293c01..070ffe24d074 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp @@ -296,7 +296,7 @@ void TextureCache::TCacheEntry::FromRenderTarget(u32 dstAddr, unsigned int dstFo GL_REPORT_ERRORD(); glActiveTexture(GL_TEXTURE0+9); - glBindTexture(getFbType(), read_texture); + glBindTexture(GL_TEXTURE_2D, read_texture); glViewport(0, 0, virtual_width, virtual_height); diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index f653dfbf019f..418e4048cd8f 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -178,8 +178,8 @@ void Init() glActiveTexture(GL_TEXTURE0 + 9); glGenTextures(1, &s_srcTexture); - glBindTexture(getFbType(), s_srcTexture); - glTexParameteri(getFbType(), GL_TEXTURE_MAX_LEVEL, 0); + glBindTexture(GL_TEXTURE_2D, s_srcTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); glGenTextures(1, &s_dstTexture); glBindTexture(GL_TEXTURE_2D, s_dstTexture); @@ -223,17 +223,17 @@ void EncodeToRamUsingShader(GLuint srcTexture, const TargetRectangle& sourceRc, // set source texture glActiveTexture(GL_TEXTURE0+9); - glBindTexture(getFbType(), srcTexture); + glBindTexture(GL_TEXTURE_2D, srcTexture); if (linearFilter) { - glTexParameteri(getFbType(), GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(getFbType(), GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); } else { - glTexParameteri(getFbType(), GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(getFbType(), GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); } GL_REPORT_ERRORD(); @@ -394,8 +394,8 @@ void DecodeToTexture(u32 xfbAddr, int srcWidth, int srcHeight, GLuint destTextur // activate source texture // set srcAddr as data for source texture glActiveTexture(GL_TEXTURE0+9); - glBindTexture(getFbType(), s_srcTexture); - glTexImage2D(getFbType(), 0, GL_RGBA, srcWidth / 2, srcHeight, 0, GL_BGRA, GL_UNSIGNED_BYTE, srcAddr); + glBindTexture(GL_TEXTURE_2D, s_srcTexture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, srcWidth / 2, srcHeight, 0, GL_BGRA, GL_UNSIGNED_BYTE, srcAddr); glViewport(0, 0, srcWidth, srcHeight); s_yuyvToRgbProgram.Bind(); From 1a3e790d9e85ec5aceb4cb54b085c6219ee8d87a Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 08:38:00 +0100 Subject: [PATCH 120/202] OpenGL: fix xfb for texture2d --- .../Core/VideoBackends/OGL/Src/TextureConverter.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index 418e4048cd8f..c5f96dba1467 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -74,13 +74,13 @@ void CreatePrograms() */ // Output is BGRA because that is slightly faster than RGBA. const char *FProgramRgbToYuyv = - "uniform sampler2DRect samp9;\n" + "uniform sampler2D samp9;\n" "VARYIN vec2 uv0;\n" "out vec4 ocol0;\n" "void main()\n" "{\n" - " vec3 c0 = texture2DRect(samp9, uv0 - dFdx(uv0) * 0.25).rgb;\n" - " vec3 c1 = texture2DRect(samp9, uv0 + dFdx(uv0) * 0.25).rgb;\n" + " vec3 c0 = texture(samp9, (uv0 - dFdx(uv0) * 0.25) / textureSize(samp9, 0)).rgb;\n" + " vec3 c1 = texture(samp9, (uv0 + dFdx(uv0) * 0.25) / textureSize(samp9, 0)).rgb;\n" " vec3 c01 = (c0 + c1) * 0.5;\n" " vec3 y_const = vec3(0.257,0.504,0.098);\n" " vec3 u_const = vec3(-0.148,-0.291,0.439);\n" @@ -103,20 +103,15 @@ void CreatePrograms() " gl_Position = vec4(rawpos*2.0-1.0, 0.0, 1.0);\n" "}\n"; const char *FProgramYuyvToRgb = - "uniform sampler2DRect samp9;\n" + "uniform sampler2D samp9;\n" "VARYIN vec2 uv0;\n" "out vec4 ocol0;\n" "void main()\n" "{\n" " ivec2 uv = ivec2(gl_FragCoord.xy);\n" -#ifdef USE_GLES3 // We switch top/bottom here. TODO: move this to screen blit. " ivec2 ts = textureSize(samp9, 0);\n" " vec4 c0 = texelFetch(samp9, ivec2(uv.x/2, ts.y-uv.y-1), 0);\n" -#else - " ivec2 ts = textureSize(samp9);\n" - " vec4 c0 = texelFetch(samp9, ivec2(uv.x/2, ts.y-uv.y-1));\n" -#endif " float y = mix(c0.b, c0.r, (uv.x & 1) == 1);\n" " float yComp = 1.164 * (y - 0.0625);\n" " float uComp = c0.g - 0.5;\n" From b904d56036f9a16e6fa95d832f388aa5d7229995 Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 08:43:55 +0100 Subject: [PATCH 121/202] OpenGL: fix efb2tex for texture2d --- Source/Core/VideoBackends/OGL/Src/TextureCache.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp b/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp index 070ffe24d074..b08a46530270 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp @@ -403,25 +403,25 @@ void TextureCache::TCacheEntry::FromRenderTarget(u32 dstAddr, unsigned int dstFo TextureCache::TextureCache() { const char *pColorMatrixProg = - "uniform sampler2DRect samp9;\n" + "uniform sampler2D samp9;\n" "uniform vec4 colmat[7];\n" "VARYIN vec2 uv0;\n" "out vec4 ocol0;\n" "\n" "void main(){\n" - " vec4 texcol = texture2DRect(samp9, uv0);\n" + " vec4 texcol = texture(samp9, uv0 / textureSize(samp9, 0));\n" " texcol = round(texcol * colmat[5]) * colmat[6];\n" " ocol0 = texcol * mat4(colmat[0], colmat[1], colmat[2], colmat[3]) + colmat[4];\n" "}\n"; const char *pDepthMatrixProg = - "uniform sampler2DRect samp9;\n" + "uniform sampler2D samp9;\n" "uniform vec4 colmat[5];\n" "VARYIN vec2 uv0;\n" "out vec4 ocol0;\n" "\n" "void main(){\n" - " vec4 texcol = texture2DRect(samp9, uv0);\n" + " vec4 texcol = texture(samp9, uv0 / textureSize(samp9, 0));\n" " vec4 EncodedDepth = fract((texcol.r * (16777215.0/16777216.0)) * vec4(1.0,256.0,256.0*256.0,1.0));\n" " texcol = round(EncodedDepth * (16777216.0/16777215.0) * vec4(255.0,255.0,255.0,15.0)) / vec4(255.0,255.0,255.0,15.0);\n" " ocol0 = texcol * mat4(colmat[0], colmat[1], colmat[2], colmat[3]) + colmat[4];" From 146e435009eec015a160e35d13cad4f22d7a8d69 Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 08:56:01 +0100 Subject: [PATCH 122/202] OpenGL: fix efb2ram for texture2D This was hacky as hell. Our efb2ram shader generator is just freaked out. --- Source/Core/VideoCommon/Src/TextureConversionShader.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp index 99e9f702c705..022d18b42775 100644 --- a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp +++ b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp @@ -81,7 +81,7 @@ void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) if (ApiType == API_OPENGL) { WRITE(p, "#define samp0 samp9\n"); - WRITE(p, "uniform sampler2DRect samp0;\n"); + WRITE(p, "uniform sampler2D samp0;\n"); WRITE(p, " out vec4 ocol0;\n"); WRITE(p, " VARYIN float2 uv0;\n"); @@ -146,7 +146,7 @@ void Write32BitSwizzler(char*& p, u32 format, API_TYPE ApiType) if (ApiType == API_OPENGL) { WRITE(p, "#define samp0 samp9\n"); - WRITE(p, "uniform sampler2DRect samp0;\n"); + WRITE(p, "uniform sampler2D samp0;\n"); WRITE(p, " out float4 ocol0;\n"); WRITE(p, " VARYIN float2 uv0;\n"); @@ -202,7 +202,7 @@ void WriteSampleColor(char*& p, const char* colorComp, const char* dest, API_TYP if (ApiType == API_D3D) texSampleOpName = "tex0.Sample"; else // OGL - texSampleOpName = "texture2DRect"; + texSampleOpName = "texture"; // the increment of sampleUv.x is delayed, so we perform it here. see WriteIncrementSampleX. const char* texSampleIncrementUnit; @@ -211,7 +211,7 @@ void WriteSampleColor(char*& p, const char* colorComp, const char* dest, API_TYP else // OGL texSampleIncrementUnit = I_COLORS"[0].x"; - WRITE(p, " %s = %s(samp0, sampleUv + float2(%d.0 * (%s), 0.0)).%s;\n", + WRITE(p, " %s = %s(samp0, (sampleUv + float2(%d.0 * (%s), 0.0)) / textureSize(samp0, 0)).%s;\n", dest, texSampleOpName, s_incrementSampleXCount, texSampleIncrementUnit, colorComp); } From afcf0e65d1a548ea0215454c58a91708fad2e92a Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 08:59:04 +0100 Subject: [PATCH 123/202] OpenGL: fix emulate format changes for texture2d --- Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp index af130fc7aa54..14b02b2f14f5 100644 --- a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp +++ b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp @@ -199,11 +199,11 @@ FramebufferManager::FramebufferManager(int targetWidth, int targetHeight, int ms "}\n"; char ps_rgba6_to_rgb8[] = - "uniform sampler2DRect samp9;\n" + "uniform sampler2D samp9;\n" "out vec4 ocol0;\n" "void main()\n" "{\n" - " ivec4 src6 = ivec4(round(texture2DRect(samp9, gl_FragCoord.xy) * 63.f));\n" + " ivec4 src6 = ivec4(round(texelFetch(samp9, ivec2(gl_FragCoord.xy), 0) * 63.f));\n" " ivec4 dst8;\n" " dst8.r = (src6.r << 2) | (src6.g >> 4);\n" " dst8.g = ((src6.g & 0xF) << 4) | (src6.b >> 2);\n" @@ -213,11 +213,11 @@ FramebufferManager::FramebufferManager(int targetWidth, int targetHeight, int ms "}"; char ps_rgb8_to_rgba6[] = - "uniform sampler2DRect samp9;\n" + "uniform sampler2D samp9;\n" "out vec4 ocol0;\n" "void main()\n" "{\n" - " ivec4 src8 = ivec4(round(texture2DRect(samp9, gl_FragCoord.xy) * 255.f));\n" + " ivec4 src8 = ivec4(round(texelFetch(samp9, ivec2(gl_FragCoord.xy), 0) * 255.f));\n" " ivec4 dst6;\n" " dst6.r = src8.r >> 2;\n" " dst6.g = ((src8.r & 0x3) << 4) | (src8.g >> 4);\n" From b93756df870021d239e95d17cf76335cd3417e95 Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 09:05:50 +0100 Subject: [PATCH 124/202] OpenGL: drop texture_rect hack Everything is moved to texture2d (but often in a hacky way), so we don't need this global hack any more. --- Source/Core/VideoBackends/OGL/Src/ProgramShaderCache.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/ProgramShaderCache.cpp b/Source/Core/VideoBackends/OGL/Src/ProgramShaderCache.cpp index f998c6ced285..9a9a6f3cf942 100644 --- a/Source/Core/VideoBackends/OGL/Src/ProgramShaderCache.cpp +++ b/Source/Core/VideoBackends/OGL/Src/ProgramShaderCache.cpp @@ -589,11 +589,6 @@ void ProgramShaderCache::CreateHeader ( void ) "#define frac fract\n" "#define lerp mix\n" - // texture2d hack - "%s\n" - "%s\n" - "%s\n" - , v==GLSLES3 ? "#version 300 es" : v==GLSL_130 ? "#version 130" : v==GLSL_140 ? "#version 140" : "#version 150" , g_ActiveConfig.backend_info.bSupportsGLSLUBO && v Date: Mon, 25 Nov 2013 12:19:34 +0100 Subject: [PATCH 125/202] OpenGL: cleanup efb2tex Also use attributeless rendering. But we need the src rect, so set it by uniform. If there is a slowdown here (I doubt as the driver likely has a fast path to update uniforms) then we should check if this rect changes and only then update the uniform. --- .../VideoBackends/OGL/Src/TextureCache.cpp | 78 ++++--------------- 1 file changed, 15 insertions(+), 63 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp b/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp index b08a46530270..af7e121ffebf 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp @@ -45,6 +45,8 @@ static SHADER s_ColorMatrixProgram; static SHADER s_DepthMatrixProgram; static GLuint s_ColorMatrixUniform; static GLuint s_DepthMatrixUniform; +static GLuint s_ColorCopyPositionUniform; +static GLuint s_DepthCopyPositionUniform; static u32 s_ColorCbufid; static u32 s_DepthCbufid; @@ -52,13 +54,6 @@ static u32 s_Textures[8]; static u32 s_ActiveTexture; static u32 s_NextStage; -struct VBOCache { - GLuint vbo; - GLuint vao; - TargetRectangle targetSource; -}; -static std::map s_VBO; - bool SaveTexture(const std::string filename, u32 textarget, u32 tex, int virtual_width, int virtual_height, unsigned int level) { #ifndef USE_GLES3 @@ -311,53 +306,12 @@ void TextureCache::TCacheEntry::FromRenderTarget(u32 dstAddr, unsigned int dstFo glUniform4fv(s_ColorMatrixUniform, 7, colmat); s_ColorCbufid = cbufid; } - GL_REPORT_ERRORD(); - TargetRectangle targetSource = g_renderer->ConvertEFBRectangle(srcRect); + TargetRectangle R = g_renderer->ConvertEFBRectangle(srcRect); + glUniform4f(srcFormat == PIXELFMT_Z24 ? s_DepthCopyPositionUniform : s_ColorCopyPositionUniform, + R.left, R.top, R.right, R.bottom); GL_REPORT_ERRORD(); - // should be unique enough, if not, vbo will "only" be uploaded to much - u64 targetSourceHash = u64(targetSource.left)<<48 | u64(targetSource.top)<<32 | u64(targetSource.right)<<16 | u64(targetSource.bottom); - std::map::iterator vbo_it = s_VBO.find(targetSourceHash); - - if(vbo_it == s_VBO.end()) { - VBOCache item; - item.targetSource.bottom = -1; - item.targetSource.top = -1; - item.targetSource.left = -1; - item.targetSource.right = -1; - glGenBuffers(1, &item.vbo); - glGenVertexArrays(1, &item.vao); - - glBindBuffer(GL_ARRAY_BUFFER, item.vbo); - glBindVertexArray(item.vao); - - glEnableVertexAttribArray(SHADER_POSITION_ATTRIB); - glVertexAttribPointer(SHADER_POSITION_ATTRIB, 2, GL_FLOAT, 0, sizeof(GLfloat)*4, (GLfloat*)NULL); - glEnableVertexAttribArray(SHADER_TEXTURE0_ATTRIB); - glVertexAttribPointer(SHADER_TEXTURE0_ATTRIB, 2, GL_FLOAT, 0, sizeof(GLfloat)*4, (GLfloat*)NULL+2); - - vbo_it = s_VBO.insert(std::pair(targetSourceHash, item)).first; - } - if(!(vbo_it->second.targetSource == targetSource)) { - GLfloat vertices[] = { - -1.f, 1.f, - (GLfloat)targetSource.left, (GLfloat)targetSource.bottom, - -1.f, -1.f, - (GLfloat)targetSource.left, (GLfloat)targetSource.top, - 1.f, 1.f, - (GLfloat)targetSource.right, (GLfloat)targetSource.bottom, - 1.f, -1.f, - (GLfloat)targetSource.right, (GLfloat)targetSource.top - }; - - glBindBuffer(GL_ARRAY_BUFFER, vbo_it->second.vbo); - glBufferData(GL_ARRAY_BUFFER, 4*4*sizeof(GLfloat), vertices, GL_STREAM_DRAW); - - vbo_it->second.targetSource = targetSource; - } - - glBindVertexArray(vbo_it->second.vao); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); GL_REPORT_ERRORD(); @@ -409,7 +363,7 @@ TextureCache::TextureCache() "out vec4 ocol0;\n" "\n" "void main(){\n" - " vec4 texcol = texture(samp9, uv0 / textureSize(samp9, 0));\n" + " vec4 texcol = texture(samp9, uv0);\n" " texcol = round(texcol * colmat[5]) * colmat[6];\n" " ocol0 = texcol * mat4(colmat[0], colmat[1], colmat[2], colmat[3]) + colmat[4];\n" "}\n"; @@ -421,20 +375,21 @@ TextureCache::TextureCache() "out vec4 ocol0;\n" "\n" "void main(){\n" - " vec4 texcol = texture(samp9, uv0 / textureSize(samp9, 0));\n" + " vec4 texcol = texture(samp9, uv0);\n" " vec4 EncodedDepth = fract((texcol.r * (16777215.0/16777216.0)) * vec4(1.0,256.0,256.0*256.0,1.0));\n" " texcol = round(EncodedDepth * (16777216.0/16777215.0) * vec4(255.0,255.0,255.0,15.0)) / vec4(255.0,255.0,255.0,15.0);\n" " ocol0 = texcol * mat4(colmat[0], colmat[1], colmat[2], colmat[3]) + colmat[4];" "}\n"; const char *VProgram = - "ATTRIN vec2 rawpos;\n" - "ATTRIN vec2 tex0;\n" "VARYOUT vec2 uv0;\n" + "uniform sampler2D samp9;\n" + "uniform vec4 copy_position;\n" // left, top, right, bottom "void main()\n" "{\n" - " uv0 = tex0;\n" - " gl_Position = vec4(rawpos,0,1);\n" + " vec2 rawpos = vec2(gl_VertexID&1, gl_VertexID&2);\n" + " uv0 = mix(copy_position.xy, copy_position.zw, rawpos) / textureSize(samp9, 0);\n" + " gl_Position = vec4(rawpos*2.0-1.0, 0.0, 1.0);\n" "}\n"; ProgramShaderCache::CompileShader(s_ColorMatrixProgram, VProgram, pColorMatrixProg); @@ -445,6 +400,9 @@ TextureCache::TextureCache() s_ColorCbufid = -1; s_DepthCbufid = -1; + s_ColorCopyPositionUniform = glGetUniformLocation(s_ColorMatrixProgram.glprogid, "copy_position"); + s_DepthCopyPositionUniform = glGetUniformLocation(s_DepthMatrixProgram.glprogid, "copy_position"); + s_ActiveTexture = -1; s_NextStage = -1; for(auto& gtex : s_Textures) @@ -456,12 +414,6 @@ TextureCache::~TextureCache() { s_ColorMatrixProgram.Destroy(); s_DepthMatrixProgram.Destroy(); - - for(auto& cache : s_VBO) { - glDeleteBuffers(1, &cache.second.vbo); - glDeleteVertexArrays(1, &cache.second.vao); - } - s_VBO.clear(); } void TextureCache::DisableStage(unsigned int stage) From 38fe05b1df7ad575b12834dfaba2788cff12ce9e Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 12:27:54 +0100 Subject: [PATCH 126/202] OpenGL: attributeless rendering in emulate format changes only cleanup --- .../OGL/Src/FramebufferManager.cpp | 24 ++----------------- .../OGL/Src/FramebufferManager.h | 2 -- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp index 14b02b2f14f5..9abd47a3613a 100644 --- a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp +++ b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.cpp @@ -33,8 +33,6 @@ GLuint FramebufferManager::m_resolvedDepthTexture; GLuint FramebufferManager::m_xfbFramebuffer; // reinterpret pixel format -GLuint FramebufferManager::m_pixel_format_vao; -GLuint FramebufferManager::m_pixel_format_vbo; SHADER FramebufferManager::m_pixel_format_shaders[2]; @@ -177,25 +175,10 @@ FramebufferManager::FramebufferManager(int targetWidth, int targetHeight, int ms glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); // reinterpret pixel format - glGenBuffers(1, &m_pixel_format_vbo); - glGenVertexArrays(1, &m_pixel_format_vao); - glBindVertexArray(m_pixel_format_vao); - glBindBuffer(GL_ARRAY_BUFFER, m_pixel_format_vbo); - glEnableVertexAttribArray(SHADER_POSITION_ATTRIB); - glVertexAttribPointer(SHADER_POSITION_ATTRIB, 2, GL_FLOAT, 0, sizeof(GLfloat)*2, NULL); - - float vertices[] = { - -1.0, -1.0, - 1.0, -1.0, - -1.0, 1.0, - 1.0, 1.0, - }; - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - char vs[] = - "ATTRIN vec2 rawpos;\n" "void main(void) {\n" - " gl_Position = vec4(rawpos,0,1);\n" + " vec2 rawpos = vec2(gl_VertexID&1, gl_VertexID&2);\n" + " gl_Position = vec4(rawpos*2.0-1.0, 0.0, 1.0);\n" "}\n"; char ps_rgba6_to_rgb8[] = @@ -261,8 +244,6 @@ FramebufferManager::~FramebufferManager() m_efbDepth = 0; // reinterpret pixel format - glDeleteVertexArrays(1, &m_pixel_format_vao); - glDeleteBuffers(1, &m_pixel_format_vbo); m_pixel_format_shaders[0].Destroy(); m_pixel_format_shaders[1].Destroy(); } @@ -393,7 +374,6 @@ void FramebufferManager::ReinterpretPixelData(unsigned int convtype) glBindTexture(GL_TEXTURE_2D, src_texture); m_pixel_format_shaders[convtype ? 1 : 0].Bind(); - glBindVertexArray(m_pixel_format_vao); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); g_renderer->RestoreAPIState(); diff --git a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.h b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.h index d3e32075deb9..9fbcdbfedd58 100644 --- a/Source/Core/VideoBackends/OGL/Src/FramebufferManager.h +++ b/Source/Core/VideoBackends/OGL/Src/FramebufferManager.h @@ -113,8 +113,6 @@ class FramebufferManager : public FramebufferManagerBase static GLuint m_xfbFramebuffer; // Only used in MSAA mode // For pixel format draw - static GLuint m_pixel_format_vbo; - static GLuint m_pixel_format_vao; static SHADER m_pixel_format_shaders[2]; }; From 6ed3f82affe9f866a580bcddfaa6f760f92a1b3e Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 12:36:17 +0100 Subject: [PATCH 127/202] OpenGL: attributeless rendering for postprocessing --- .../VideoBackends/OGL/Src/PostProcessing.cpp | 34 +++---------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/PostProcessing.cpp b/Source/Core/VideoBackends/OGL/Src/PostProcessing.cpp index 84fb0c4d1cbe..c0460e489a53 100644 --- a/Source/Core/VideoBackends/OGL/Src/PostProcessing.cpp +++ b/Source/Core/VideoBackends/OGL/Src/PostProcessing.cpp @@ -25,18 +25,15 @@ static u32 s_width; static u32 s_height; static GLuint s_fbo; static GLuint s_texture; -static GLuint s_vao; -static GLuint s_vbo; static GLuint s_uniform_resolution; static char s_vertex_shader[] = - "in vec2 rawpos;\n" - "in vec2 tex0;\n" "out vec2 uv0;\n" "void main(void) {\n" - " gl_Position = vec4(rawpos,0,1);\n" - " uv0 = tex0;\n" + " vec2 rawpos = vec2(gl_VertexID&1, gl_VertexID&2);\n" + " gl_Position = vec4(rawpos*2.0-1.0, 0.0, 1.0);\n" + " uv0 = rawpos;\n" "}\n"; void Init() @@ -56,34 +53,14 @@ void Init() glBindFramebuffer(GL_FRAMEBUFFER, s_fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, s_texture, 0); FramebufferManager::SetFramebuffer(0); - - glGenBuffers(1, &s_vbo); - glBindBuffer(GL_ARRAY_BUFFER, s_vbo); - GLfloat vertices[] = { - -1.f, -1.f, 0.f, 0.f, - -1.f, 1.f, 0.f, 1.f, - 1.f, -1.f, 1.f, 0.f, - 1.f, 1.f, 1.f, 1.f - }; - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - glGenVertexArrays(1, &s_vao); - glBindVertexArray( s_vao ); - glEnableVertexAttribArray(SHADER_POSITION_ATTRIB); - glVertexAttribPointer(SHADER_POSITION_ATTRIB, 2, GL_FLOAT, 0, sizeof(GLfloat)*4, NULL); - glEnableVertexAttribArray(SHADER_TEXTURE0_ATTRIB); - glVertexAttribPointer(SHADER_TEXTURE0_ATTRIB, 2, GL_FLOAT, 0, sizeof(GLfloat)*4, (GLfloat*)NULL+2); } void Shutdown() { s_shader.Destroy(); - glDeleteFramebuffers(1, &s_vbo); + glDeleteFramebuffers(1, &s_fbo); glDeleteTextures(1, &s_texture); - - glDeleteBuffers(1, &s_vbo); - glDeleteVertexArrays(1, &s_vao); } void ReloadShader() @@ -103,7 +80,6 @@ void BlitToScreen() glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glViewport(0, 0, s_width, s_height); - glBindVertexArray(s_vao); s_shader.Bind(); glUniform4f(s_uniform_resolution, (float)s_width, (float)s_height, 1.0f/(float)s_width, 1.0f/(float)s_height); @@ -111,7 +87,6 @@ void BlitToScreen() glActiveTexture(GL_TEXTURE0+9); glBindTexture(GL_TEXTURE_2D, s_texture); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glBindTexture(GL_TEXTURE_2D, 0); /* glBindFramebuffer(GL_READ_FRAMEBUFFER, s_fbo); @@ -132,7 +107,6 @@ void Update ( u32 width, u32 height ) glActiveTexture(GL_TEXTURE0+9); glBindTexture(GL_TEXTURE_2D, s_texture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); - glBindTexture(GL_TEXTURE_2D, 0); } } From 454e1dd9a2434bf68121d8a2765a0f2413ba4dcf Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 13:03:35 +0100 Subject: [PATCH 128/202] OpenGL: attributeless rendering for efb2ram This wasn't as easy as we now have to cache also the uniform locations. --- .../OGL/Src/TextureConverter.cpp | 74 +++++++------------ 1 file changed, 26 insertions(+), 48 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index c5f96dba1467..161468d2b033 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -34,24 +34,23 @@ const int renderBufferWidth = 1024; const int renderBufferHeight = 1024; static SHADER s_rgbToYuyvProgram; +static int s_rgbToYuyvUniform_loc; + static SHADER s_yuyvToRgbProgram; // Not all slots are taken - but who cares. const u32 NUM_ENCODING_PROGRAMS = 64; static SHADER s_encodingPrograms[NUM_ENCODING_PROGRAMS]; - -static GLuint s_encode_VBO = 0; -static GLuint s_encode_VAO = 0; -static TargetRectangle s_cached_sourceRc; +static int s_encodingUniform_loc[NUM_ENCODING_PROGRAMS]; static const char *VProgram = - "ATTRIN vec2 rawpos;\n" - "ATTRIN vec2 tex0;\n" "VARYOUT vec2 uv0;\n" + "uniform vec4 copy_position;\n" // left, top, right, bottom "void main()\n" "{\n" - " uv0 = tex0;\n" - " gl_Position = vec4(rawpos, 0.0, 1.0);\n" + " vec2 rawpos = vec2(gl_VertexID&1, gl_VertexID&2);\n" + " gl_Position = vec4(rawpos*2.0-1.0, 0.0, 1.0);\n" + " uv0 = mix(copy_position.xy, copy_position.zw, rawpos);\n" "}\n"; void CreatePrograms() @@ -73,14 +72,24 @@ void CreatePrograms() * inbetween the two Pixels, and only blurs over these two pixels. */ // Output is BGRA because that is slightly faster than RGBA. + const char *VProgramRgbToYuyv = + "VARYOUT vec2 uv0;\n" + "uniform vec4 copy_position;\n" // left, top, right, bottom + "uniform sampler2D samp9;\n" + "void main()\n" + "{\n" + " vec2 rawpos = vec2(gl_VertexID&1, gl_VertexID&2);\n" + " gl_Position = vec4(rawpos*2.0-1.0, 0.0, 1.0);\n" + " uv0 = mix(copy_position.xy, copy_position.zw, rawpos) / textureSize(samp9, 0);\n" + "}\n"; const char *FProgramRgbToYuyv = "uniform sampler2D samp9;\n" "VARYIN vec2 uv0;\n" "out vec4 ocol0;\n" "void main()\n" "{\n" - " vec3 c0 = texture(samp9, (uv0 - dFdx(uv0) * 0.25) / textureSize(samp9, 0)).rgb;\n" - " vec3 c1 = texture(samp9, (uv0 + dFdx(uv0) * 0.25) / textureSize(samp9, 0)).rgb;\n" + " vec3 c0 = texture(samp9, (uv0 - dFdx(uv0) * 0.25)).rgb;\n" + " vec3 c1 = texture(samp9, (uv0 + dFdx(uv0) * 0.25)).rgb;\n" " vec3 c01 = (c0 + c1) * 0.5;\n" " vec3 y_const = vec3(0.257,0.504,0.098);\n" " vec3 u_const = vec3(-0.148,-0.291,0.439);\n" @@ -88,6 +97,8 @@ void CreatePrograms() " vec4 const3 = vec4(0.0625,0.5,0.0625,0.5);\n" " ocol0 = vec4(dot(c1,y_const),dot(c01,u_const),dot(c0,y_const),dot(c01, v_const)) + const3;\n" "}\n"; + ProgramShaderCache::CompileShader(s_rgbToYuyvProgram, VProgramRgbToYuyv, FProgramRgbToYuyv); + s_rgbToYuyvUniform_loc = glGetUniformLocation(s_rgbToYuyvProgram.glprogid, "copy_position"); /* TODO: Accuracy Improvements * @@ -121,8 +132,6 @@ void CreatePrograms() " yComp + (2.018 * uComp),\n" " 1.0);\n" "}\n"; - - ProgramShaderCache::CompileShader(s_rgbToYuyvProgram, VProgram, FProgramRgbToYuyv); ProgramShaderCache::CompileShader(s_yuyvToRgbProgram, VProgramYuyvToRgb, FProgramYuyvToRgb); } @@ -150,6 +159,7 @@ SHADER &GetOrCreateEncodingShader(u32 format) #endif ProgramShaderCache::CompileShader(s_encodingPrograms[format], VProgram, shader); + s_encodingUniform_loc[format] = glGetUniformLocation(s_encodingPrograms[format].glprogid, "copy_position"); } return s_encodingPrograms[format]; } @@ -158,19 +168,6 @@ void Init() { glGenFramebuffers(1, &s_texConvFrameBuffer); - glGenBuffers(1, &s_encode_VBO ); - glGenVertexArrays(1, &s_encode_VAO ); - glBindBuffer(GL_ARRAY_BUFFER, s_encode_VBO ); - glBindVertexArray( s_encode_VAO ); - glEnableVertexAttribArray(SHADER_POSITION_ATTRIB); - glVertexAttribPointer(SHADER_POSITION_ATTRIB, 2, GL_FLOAT, 0, sizeof(GLfloat)*4, (GLfloat*)NULL); - glEnableVertexAttribArray(SHADER_TEXTURE0_ATTRIB); - glVertexAttribPointer(SHADER_TEXTURE0_ATTRIB, 2, GL_FLOAT, 0, sizeof(GLfloat)*4, (GLfloat*)NULL+2); - s_cached_sourceRc.top = -1; - s_cached_sourceRc.bottom = -1; - s_cached_sourceRc.left = -1; - s_cached_sourceRc.right = -1; - glActiveTexture(GL_TEXTURE0 + 9); glGenTextures(1, &s_srcTexture); glBindTexture(GL_TEXTURE_2D, s_srcTexture); @@ -189,8 +186,6 @@ void Shutdown() glDeleteTextures(1, &s_srcTexture); glDeleteTextures(1, &s_dstTexture); glDeleteFramebuffers(1, &s_texConvFrameBuffer); - glDeleteBuffers(1, &s_encode_VBO ); - glDeleteVertexArrays(1, &s_encode_VAO ); s_rgbToYuyvProgram.Destroy(); s_yuyvToRgbProgram.Destroy(); @@ -205,7 +200,7 @@ void Shutdown() void EncodeToRamUsingShader(GLuint srcTexture, const TargetRectangle& sourceRc, u8* destAddr, int dstWidth, int dstHeight, int readStride, - bool toTexture, bool linearFilter) + bool toTexture, bool linearFilter, int uniform_loc) { @@ -235,25 +230,8 @@ void EncodeToRamUsingShader(GLuint srcTexture, const TargetRectangle& sourceRc, glViewport(0, 0, (GLsizei)dstWidth, (GLsizei)dstHeight); - GL_REPORT_ERRORD(); - if(!(s_cached_sourceRc == sourceRc)) { - GLfloat vertices[] = { - -1.f, -1.f, - (float)sourceRc.left, (float)sourceRc.top, - -1.f, 1.f, - (float)sourceRc.left, (float)sourceRc.bottom, - 1.f, -1.f, - (float)sourceRc.right, (float)sourceRc.top, - 1.f, 1.f, - (float)sourceRc.right, (float)sourceRc.bottom - }; - glBindBuffer(GL_ARRAY_BUFFER, s_encode_VBO ); - glBufferData(GL_ARRAY_BUFFER, 4*4*sizeof(GLfloat), vertices, GL_STREAM_DRAW); - - s_cached_sourceRc = sourceRc; - } + glUniform4f(uniform_loc, sourceRc.left, sourceRc.top, sourceRc.right, sourceRc.bottom); - glBindVertexArray( s_encode_VAO ); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); GL_REPORT_ERRORD(); @@ -345,7 +323,7 @@ int EncodeToRamFromTexture(u32 address,GLuint source_texture, bool bFromZBuffer, TexDecoder_GetBlockWidthInTexels(format); EncodeToRamUsingShader(source_texture, scaledSource, dest_ptr, expandedWidth / samples, expandedHeight, readStride, - true, bScaleByHalf > 0 && !bFromZBuffer); + true, bScaleByHalf > 0 && !bFromZBuffer, s_encodingUniform_loc[format]); return size_in_bytes; // TODO: D3D11 is calculating this value differently! } @@ -359,7 +337,7 @@ void EncodeToRamYUYV(GLuint srcTexture, const TargetRectangle& sourceRc, u8* des // We enable linear filtering, because the gamecube does filtering in the vertical direction when // yscale is enabled. // Otherwise we get jaggies when a game uses yscaling (most PAL games) - EncodeToRamUsingShader(srcTexture, sourceRc, destAddr, dstWidth / 2, dstHeight, 0, false, true); + EncodeToRamUsingShader(srcTexture, sourceRc, destAddr, dstWidth / 2, dstHeight, 0, false, true, s_rgbToYuyvUniform_loc); FramebufferManager::SetFramebuffer(0); TextureCache::DisableStage(0); g_renderer->RestoreAPIState(); From a289e0604f3fd3663a7d32e8aafbe9d8286b35ad Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 14:53:44 +0100 Subject: [PATCH 129/202] TextureConverter: remove D3D9 foo This file is in VideoCommon, but as D3D11 doesn't use it and D3D9 is dropped, it's time to clean up. --- .../Src/TextureConversionShader.cpp | 49 +++---------------- 1 file changed, 8 insertions(+), 41 deletions(-) diff --git a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp index 022d18b42775..c78b854535c8 100644 --- a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp +++ b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp @@ -57,15 +57,6 @@ u16 GetEncodedSampleCount(u32 format) } } -const char* WriteRegister(API_TYPE ApiType, const char *prefix, const u32 num) -{ - if (ApiType == API_OPENGL) - return ""; // Once we switch to GLSL 1.3 we can do something here - static char result[64]; - sprintf(result, " : register(%s%d)", prefix, num); - return result; -} - // block dimensions : widthStride, heightStride // texture dims : width, height, x offset, y offset void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) @@ -73,7 +64,7 @@ void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) // [0] left, top, right, bottom of source rectangle within source texture // [1] width and height of destination texture in pixels // Two were merged for GLSL - WRITE(p, "uniform float4 " I_COLORS"[2] %s;\n", WriteRegister(ApiType, "c", C_COLORS)); + WRITE(p, "uniform float4 " I_COLORS"[2];\n"); int blkW = TexDecoder_GetBlockWidthInTexels(format); int blkH = TexDecoder_GetBlockHeightInTexels(format); @@ -118,16 +109,9 @@ void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) WRITE(p, " sampleUv = sampleUv * " I_COLORS"[0].xy;\n"); - if (ApiType == API_OPENGL) - WRITE(p," sampleUv.y = " I_COLORS"[1].y - sampleUv.y;\n"); + WRITE(p," sampleUv.y = " I_COLORS"[1].y - sampleUv.y;\n"); WRITE(p, " sampleUv = sampleUv + " I_COLORS"[1].zw;\n"); - - if (ApiType != API_OPENGL) - { - WRITE(p, " sampleUv = sampleUv + float2(0.0,1.0);\n"); // still need to determine the reason for this - WRITE(p, " sampleUv = sampleUv / " I_COLORS"[0].zw;\n"); - } } // block dimensions : widthStride, heightStride @@ -137,7 +121,7 @@ void Write32BitSwizzler(char*& p, u32 format, API_TYPE ApiType) // [0] left, top, right, bottom of source rectangle within source texture // [1] width and height of destination texture in pixels // Two were merged for GLSL - WRITE(p, "uniform float4 " I_COLORS"[2] %s;\n", WriteRegister(ApiType, "c", C_COLORS)); + WRITE(p, "uniform float4 " I_COLORS"[2];\n"); int blkW = TexDecoder_GetBlockWidthInTexels(format); int blkH = TexDecoder_GetBlockHeightInTexels(format); @@ -184,35 +168,18 @@ void Write32BitSwizzler(char*& p, u32 format, API_TYPE ApiType) WRITE(p, " sampleUv.y = yb + xoff;\n"); WRITE(p, " sampleUv = sampleUv * " I_COLORS"[0].xy;\n"); - if (ApiType == API_OPENGL) - WRITE(p," sampleUv.y = " I_COLORS"[1].y - sampleUv.y;\n"); + WRITE(p," sampleUv.y = " I_COLORS"[1].y - sampleUv.y;\n"); WRITE(p, " sampleUv = sampleUv + " I_COLORS"[1].zw;\n"); - - if (ApiType != API_OPENGL) - { - WRITE(p, " sampleUv = sampleUv + float2(0.0,1.0);\n");// still to determine the reason for this - WRITE(p, " sampleUv = sampleUv / " I_COLORS"[0].zw;\n"); - } } void WriteSampleColor(char*& p, const char* colorComp, const char* dest, API_TYPE ApiType) { - const char* texSampleOpName; - if (ApiType == API_D3D) - texSampleOpName = "tex0.Sample"; - else // OGL - texSampleOpName = "texture"; - // the increment of sampleUv.x is delayed, so we perform it here. see WriteIncrementSampleX. - const char* texSampleIncrementUnit; - if (ApiType == API_D3D) - texSampleIncrementUnit = I_COLORS"[0].x / " I_COLORS"[0].z"; - else // OGL - texSampleIncrementUnit = I_COLORS"[0].x"; - - WRITE(p, " %s = %s(samp0, (sampleUv + float2(%d.0 * (%s), 0.0)) / textureSize(samp0, 0)).%s;\n", - dest, texSampleOpName, s_incrementSampleXCount, texSampleIncrementUnit, colorComp); + const char* texSampleIncrementUnit = I_COLORS"[0].x"; + + WRITE(p, " %s = texture(samp0, (sampleUv + float2(%d.0 * (%s), 0.0)) / textureSize(samp0, 0)).%s;\n", + dest, s_incrementSampleXCount, texSampleIncrementUnit, colorComp); } void WriteColorToIntensity(char*& p, const char* src, const char* dest) From bcb31b09d38463f8a7a515858a2143477bfc8935 Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 15:01:18 +0100 Subject: [PATCH 130/202] TextureConverter: Use gl_FragCoord instead of uv0 --- .../OGL/Src/TextureConverter.cpp | 29 ++++++++----------- .../Src/TextureConversionShader.cpp | 12 +++----- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index 161468d2b033..08932f562e9e 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -41,17 +41,6 @@ static SHADER s_yuyvToRgbProgram; // Not all slots are taken - but who cares. const u32 NUM_ENCODING_PROGRAMS = 64; static SHADER s_encodingPrograms[NUM_ENCODING_PROGRAMS]; -static int s_encodingUniform_loc[NUM_ENCODING_PROGRAMS]; - -static const char *VProgram = - "VARYOUT vec2 uv0;\n" - "uniform vec4 copy_position;\n" // left, top, right, bottom - "void main()\n" - "{\n" - " vec2 rawpos = vec2(gl_VertexID&1, gl_VertexID&2);\n" - " gl_Position = vec4(rawpos*2.0-1.0, 0.0, 1.0);\n" - " uv0 = mix(copy_position.xy, copy_position.zw, rawpos);\n" - "}\n"; void CreatePrograms() { @@ -158,8 +147,14 @@ SHADER &GetOrCreateEncodingShader(u32 format) } #endif + const char *VProgram = + "void main()\n" + "{\n" + " vec2 rawpos = vec2(gl_VertexID&1, gl_VertexID&2);\n" + " gl_Position = vec4(rawpos*2.0-1.0, 0.0, 1.0);\n" + "}\n"; + ProgramShaderCache::CompileShader(s_encodingPrograms[format], VProgram, shader); - s_encodingUniform_loc[format] = glGetUniformLocation(s_encodingPrograms[format].glprogid, "copy_position"); } return s_encodingPrograms[format]; } @@ -200,7 +195,7 @@ void Shutdown() void EncodeToRamUsingShader(GLuint srcTexture, const TargetRectangle& sourceRc, u8* destAddr, int dstWidth, int dstHeight, int readStride, - bool toTexture, bool linearFilter, int uniform_loc) + bool toTexture, bool linearFilter) { @@ -230,8 +225,6 @@ void EncodeToRamUsingShader(GLuint srcTexture, const TargetRectangle& sourceRc, glViewport(0, 0, (GLsizei)dstWidth, (GLsizei)dstHeight); - glUniform4f(uniform_loc, sourceRc.left, sourceRc.top, sourceRc.right, sourceRc.bottom); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); GL_REPORT_ERRORD(); @@ -323,7 +316,7 @@ int EncodeToRamFromTexture(u32 address,GLuint source_texture, bool bFromZBuffer, TexDecoder_GetBlockWidthInTexels(format); EncodeToRamUsingShader(source_texture, scaledSource, dest_ptr, expandedWidth / samples, expandedHeight, readStride, - true, bScaleByHalf > 0 && !bFromZBuffer, s_encodingUniform_loc[format]); + true, bScaleByHalf > 0 && !bFromZBuffer); return size_in_bytes; // TODO: D3D11 is calculating this value differently! } @@ -334,10 +327,12 @@ void EncodeToRamYUYV(GLuint srcTexture, const TargetRectangle& sourceRc, u8* des s_rgbToYuyvProgram.Bind(); + glUniform4f(s_rgbToYuyvUniform_loc, sourceRc.left, sourceRc.top, sourceRc.right, sourceRc.bottom); + // We enable linear filtering, because the gamecube does filtering in the vertical direction when // yscale is enabled. // Otherwise we get jaggies when a game uses yscaling (most PAL games) - EncodeToRamUsingShader(srcTexture, sourceRc, destAddr, dstWidth / 2, dstHeight, 0, false, true, s_rgbToYuyvUniform_loc); + EncodeToRamUsingShader(srcTexture, sourceRc, destAddr, dstWidth / 2, dstHeight, 0, false, true); FramebufferManager::SetFramebuffer(0); TextureCache::DisableStage(0); g_renderer->RestoreAPIState(); diff --git a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp index c78b854535c8..a6f63ad6ee7d 100644 --- a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp +++ b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp @@ -75,7 +75,6 @@ void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) WRITE(p, "uniform sampler2D samp0;\n"); WRITE(p, " out vec4 ocol0;\n"); - WRITE(p, " VARYIN float2 uv0;\n"); WRITE(p, "void main()\n"); } else // D3D @@ -84,13 +83,12 @@ void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) WRITE(p, "Texture2D Tex0 : register(t0);\n"); WRITE(p,"void main(\n"); - WRITE(p," out float4 ocol0 : SV_Target,\n"); - WRITE(p," in float2 uv0 : TEXCOORD0)\n"); + WRITE(p," out float4 ocol0 : SV_Target)\n"); } WRITE(p, "{\n" " float2 sampleUv;\n" - " float2 uv1 = floor(uv0);\n"); + " float2 uv1 = floor(gl_FragCoord.xy);\n"); WRITE(p, " uv1.x = uv1.x * %d.0;\n", samples); @@ -133,7 +131,6 @@ void Write32BitSwizzler(char*& p, u32 format, API_TYPE ApiType) WRITE(p, "uniform sampler2D samp0;\n"); WRITE(p, " out float4 ocol0;\n"); - WRITE(p, " VARYIN float2 uv0;\n"); WRITE(p, "void main()\n"); } else @@ -142,14 +139,13 @@ void Write32BitSwizzler(char*& p, u32 format, API_TYPE ApiType) WRITE(p, "Texture2D Tex0 : register(t0);\n"); WRITE(p,"void main(\n"); - WRITE(p," out float4 ocol0 : SV_Target,\n"); - WRITE(p," in float2 uv0 : TEXCOORD0)\n"); + WRITE(p," out float4 ocol0 : SV_Target)\n"); } WRITE(p, "{\n" " float2 sampleUv;\n" - " float2 uv1 = floor(uv0);\n"); + " float2 uv1 = floor(gl_FragCoord.xy);\n"); WRITE(p, " float yl = floor(uv1.y / %d.0);\n", blkH); WRITE(p, " float yb = yl * %d.0;\n", blkH); From 6750a81972822099980a59f334b7928e5ac92be7 Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 15:49:13 +0100 Subject: [PATCH 131/202] TextureConverter: Use integer math for swizzling also move int(efb_coord) -> float(ogl_fb_coord) into WriteSampleColor --- .../OGL/Src/ProgramShaderCache.cpp | 3 + .../OGL/Src/TextureConverter.cpp | 13 +-- .../Src/TextureConversionShader.cpp | 98 +++++++++---------- 3 files changed, 53 insertions(+), 61 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/ProgramShaderCache.cpp b/Source/Core/VideoBackends/OGL/Src/ProgramShaderCache.cpp index 9a9a6f3cf942..cc7e29dce539 100644 --- a/Source/Core/VideoBackends/OGL/Src/ProgramShaderCache.cpp +++ b/Source/Core/VideoBackends/OGL/Src/ProgramShaderCache.cpp @@ -584,6 +584,9 @@ void ProgramShaderCache::CreateHeader ( void ) "#define float2 vec2\n" "#define float3 vec3\n" "#define float4 vec4\n" + "#define int2 ivec2\n" + "#define int3 ivec3\n" + "#define int4 ivec4\n" // hlsl to glsl function translation "#define frac fract\n" diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index 08932f562e9e..eab62d7f7fd9 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -291,17 +291,10 @@ int EncodeToRamFromTexture(u32 address,GLuint source_texture, bool bFromZBuffer, s32 expandedWidth = (width + blkW) & (~blkW); s32 expandedHeight = (height + blkH) & (~blkH); - float sampleStride = bScaleByHalf ? 2.f : 1.f; - - float params[] = { - Renderer::EFBToScaledXf(sampleStride), Renderer::EFBToScaledYf(sampleStride), - 0.0f, 0.0f, - (float)expandedWidth, (float)Renderer::EFBToScaledY(expandedHeight)-1, - (float)Renderer::EFBToScaledX(source.left), (float)Renderer::EFBToScaledY(EFB_HEIGHT - source.top - expandedHeight) - }; - texconv_shader.Bind(); - glUniform4fv(texconv_shader.UniformLocations[0], 2, params); + glUniform4f(texconv_shader.UniformLocations[0], + float(source.left), float(source.top), + (float)expandedWidth, bScaleByHalf ? 2.f : 1.f); TargetRectangle scaledSource; scaledSource.top = 0; diff --git a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp index a6f63ad6ee7d..573b35383af1 100644 --- a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp +++ b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp @@ -64,7 +64,7 @@ void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) // [0] left, top, right, bottom of source rectangle within source texture // [1] width and height of destination texture in pixels // Two were merged for GLSL - WRITE(p, "uniform float4 " I_COLORS"[2];\n"); + WRITE(p, "uniform float4 " I_COLORS";\n"); int blkW = TexDecoder_GetBlockWidthInTexels(format); int blkH = TexDecoder_GetBlockHeightInTexels(format); @@ -87,29 +87,23 @@ void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) } WRITE(p, "{\n" - " float2 sampleUv;\n" - " float2 uv1 = floor(gl_FragCoord.xy);\n"); - - WRITE(p, " uv1.x = uv1.x * %d.0;\n", samples); - - WRITE(p, " float xl = floor(uv1.x / %d.0);\n", blkW); - WRITE(p, " float xib = uv1.x - (xl * %d.0);\n", blkW); - WRITE(p, " float yl = floor(uv1.y / %d.0);\n", blkH); - WRITE(p, " float yb = yl * %d.0;\n", blkH); - WRITE(p, " float yoff = uv1.y - yb;\n"); - WRITE(p, " float xp = uv1.x + (yoff * " I_COLORS"[1].x);\n"); - WRITE(p, " float xel = floor(xp / %d.0);\n", blkW); - WRITE(p, " float xb = floor(xel / %d.0);\n", blkH); - WRITE(p, " float xoff = xel - (xb * %d.0);\n", blkH); - - WRITE(p, " sampleUv.x = xib + (xb * %d.0);\n", blkW); + " int2 sampleUv;\n" + " int2 uv1 = int2(gl_FragCoord.xy);\n"); + + WRITE(p, " uv1.x = uv1.x * %d;\n", samples); + + WRITE(p, " int xl = uv1.x / %d;\n", blkW); + WRITE(p, " int xib = uv1.x - xl * %d;\n", blkW); + WRITE(p, " int yl = uv1.y / %d;\n", blkH); + WRITE(p, " int yb = yl * %d;\n", blkH); + WRITE(p, " int yoff = uv1.y - yb;\n"); + WRITE(p, " int xp = uv1.x + yoff * int(" I_COLORS".z);\n"); + WRITE(p, " int xel = xp / %d;\n", blkW); + WRITE(p, " int xb = xel / %d;\n", blkH); + WRITE(p, " int xoff = xel - xb * %d;\n", blkH); + + WRITE(p, " sampleUv.x = xib + xb * %d;\n", blkW); WRITE(p, " sampleUv.y = yb + xoff;\n"); - - WRITE(p, " sampleUv = sampleUv * " I_COLORS"[0].xy;\n"); - - WRITE(p," sampleUv.y = " I_COLORS"[1].y - sampleUv.y;\n"); - - WRITE(p, " sampleUv = sampleUv + " I_COLORS"[1].zw;\n"); } // block dimensions : widthStride, heightStride @@ -119,7 +113,7 @@ void Write32BitSwizzler(char*& p, u32 format, API_TYPE ApiType) // [0] left, top, right, bottom of source rectangle within source texture // [1] width and height of destination texture in pixels // Two were merged for GLSL - WRITE(p, "uniform float4 " I_COLORS"[2];\n"); + WRITE(p, "uniform float4 " I_COLORS";\n"); int blkW = TexDecoder_GetBlockWidthInTexels(format); int blkH = TexDecoder_GetBlockHeightInTexels(format); @@ -144,38 +138,40 @@ void Write32BitSwizzler(char*& p, u32 format, API_TYPE ApiType) WRITE(p, "{\n" - " float2 sampleUv;\n" - " float2 uv1 = floor(gl_FragCoord.xy);\n"); - - WRITE(p, " float yl = floor(uv1.y / %d.0);\n", blkH); - WRITE(p, " float yb = yl * %d.0;\n", blkH); - WRITE(p, " float yoff = uv1.y - yb;\n"); - WRITE(p, " float xp = uv1.x + (yoff * " I_COLORS"[1].x);\n"); - WRITE(p, " float xel = floor(xp / 2.0);\n"); - WRITE(p, " float xb = floor(xel / %d.0);\n", blkH); - WRITE(p, " float xoff = xel - (xb * %d.0);\n", blkH); - - WRITE(p, " float x2 = uv1.x * 2.0;\n"); - WRITE(p, " float xl = floor(x2 / %d.0);\n", blkW); - WRITE(p, " float xib = x2 - (xl * %d.0);\n", blkW); - WRITE(p, " float halfxb = floor(xb / 2.0);\n"); - - WRITE(p, " sampleUv.x = xib + (halfxb * %d.0);\n", blkW); + " int2 sampleUv;\n" + " int2 uv1 = int2(gl_FragCoord.xy);\n"); + + WRITE(p, " int yl = uv1.y / %d;\n", blkH); + WRITE(p, " int yb = yl * %d;\n", blkH); + WRITE(p, " int yoff = uv1.y - yb;\n"); + WRITE(p, " int xp = uv1.x + yoff * int(" I_COLORS".z);\n"); + WRITE(p, " int xel = xp / 2;\n"); + WRITE(p, " int xb = xel / %d;\n", blkH); + WRITE(p, " int xoff = xel - xb * %d;\n", blkH); + + WRITE(p, " int x2 = uv1.x * 2;\n"); + WRITE(p, " int xl = x2 / %d;\n", blkW); + WRITE(p, " int xib = x2 - xl * %d;\n", blkW); + WRITE(p, " int halfxb = xb / 2;\n"); + + WRITE(p, " sampleUv.x = xib + halfxb * %d;\n", blkW); WRITE(p, " sampleUv.y = yb + xoff;\n"); - WRITE(p, " sampleUv = sampleUv * " I_COLORS"[0].xy;\n"); - - WRITE(p," sampleUv.y = " I_COLORS"[1].y - sampleUv.y;\n"); - - WRITE(p, " sampleUv = sampleUv + " I_COLORS"[1].zw;\n"); } void WriteSampleColor(char*& p, const char* colorComp, const char* dest, API_TYPE ApiType) { - // the increment of sampleUv.x is delayed, so we perform it here. see WriteIncrementSampleX. - const char* texSampleIncrementUnit = I_COLORS"[0].x"; - - WRITE(p, " %s = texture(samp0, (sampleUv + float2(%d.0 * (%s), 0.0)) / textureSize(samp0, 0)).%s;\n", - dest, s_incrementSampleXCount, texSampleIncrementUnit, colorComp); + WRITE(p, + "{\n" + "float2 uv = sampleUv + int2(%d,0);\n" // pixel offset + "uv *= " I_COLORS".w;\n" // scale by two (if wanted) + "uv += " I_COLORS".xy;\n" // move to copyed rect + "uv += float2(0.5, 0.5);\n" // move center of pixel + "uv /= float2(%d, %d);\n" // normlize to [0:1] + "uv.y = 1-uv.y;\n" // ogl foo (disable this line for d3d) + "%s = texture(samp0, uv).%s;\n" + "}\n", + s_incrementSampleXCount, EFB_WIDTH, EFB_HEIGHT, dest, colorComp + ); } void WriteColorToIntensity(char*& p, const char* src, const char* dest) From 9dbb262aab891bad6b299199ff8cd0a71a177c31 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Mon, 25 Nov 2013 15:11:06 +0000 Subject: [PATCH 132/202] Fix for OpenGL ES 3. --- Source/Core/VideoBackends/OGL/Src/TextureCache.cpp | 2 +- Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp b/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp index af7e121ffebf..0666679ae9d0 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureCache.cpp @@ -388,7 +388,7 @@ TextureCache::TextureCache() "void main()\n" "{\n" " vec2 rawpos = vec2(gl_VertexID&1, gl_VertexID&2);\n" - " uv0 = mix(copy_position.xy, copy_position.zw, rawpos) / textureSize(samp9, 0);\n" + " uv0 = mix(copy_position.xy, copy_position.zw, rawpos) / vec2(textureSize(samp9, 0));\n" " gl_Position = vec4(rawpos*2.0-1.0, 0.0, 1.0);\n" "}\n"; diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index eab62d7f7fd9..7f8ae23cc90e 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -69,7 +69,7 @@ void CreatePrograms() "{\n" " vec2 rawpos = vec2(gl_VertexID&1, gl_VertexID&2);\n" " gl_Position = vec4(rawpos*2.0-1.0, 0.0, 1.0);\n" - " uv0 = mix(copy_position.xy, copy_position.zw, rawpos) / textureSize(samp9, 0);\n" + " uv0 = mix(copy_position.xy, copy_position.zw, rawpos) / vec2(textureSize(samp9, 0));\n" "}\n"; const char *FProgramRgbToYuyv = "uniform sampler2D samp9;\n" From 2a2f2fd4ebd29fec3de1498946d3374598d8fb6a Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 16:19:08 +0100 Subject: [PATCH 133/202] TextureConvertion: merge Write*Swizzler --- .../Src/TextureConversionShader.cpp | 84 ++++--------------- 1 file changed, 17 insertions(+), 67 deletions(-) diff --git a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp index 573b35383af1..bf625223dd32 100644 --- a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp +++ b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp @@ -61,14 +61,15 @@ u16 GetEncodedSampleCount(u32 format) // texture dims : width, height, x offset, y offset void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) { - // [0] left, top, right, bottom of source rectangle within source texture - // [1] width and height of destination texture in pixels - // Two were merged for GLSL + // left, top, of source rectangle within source texture + // width of the destination rectangle, scale_factor (1 or 2) WRITE(p, "uniform float4 " I_COLORS";\n"); int blkW = TexDecoder_GetBlockWidthInTexels(format); int blkH = TexDecoder_GetBlockHeightInTexels(format); int samples = GetEncodedSampleCount(format); + // 32 bit textures (RGBA8 and Z24) are store in 2 cache line increments + int factor = samples == 1 ? 2 : 1; if (ApiType == API_OPENGL) { WRITE(p, "#define samp0 samp9\n"); @@ -92,67 +93,16 @@ void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) WRITE(p, " uv1.x = uv1.x * %d;\n", samples); - WRITE(p, " int xl = uv1.x / %d;\n", blkW); - WRITE(p, " int xib = uv1.x - xl * %d;\n", blkW); WRITE(p, " int yl = uv1.y / %d;\n", blkH); WRITE(p, " int yb = yl * %d;\n", blkH); WRITE(p, " int yoff = uv1.y - yb;\n"); WRITE(p, " int xp = uv1.x + yoff * int(" I_COLORS".z);\n"); - WRITE(p, " int xel = xp / %d;\n", blkW); + WRITE(p, " int xel = xp / %d;\n", samples == 1 ? factor : blkW); WRITE(p, " int xb = xel / %d;\n", blkH); WRITE(p, " int xoff = xel - xb * %d;\n", blkH); - - WRITE(p, " sampleUv.x = xib + xb * %d;\n", blkW); - WRITE(p, " sampleUv.y = yb + xoff;\n"); -} - -// block dimensions : widthStride, heightStride -// texture dims : width, height, x offset, y offset -void Write32BitSwizzler(char*& p, u32 format, API_TYPE ApiType) -{ - // [0] left, top, right, bottom of source rectangle within source texture - // [1] width and height of destination texture in pixels - // Two were merged for GLSL - WRITE(p, "uniform float4 " I_COLORS";\n"); - - int blkW = TexDecoder_GetBlockWidthInTexels(format); - int blkH = TexDecoder_GetBlockHeightInTexels(format); - - // 32 bit textures (RGBA8 and Z24) are store in 2 cache line increments - if (ApiType == API_OPENGL) - { - WRITE(p, "#define samp0 samp9\n"); - WRITE(p, "uniform sampler2D samp0;\n"); - - WRITE(p, " out float4 ocol0;\n"); - WRITE(p, "void main()\n"); - } - else - { - WRITE(p,"sampler samp0 : register(s0);\n"); - WRITE(p, "Texture2D Tex0 : register(t0);\n"); - - WRITE(p,"void main(\n"); - WRITE(p," out float4 ocol0 : SV_Target)\n"); - } - - - WRITE(p, "{\n" - " int2 sampleUv;\n" - " int2 uv1 = int2(gl_FragCoord.xy);\n"); - - WRITE(p, " int yl = uv1.y / %d;\n", blkH); - WRITE(p, " int yb = yl * %d;\n", blkH); - WRITE(p, " int yoff = uv1.y - yb;\n"); - WRITE(p, " int xp = uv1.x + yoff * int(" I_COLORS".z);\n"); - WRITE(p, " int xel = xp / 2;\n"); - WRITE(p, " int xb = xel / %d;\n", blkH); - WRITE(p, " int xoff = xel - xb * %d;\n", blkH); - - WRITE(p, " int x2 = uv1.x * 2;\n"); - WRITE(p, " int xl = x2 / %d;\n", blkW); - WRITE(p, " int xib = x2 - xl * %d;\n", blkW); - WRITE(p, " int halfxb = xb / 2;\n"); + WRITE(p, " int xl = uv1.x * %d / %d;\n", factor, blkW); + WRITE(p, " int xib = uv1.x * %d - xl * %d;\n", factor, blkW); + WRITE(p, " int halfxb = xb / %d;\n", factor); WRITE(p, " sampleUv.x = xib + halfxb * %d;\n", blkW); WRITE(p, " sampleUv.y = yb + xoff;\n"); @@ -161,13 +111,13 @@ void Write32BitSwizzler(char*& p, u32 format, API_TYPE ApiType) void WriteSampleColor(char*& p, const char* colorComp, const char* dest, API_TYPE ApiType) { WRITE(p, - "{\n" - "float2 uv = sampleUv + int2(%d,0);\n" // pixel offset - "uv *= " I_COLORS".w;\n" // scale by two (if wanted) - "uv += " I_COLORS".xy;\n" // move to copyed rect - "uv += float2(0.5, 0.5);\n" // move center of pixel - "uv /= float2(%d, %d);\n" // normlize to [0:1] - "uv.y = 1-uv.y;\n" // ogl foo (disable this line for d3d) + "{\n" // sampleUv is the sample position in (int)gx_coords + "float2 uv = float2(sampleUv) + int2(%d,0);\n" // pixel offset (if more than one pixel is samped) + "uv *= " I_COLORS".w;\n" // scale by two (if wanted) + "uv += " I_COLORS".xy;\n" // move to copyed rect + "uv += float2(0.5, 0.5);\n" // move to center of pixel + "uv /= float2(%d, %d);\n" // normlize to [0:1] + "uv.y = 1.0-uv.y;\n" // ogl foo (disable this line for d3d) "%s = texture(samp0, uv).%s;\n" "}\n", s_incrementSampleXCount, EFB_WIDTH, EFB_HEIGHT, dest, colorComp @@ -462,7 +412,7 @@ void WriteRGBA4443Encoder(char* p,API_TYPE ApiType) void WriteRGBA8Encoder(char* p,API_TYPE ApiType) { - Write32BitSwizzler(p, GX_TF_RGBA8, ApiType); + WriteSwizzler(p, GX_TF_RGBA8, ApiType); WRITE(p, " float cl1 = xb - (halfxb * 2.0);\n"); WRITE(p, " float cl0 = 1.0 - cl1;\n"); @@ -687,7 +637,7 @@ void WriteZ16LEncoder(char* p,API_TYPE ApiType) void WriteZ24Encoder(char* p, API_TYPE ApiType) { - Write32BitSwizzler(p, GX_TF_Z24X8, ApiType); + WriteSwizzler(p, GX_TF_Z24X8, ApiType); WRITE(p, " float cl = xb - (halfxb * 2.0);\n"); From 64a1969e36960ab066c2130a276d005aa272bba9 Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 16:34:08 +0100 Subject: [PATCH 134/202] TextureConverter: fix scoping --- .../Src/TextureConversionShader.cpp | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp index bf625223dd32..92c38ca77d67 100644 --- a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp +++ b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp @@ -89,7 +89,9 @@ void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) WRITE(p, "{\n" " int2 sampleUv;\n" - " int2 uv1 = int2(gl_FragCoord.xy);\n"); + " int2 uv1 = int2(gl_FragCoord.xy);\n" + " float2 uv0 = float2(0.0, 0.0);\n" + ); WRITE(p, " uv1.x = uv1.x * %d;\n", samples); @@ -110,16 +112,14 @@ void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) void WriteSampleColor(char*& p, const char* colorComp, const char* dest, API_TYPE ApiType) { - WRITE(p, - "{\n" // sampleUv is the sample position in (int)gx_coords - "float2 uv = float2(sampleUv) + int2(%d,0);\n" // pixel offset (if more than one pixel is samped) - "uv *= " I_COLORS".w;\n" // scale by two (if wanted) - "uv += " I_COLORS".xy;\n" // move to copyed rect - "uv += float2(0.5, 0.5);\n" // move to center of pixel - "uv /= float2(%d, %d);\n" // normlize to [0:1] - "uv.y = 1.0-uv.y;\n" // ogl foo (disable this line for d3d) - "%s = texture(samp0, uv).%s;\n" - "}\n", + WRITE(p, // sampleUv is the sample position in (int)gx_coords + "uv0 = float2(sampleUv) + int2(%d,0);\n" // pixel offset (if more than one pixel is samped) + "uv0 *= " I_COLORS".w;\n" // scale by two (if wanted) + "uv0 += " I_COLORS".xy;\n" // move to copyed rect + "uv0 += float2(0.5, 0.5);\n" // move to center of pixel + "uv0 /= float2(%d, %d);\n" // normlize to [0:1] + "uv0.y = 1.0-uv0.y;\n" // ogl foo (disable this line for d3d) + "%s = texture(samp0, uv0).%s;\n", s_incrementSampleXCount, EFB_WIDTH, EFB_HEIGHT, dest, colorComp ); } From 421fd0e16e7de9e599c21fadc2f2b545d95bd1c1 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Mon, 25 Nov 2013 15:36:24 +0000 Subject: [PATCH 135/202] Fix OpenGL ES 3. --- Source/Core/VideoCommon/Src/TextureConversionShader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp index 92c38ca77d67..b67a8994388e 100644 --- a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp +++ b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp @@ -113,7 +113,7 @@ void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) void WriteSampleColor(char*& p, const char* colorComp, const char* dest, API_TYPE ApiType) { WRITE(p, // sampleUv is the sample position in (int)gx_coords - "uv0 = float2(sampleUv) + int2(%d,0);\n" // pixel offset (if more than one pixel is samped) + "uv0 = float2(sampleUv + int2(%d, 0));\n" // pixel offset (if more than one pixel is samped) "uv0 *= " I_COLORS".w;\n" // scale by two (if wanted) "uv0 += " I_COLORS".xy;\n" // move to copyed rect "uv0 += float2(0.5, 0.5);\n" // move to center of pixel From 11973d31c18c342ab5636296a7a34e7d9f8460e4 Mon Sep 17 00:00:00 2001 From: degasus Date: Mon, 25 Nov 2013 17:01:35 +0100 Subject: [PATCH 136/202] TextureConverter: remove WriteIncrementSampleX --- .../Src/TextureConversionShader.cpp | 186 ++++++------------ 1 file changed, 56 insertions(+), 130 deletions(-) diff --git a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp index b67a8994388e..e7db8677eb69 100644 --- a/Source/Core/VideoCommon/Src/TextureConversionShader.cpp +++ b/Source/Core/VideoCommon/Src/TextureConversionShader.cpp @@ -21,7 +21,6 @@ static char text[16384]; static bool IntensityConstantAdded = false; -static int s_incrementSampleXCount = 0; namespace TextureConversionShader { @@ -110,7 +109,7 @@ void WriteSwizzler(char*& p, u32 format, API_TYPE ApiType) WRITE(p, " sampleUv.y = yb + xoff;\n"); } -void WriteSampleColor(char*& p, const char* colorComp, const char* dest, API_TYPE ApiType) +void WriteSampleColor(char*& p, const char* colorComp, const char* dest, int xoffset, API_TYPE ApiType) { WRITE(p, // sampleUv is the sample position in (int)gx_coords "uv0 = float2(sampleUv + int2(%d, 0));\n" // pixel offset (if more than one pixel is samped) @@ -120,7 +119,7 @@ void WriteSampleColor(char*& p, const char* colorComp, const char* dest, API_TYP "uv0 /= float2(%d, %d);\n" // normlize to [0:1] "uv0.y = 1.0-uv0.y;\n" // ogl foo (disable this line for d3d) "%s = texture(samp0, uv0).%s;\n", - s_incrementSampleXCount, EFB_WIDTH, EFB_HEIGHT, dest, colorComp + xoffset, EFB_WIDTH, EFB_HEIGHT, dest, colorComp ); } @@ -135,25 +134,6 @@ void WriteColorToIntensity(char*& p, const char* src, const char* dest) // don't add IntensityConst.a yet, because doing it later is faster and uses less instructions, due to vectorization } -void WriteIncrementSampleX(char*& p,API_TYPE ApiType) -{ - // the shader compiler apparently isn't smart or aggressive enough to recognize that: - // foo1 = lookup(x) - // x = x + increment; - // foo2 = lookup(x) - // x = x + increment; - // foo3 = lookup(x) - // can be replaced with this: - // foo1 = lookup(x + 0.0 * increment) - // foo2 = lookup(x + 1.0 * increment) - // foo3 = lookup(x + 2.0 * increment) - // which looks like the same operations but uses considerably fewer ALU instruction slots. - // thus, instead of using the former method, we only increment a counter internally here, - // and we wait until WriteSampleColor to write out the constant multiplier - // to achieve the increment as in the latter case. - s_incrementSampleXCount++; -} - void WriteToBitDepth(char*& p, u8 depth, const char* src, const char* dest) { WRITE(p, " %s = floor(%s * 255.0 / exp2(8.0 - %d.0));\n", dest, src, depth); @@ -163,7 +143,6 @@ void WriteEncoderEnd(char* p, API_TYPE ApiType) { WRITE(p, "}\n"); IntensityConstantAdded = false; - s_incrementSampleXCount = 0; } void WriteI8Encoder(char* p, API_TYPE ApiType) @@ -171,19 +150,16 @@ void WriteI8Encoder(char* p, API_TYPE ApiType) WriteSwizzler(p, GX_TF_I8, ApiType); WRITE(p, " float3 texSample;\n"); - WriteSampleColor(p, "rgb", "texSample", ApiType); + WriteSampleColor(p, "rgb", "texSample", 0, ApiType); WriteColorToIntensity(p, "texSample", "ocol0.b"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgb", "texSample", ApiType); + WriteSampleColor(p, "rgb", "texSample", 1, ApiType); WriteColorToIntensity(p, "texSample", "ocol0.g"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgb", "texSample", ApiType); + WriteSampleColor(p, "rgb", "texSample", 2, ApiType); WriteColorToIntensity(p, "texSample", "ocol0.r"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgb", "texSample", ApiType); + WriteSampleColor(p, "rgb", "texSample", 3, ApiType); WriteColorToIntensity(p, "texSample", "ocol0.a"); WRITE(p, " ocol0.rgba += IntensityConst.aaaa;\n"); // see WriteColorToIntensity @@ -198,35 +174,28 @@ void WriteI4Encoder(char* p, API_TYPE ApiType) WRITE(p, " float4 color0;\n"); WRITE(p, " float4 color1;\n"); - WriteSampleColor(p, "rgb", "texSample", ApiType); + WriteSampleColor(p, "rgb", "texSample", 0, ApiType); WriteColorToIntensity(p, "texSample", "color0.b"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgb", "texSample", ApiType); + WriteSampleColor(p, "rgb", "texSample", 1, ApiType); WriteColorToIntensity(p, "texSample", "color1.b"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgb", "texSample", ApiType); + WriteSampleColor(p, "rgb", "texSample", 2, ApiType); WriteColorToIntensity(p, "texSample", "color0.g"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgb", "texSample", ApiType); + WriteSampleColor(p, "rgb", "texSample", 3, ApiType); WriteColorToIntensity(p, "texSample", "color1.g"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgb", "texSample", ApiType); + WriteSampleColor(p, "rgb", "texSample", 4, ApiType); WriteColorToIntensity(p, "texSample", "color0.r"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgb", "texSample", ApiType); + WriteSampleColor(p, "rgb", "texSample", 5, ApiType); WriteColorToIntensity(p, "texSample", "color1.r"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgb", "texSample", ApiType); + WriteSampleColor(p, "rgb", "texSample", 6, ApiType); WriteColorToIntensity(p, "texSample", "color0.a"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgb", "texSample", ApiType); + WriteSampleColor(p, "rgb", "texSample", 7, ApiType); WriteColorToIntensity(p, "texSample", "color1.a"); WRITE(p, " color0.rgba += IntensityConst.aaaa;\n"); @@ -244,12 +213,11 @@ void WriteIA8Encoder(char* p,API_TYPE ApiType) WriteSwizzler(p, GX_TF_IA8, ApiType); WRITE(p, " float4 texSample;\n"); - WriteSampleColor(p, "rgba", "texSample", ApiType); + WriteSampleColor(p, "rgba", "texSample", 0, ApiType); WRITE(p, " ocol0.b = texSample.a;\n"); WriteColorToIntensity(p, "texSample", "ocol0.g"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgba", "texSample", ApiType); + WriteSampleColor(p, "rgba", "texSample", 1, ApiType); WRITE(p, " ocol0.r = texSample.a;\n"); WriteColorToIntensity(p, "texSample", "ocol0.a"); @@ -265,22 +233,19 @@ void WriteIA4Encoder(char* p,API_TYPE ApiType) WRITE(p, " float4 color0;\n"); WRITE(p, " float4 color1;\n"); - WriteSampleColor(p, "rgba", "texSample", ApiType); + WriteSampleColor(p, "rgba", "texSample", 0, ApiType); WRITE(p, " color0.b = texSample.a;\n"); WriteColorToIntensity(p, "texSample", "color1.b"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgba", "texSample", ApiType); + WriteSampleColor(p, "rgba", "texSample", 1, ApiType); WRITE(p, " color0.g = texSample.a;\n"); WriteColorToIntensity(p, "texSample", "color1.g"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgba", "texSample", ApiType); + WriteSampleColor(p, "rgba", "texSample", 2, ApiType); WRITE(p, " color0.r = texSample.a;\n"); WriteColorToIntensity(p, "texSample", "color1.r"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgba", "texSample", ApiType); + WriteSampleColor(p, "rgba", "texSample", 3, ApiType); WRITE(p, " color0.a = texSample.a;\n"); WriteColorToIntensity(p, "texSample", "color1.a"); @@ -297,9 +262,8 @@ void WriteRGB565Encoder(char* p,API_TYPE ApiType) { WriteSwizzler(p, GX_TF_RGB565, ApiType); - WriteSampleColor(p, "rgb", "float3 texSample0", ApiType); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgb", "float3 texSample1", ApiType); + WriteSampleColor(p, "rgb", "float3 texSample0", 0, ApiType); + WriteSampleColor(p, "rgb", "float3 texSample1", 1, ApiType); WRITE(p, " float2 texRs = float2(texSample0.r, texSample1.r);\n"); WRITE(p, " float2 texGs = float2(texSample0.g, texSample1.g);\n"); WRITE(p, " float2 texBs = float2(texSample0.b, texSample1.b);\n"); @@ -326,7 +290,7 @@ void WriteRGB5A3Encoder(char* p,API_TYPE ApiType) WRITE(p, " float gUpper;\n"); WRITE(p, " float gLower;\n"); - WriteSampleColor(p, "rgba", "texSample", ApiType); + WriteSampleColor(p, "rgba", "texSample", 0, ApiType); // 0.8784 = 224 / 255 which is the maximum alpha value that can be represented in 3 bits WRITE(p, "if(texSample.a > 0.878f) {\n"); @@ -353,9 +317,8 @@ void WriteRGB5A3Encoder(char* p,API_TYPE ApiType) WRITE(p, "}\n"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "rgba", "texSample", ApiType); + WriteSampleColor(p, "rgba", "texSample", 1, ApiType); WRITE(p, "if(texSample.a > 0.878f) {\n"); @@ -392,15 +355,13 @@ void WriteRGBA4443Encoder(char* p,API_TYPE ApiType) WRITE(p, " float4 color0;\n"); WRITE(p, " float4 color1;\n"); - WriteSampleColor(p, "rgba", "texSample", ApiType); + WriteSampleColor(p, "rgba", "texSample", 0, ApiType); WriteToBitDepth(p, 3, "texSample.a", "color0.b"); WriteToBitDepth(p, 4, "texSample.r", "color1.b"); WriteToBitDepth(p, 4, "texSample.g", "color0.g"); WriteToBitDepth(p, 4, "texSample.b", "color1.g"); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, "rgba", "texSample", ApiType); + WriteSampleColor(p, "rgba", "texSample", 1, ApiType); WriteToBitDepth(p, 3, "texSample.a", "color0.r"); WriteToBitDepth(p, 4, "texSample.r", "color1.r"); WriteToBitDepth(p, 4, "texSample.g", "color0.a"); @@ -421,15 +382,13 @@ void WriteRGBA8Encoder(char* p,API_TYPE ApiType) WRITE(p, " float4 color0;\n"); WRITE(p, " float4 color1;\n"); - WriteSampleColor(p, "rgba", "texSample", ApiType); + WriteSampleColor(p, "rgba", "texSample", 0, ApiType); WRITE(p, " color0.b = texSample.a;\n"); WRITE(p, " color0.g = texSample.r;\n"); WRITE(p, " color1.b = texSample.g;\n"); WRITE(p, " color1.g = texSample.b;\n"); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, "rgba", "texSample", ApiType); + WriteSampleColor(p, "rgba", "texSample", 1, ApiType); WRITE(p, " color0.r = texSample.a;\n"); WRITE(p, " color0.a = texSample.r;\n"); WRITE(p, " color1.r = texSample.g;\n"); @@ -446,28 +405,14 @@ void WriteC4Encoder(char* p, const char* comp,API_TYPE ApiType) WRITE(p, " float4 color0;\n"); WRITE(p, " float4 color1;\n"); - WriteSampleColor(p, comp, "color0.b", ApiType); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, comp, "color1.b", ApiType); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, comp, "color0.g", ApiType); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, comp, "color1.g", ApiType); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, comp, "color0.r", ApiType); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, comp, "color1.r", ApiType); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, comp, "color0.a", ApiType); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, comp, "color1.a", ApiType); + WriteSampleColor(p, comp, "color0.b", 0, ApiType); + WriteSampleColor(p, comp, "color1.b", 1, ApiType); + WriteSampleColor(p, comp, "color0.g", 2, ApiType); + WriteSampleColor(p, comp, "color1.g", 3, ApiType); + WriteSampleColor(p, comp, "color0.r", 4, ApiType); + WriteSampleColor(p, comp, "color1.r", 5, ApiType); + WriteSampleColor(p, comp, "color0.a", 6, ApiType); + WriteSampleColor(p, comp, "color1.a", 7, ApiType); WriteToBitDepth(p, 4, "color0", "color0"); WriteToBitDepth(p, 4, "color1", "color1"); @@ -480,16 +425,10 @@ void WriteC8Encoder(char* p, const char* comp,API_TYPE ApiType) { WriteSwizzler(p, GX_CTF_R8, ApiType); - WriteSampleColor(p, comp, "ocol0.b", ApiType); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, comp, "ocol0.g", ApiType); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, comp, "ocol0.r", ApiType); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, comp, "ocol0.a", ApiType); + WriteSampleColor(p, comp, "ocol0.b", 0, ApiType); + WriteSampleColor(p, comp, "ocol0.g", 1, ApiType); + WriteSampleColor(p, comp, "ocol0.r", 2, ApiType); + WriteSampleColor(p, comp, "ocol0.a", 3, ApiType); WriteEncoderEnd(p, ApiType); } @@ -501,22 +440,19 @@ void WriteCC4Encoder(char* p, const char* comp,API_TYPE ApiType) WRITE(p, " float4 color0;\n"); WRITE(p, " float4 color1;\n"); - WriteSampleColor(p, comp, "texSample", ApiType); + WriteSampleColor(p, comp, "texSample", 0, ApiType); WRITE(p, " color0.b = texSample.x;\n"); WRITE(p, " color1.b = texSample.y;\n"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, comp, "texSample", ApiType); + WriteSampleColor(p, comp, "texSample", 1, ApiType); WRITE(p, " color0.g = texSample.x;\n"); WRITE(p, " color1.g = texSample.y;\n"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, comp, "texSample", ApiType); + WriteSampleColor(p, comp, "texSample", 2, ApiType); WRITE(p, " color0.r = texSample.x;\n"); WRITE(p, " color1.r = texSample.y;\n"); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, comp, "texSample", ApiType); + WriteSampleColor(p, comp, "texSample", 3, ApiType); WRITE(p, " color0.a = texSample.x;\n"); WRITE(p, " color1.a = texSample.y;\n"); @@ -531,10 +467,8 @@ void WriteCC8Encoder(char* p, const char* comp, API_TYPE ApiType) { WriteSwizzler(p, GX_CTF_RA8, ApiType); - WriteSampleColor(p, comp, "ocol0.bg", ApiType); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, comp, "ocol0.ra", ApiType); + WriteSampleColor(p, comp, "ocol0.bg", 0, ApiType); + WriteSampleColor(p, comp, "ocol0.ra", 1, ApiType); WriteEncoderEnd(p, ApiType); } @@ -545,19 +479,16 @@ void WriteZ8Encoder(char* p, const char* multiplier,API_TYPE ApiType) WRITE(p, " float depth;\n"); - WriteSampleColor(p, "b", "depth", ApiType); + WriteSampleColor(p, "b", "depth", 0, ApiType); WRITE(p, "ocol0.b = frac(depth * %s);\n", multiplier); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "b", "depth", ApiType); + WriteSampleColor(p, "b", "depth", 1, ApiType); WRITE(p, "ocol0.g = frac(depth * %s);\n", multiplier); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "b", "depth", ApiType); + WriteSampleColor(p, "b", "depth", 2, ApiType); WRITE(p, "ocol0.r = frac(depth * %s);\n", multiplier); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "b", "depth", ApiType); + WriteSampleColor(p, "b", "depth", 3, ApiType); WRITE(p, "ocol0.a = frac(depth * %s);\n", multiplier); WriteEncoderEnd(p, ApiType); @@ -572,7 +503,7 @@ void WriteZ16Encoder(char* p,API_TYPE ApiType) // byte order is reversed - WriteSampleColor(p, "b", "depth", ApiType); + WriteSampleColor(p, "b", "depth", 0, ApiType); WRITE(p, " depth *= 16777215.0;\n"); WRITE(p, " expanded.r = floor(depth / (256.0 * 256.0));\n"); @@ -582,9 +513,7 @@ void WriteZ16Encoder(char* p,API_TYPE ApiType) WRITE(p, " ocol0.b = expanded.g / 255.0;\n"); WRITE(p, " ocol0.g = expanded.r / 255.0;\n"); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, "b", "depth", ApiType); + WriteSampleColor(p, "b", "depth", 1, ApiType); WRITE(p, " depth *= 16777215.0;\n"); WRITE(p, " expanded.r = floor(depth / (256.0 * 256.0));\n"); @@ -606,7 +535,7 @@ void WriteZ16LEncoder(char* p,API_TYPE ApiType) // byte order is reversed - WriteSampleColor(p, "b", "depth", ApiType); + WriteSampleColor(p, "b", "depth", 0, ApiType); WRITE(p, " depth *= 16777215.0;\n"); WRITE(p, " expanded.r = floor(depth / (256.0 * 256.0));\n"); @@ -618,9 +547,7 @@ void WriteZ16LEncoder(char* p,API_TYPE ApiType) WRITE(p, " ocol0.b = expanded.b / 255.0;\n"); WRITE(p, " ocol0.g = expanded.g / 255.0;\n"); - WriteIncrementSampleX(p, ApiType); - - WriteSampleColor(p, "b", "depth", ApiType); + WriteSampleColor(p, "b", "depth", 1, ApiType); WRITE(p, " depth *= 16777215.0;\n"); WRITE(p, " expanded.r = floor(depth / (256.0 * 256.0));\n"); @@ -646,9 +573,8 @@ void WriteZ24Encoder(char* p, API_TYPE ApiType) WRITE(p, " float3 expanded0;\n"); WRITE(p, " float3 expanded1;\n"); - WriteSampleColor(p, "b", "depth0", ApiType); - WriteIncrementSampleX(p, ApiType); - WriteSampleColor(p, "b", "depth1", ApiType); + WriteSampleColor(p, "b", "depth0", 0, ApiType); + WriteSampleColor(p, "b", "depth1", 1, ApiType); for (int i = 0; i < 2; i++) { From 7718c9959e6185cc72520072c28a07406c02e6ef Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Mon, 25 Nov 2013 10:58:05 -0600 Subject: [PATCH 137/202] [Android-overlay] Multiple physical gamepad support. --- .../DolphinWX/Src/Android/ButtonManager.cpp | 77 +++++++++++-------- .../DolphinWX/Src/Android/ButtonManager.h | 17 ++-- Source/Core/DolphinWX/Src/MainAndroid.cpp | 4 +- 3 files changed, 54 insertions(+), 44 deletions(-) diff --git a/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp b/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp index b33148338d43..5fa7bc799a62 100644 --- a/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp +++ b/Source/Core/DolphinWX/Src/Android/ButtonManager.cpp @@ -28,7 +28,7 @@ namespace ButtonManager std::map, Button*> m_buttons; std::map, Axis*> m_axises; std::unordered_map m_controllers; - const char *configStrings[] = { "InputA", + const char* configStrings[] = { "InputA", "InputB", "InputStart", "InputX", @@ -94,29 +94,34 @@ namespace ButtonManager ini.Load(File::GetUserPath(D_CONFIG_IDX) + std::string("Dolphin.ini")); for (int a = 0; a < configStringNum; ++a) { - BindType type; - int bindnum; - char dev[128]; - bool hasbind = false; - char modifier = 0; - std::string value; - ini.Get("Android", configStrings[a], &value, "None"); - if (value == "None") - continue; - if (std::string::npos != value.find("Axis")) + for (int padID = 0; padID < 4; ++padID) { - hasbind = true; - type = BIND_AXIS; - sscanf(value.c_str(), "Device '%[^\']'-Axis %d%c", dev, &bindnum, &modifier); + std::ostringstream config; + config << configStrings[a] << "_" << padID; + BindType type; + int bindnum; + char dev[128]; + bool hasbind = false; + char modifier = 0; + std::string value; + ini.Get("Android", config.str().c_str(), &value, "None"); + if (value == "None") + continue; + if (std::string::npos != value.find("Axis")) + { + hasbind = true; + type = BIND_AXIS; + sscanf(value.c_str(), "Device '%[^\']'-Axis %d%c", dev, &bindnum, &modifier); + } + else if (std::string::npos != value.find("Button")) + { + hasbind = true; + type = BIND_BUTTON; + sscanf(value.c_str(), "Device '%[^\']'-Button %d", dev, &bindnum); + } + if (hasbind) + AddBind(std::string(dev), new sBind(padID, (ButtonType)a, type, bindnum, modifier == '-' ? -1.0f : 1.0f)); } - else if (std::string::npos != value.find("Button")) - { - hasbind = true; - type = BIND_BUTTON; - sscanf(value.c_str(), "Device '%[^\']'-Button %d", dev, &bindnum); - } - if (hasbind) - AddBind(std::string(dev), new sBind((ButtonType)a, type, bindnum, modifier == '-' ? -1.0f : 1.0f)); } } @@ -126,7 +131,7 @@ namespace ButtonManager pressed = m_buttons[std::make_pair(padID, button)]->Pressed(); for (auto it = m_controllers.begin(); it != m_controllers.end(); ++it) - pressed |= it->second->ButtonValue(button); + pressed |= it->second->ButtonValue(padID, button); return pressed; } @@ -138,7 +143,7 @@ namespace ButtonManager auto it = m_controllers.begin(); if (it == m_controllers.end()) return value; - return it->second->AxisValue(axis); + return it->second->AxisValue(padID, axis); } void TouchEvent(int padID, int button, int action) { @@ -148,7 +153,7 @@ namespace ButtonManager { m_axises[std::make_pair(padID, axis)]->SetValue(value); } - void GamepadEvent(std::string dev, int button, int action) + void GamepadEvent(std::string dev, ButtonType button, int action) { auto it = m_controllers.find(dev); if (it != m_controllers.end()) @@ -159,7 +164,7 @@ namespace ButtonManager m_controllers[dev] = new InputDevice(dev); m_controllers[dev]->PressEvent(button, action); } - void GamepadAxisEvent(std::string dev, int axis, float value) + void GamepadAxisEvent(std::string dev, ButtonType axis, float value) { auto it = m_controllers.find(dev); if (it != m_controllers.end()) @@ -181,31 +186,35 @@ namespace ButtonManager } // InputDevice - void InputDevice::PressEvent(int button, int action) + void InputDevice::PressEvent(ButtonType button, int action) { - m_buttons[button] = action == 0 ? true : false; + m_buttons[m_binds[button]->m_bind] = action == 0 ? true : false; } - void InputDevice::AxisEvent(int axis, float value) + void InputDevice::AxisEvent(ButtonType axis, float value) { - m_axises[axis] = value; + m_axises[m_binds[axis]->m_bind] = value; } - bool InputDevice::ButtonValue(ButtonType button) + bool InputDevice::ButtonValue(int padID, ButtonType button) { auto it = m_binds.find(button); if (it == m_binds.end()) return false; + if (it->second->m_padID != padID) + return false; if (it->second->m_bindtype == BIND_BUTTON) return m_buttons[it->second->m_bind]; else - return AxisValue(button); + return AxisValue(padID, button); } - float InputDevice::AxisValue(ButtonType axis) + float InputDevice::AxisValue(int padID, ButtonType axis) { auto it = m_binds.find(axis); if (it == m_binds.end()) return 0.0f; + if (it->second->m_padID != padID) + return 0.0f; if (it->second->m_bindtype == BIND_BUTTON) - return ButtonValue(axis); + return ButtonValue(padID, axis); else return m_axises[it->second->m_bind] * it->second->m_neg; } diff --git a/Source/Core/DolphinWX/Src/Android/ButtonManager.h b/Source/Core/DolphinWX/Src/Android/ButtonManager.h index f94b3596bcf6..e6ce2ff8e0b4 100644 --- a/Source/Core/DolphinWX/Src/Android/ButtonManager.h +++ b/Source/Core/DolphinWX/Src/Android/ButtonManager.h @@ -85,12 +85,13 @@ namespace ButtonManager struct sBind { + const int m_padID; const ButtonType m_buttontype; const BindType m_bindtype; const int m_bind; const float m_neg; - sBind(ButtonType buttontype, BindType bindtype, int bind, float neg) - : m_buttontype(buttontype), m_bindtype(bindtype), m_bind(bind), m_neg(neg) + sBind(int padID, ButtonType buttontype, BindType bindtype, int bind, float neg) + : m_padID(padID), m_buttontype(buttontype), m_bindtype(bindtype), m_bind(bind), m_neg(neg) {} }; @@ -113,10 +114,10 @@ namespace ButtonManager delete it->second; } void AddBind(sBind *bind) { m_binds[bind->m_buttontype] = bind; } - void PressEvent(int button, int action); - void AxisEvent(int axis, float value); - bool ButtonValue(ButtonType button); - float AxisValue(ButtonType axis); + void PressEvent(ButtonType button, int action); + void AxisEvent(ButtonType axis, float value); + bool ButtonValue(int padID, ButtonType button); + float AxisValue(int padID, ButtonType axis); }; void Init(); @@ -124,7 +125,7 @@ namespace ButtonManager float GetAxisValue(int padID, ButtonType axis); void TouchEvent(int padID, int button, int action); void TouchAxisEvent(int padID, int axis, float value); - void GamepadEvent(std::string dev, int button, int action); - void GamepadAxisEvent(std::string dev, int axis, float value); + void GamepadEvent(std::string dev, ButtonType button, int action); + void GamepadAxisEvent(std::string dev, ButtonType axis, float value); void Shutdown(); } diff --git a/Source/Core/DolphinWX/Src/MainAndroid.cpp b/Source/Core/DolphinWX/Src/MainAndroid.cpp index 949724bcf6ee..c0421d99e3ce 100644 --- a/Source/Core/DolphinWX/Src/MainAndroid.cpp +++ b/Source/Core/DolphinWX/Src/MainAndroid.cpp @@ -249,14 +249,14 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onGamePadEve { const char *Device = env->GetStringUTFChars(jDevice, NULL); std::string strDevice = std::string(Device); - ButtonManager::GamepadEvent(strDevice, Button, Action); + ButtonManager::GamepadEvent(strDevice, (ButtonManager::ButtonType)Button, Action); env->ReleaseStringUTFChars(jDevice, Device); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onGamePadMoveEvent(JNIEnv *env, jobject obj, jstring jDevice, jint Axis, jfloat Value) { const char *Device = env->GetStringUTFChars(jDevice, NULL); std::string strDevice = std::string(Device); - ButtonManager::GamepadAxisEvent(strDevice, Axis, Value); + ButtonManager::GamepadAxisEvent(strDevice, (ButtonManager::ButtonType)Axis, Value); env->ReleaseStringUTFChars(jDevice, Device); } From 76843b450bdfc11fc91128b2ce81442611b8d6d7 Mon Sep 17 00:00:00 2001 From: Lioncash Date: Mon, 25 Nov 2013 14:23:28 -0500 Subject: [PATCH 138/202] [Android] Build the configuration window for the overlay programmatically. Moved the overlay configuration classes into their own package. Also launch the overlay config activity through the preference XML via an embedded Intent. Lets us remove code explicitly handling this. --- Source/Android/AndroidManifest.xml | 2 +- .../layout/input_overlay_config_layout.xml | 56 ------------------- Source/Android/res/xml/input_prefs.xml | 6 +- .../settings/input/InputConfigFragment.java | 16 ------ .../input/InputOverlayConfigActivity.java | 27 --------- .../overlayconfig/OverlayConfigActivity.java | 39 +++++++++++++ .../OverlayConfigButton.java} | 41 +++++++++----- 7 files changed, 73 insertions(+), 114 deletions(-) delete mode 100644 Source/Android/res/layout/input_overlay_config_layout.xml delete mode 100644 Source/Android/src/org/dolphinemu/dolphinemu/settings/input/InputOverlayConfigActivity.java create mode 100644 Source/Android/src/org/dolphinemu/dolphinemu/settings/input/overlayconfig/OverlayConfigActivity.java rename Source/Android/src/org/dolphinemu/dolphinemu/settings/input/{InputOverlayConfigButton.java => overlayconfig/OverlayConfigButton.java} (60%) diff --git a/Source/Android/AndroidManifest.xml b/Source/Android/AndroidManifest.xml index b913f28d1d38..b97809f5e131 100644 --- a/Source/Android/AndroidManifest.xml +++ b/Source/Android/AndroidManifest.xml @@ -40,7 +40,7 @@ android:screenOrientation="landscape" /> diff --git a/Source/Android/res/layout/input_overlay_config_layout.xml b/Source/Android/res/layout/input_overlay_config_layout.xml deleted file mode 100644 index f1261d3c3523..000000000000 --- a/Source/Android/res/layout/input_overlay_config_layout.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/Source/Android/res/xml/input_prefs.xml b/Source/Android/res/xml/input_prefs.xml index 22cf0117e9bf..ed85d634a88a 100644 --- a/Source/Android/res/xml/input_prefs.xml +++ b/Source/Android/res/xml/input_prefs.xml @@ -5,7 +5,11 @@ + android:title="@string/input_overlay_layout"> + + Date: Mon, 25 Nov 2013 22:27:11 +0100 Subject: [PATCH 139/202] OpenGL: split real xfb + efb2ram framebuffers --- .../OGL/Src/TextureConverter.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index 7f8ae23cc90e..d2cb5b0414d0 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -26,7 +26,7 @@ namespace TextureConverter using OGL::TextureCache; -static GLuint s_texConvFrameBuffer = 0; +static GLuint s_texConvFrameBuffer[2] = {0,0}; static GLuint s_srcTexture = 0; // for decoding from RAM static GLuint s_dstTexture = 0; // for encoding to RAM @@ -161,7 +161,7 @@ SHADER &GetOrCreateEncodingShader(u32 format) void Init() { - glGenFramebuffers(1, &s_texConvFrameBuffer); + glGenFramebuffers(2, s_texConvFrameBuffer); glActiveTexture(GL_TEXTURE0 + 9); glGenTextures(1, &s_srcTexture); @@ -172,6 +172,10 @@ void Init() glBindTexture(GL_TEXTURE_2D, s_dstTexture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, renderBufferWidth, renderBufferHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + + + FramebufferManager::SetFramebuffer(s_texConvFrameBuffer[0]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, s_dstTexture, 0); CreatePrograms(); } @@ -180,7 +184,7 @@ void Shutdown() { glDeleteTextures(1, &s_srcTexture); glDeleteTextures(1, &s_dstTexture); - glDeleteFramebuffers(1, &s_texConvFrameBuffer); + glDeleteFramebuffers(2, s_texConvFrameBuffer); s_rgbToYuyvProgram.Destroy(); s_yuyvToRgbProgram.Destroy(); @@ -190,7 +194,8 @@ void Shutdown() s_srcTexture = 0; s_dstTexture = 0; - s_texConvFrameBuffer = 0; + s_texConvFrameBuffer[0] = 0; + s_texConvFrameBuffer[1] = 0; } void EncodeToRamUsingShader(GLuint srcTexture, const TargetRectangle& sourceRc, @@ -201,9 +206,7 @@ void EncodeToRamUsingShader(GLuint srcTexture, const TargetRectangle& sourceRc, // switch to texture converter frame buffer // attach render buffer as color destination - FramebufferManager::SetFramebuffer(s_texConvFrameBuffer); - - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, s_dstTexture, 0); + FramebufferManager::SetFramebuffer(s_texConvFrameBuffer[0]); GL_REPORT_ERRORD(); // set source texture @@ -347,7 +350,7 @@ void DecodeToTexture(u32 xfbAddr, int srcWidth, int srcHeight, GLuint destTextur // switch to texture converter frame buffer // attach destTexture as color destination - FramebufferManager::SetFramebuffer(s_texConvFrameBuffer); + FramebufferManager::SetFramebuffer(s_texConvFrameBuffer[1]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, destTexture, 0); GL_REPORT_FBO_ERROR(); From 7bacaec75bdac3c65653e4153ca69b183e8c6bcc Mon Sep 17 00:00:00 2001 From: comex Date: Mon, 25 Nov 2013 16:11:40 -0500 Subject: [PATCH 140/202] Aargh. Dynamic pad mapping seems to be working. --- Source/Core/Common/Src/ChunkFile.h | 4 +- Source/Core/Common/Src/NetHost.cpp | 12 + Source/Core/Common/Src/NetHost.h | 1 + Source/Core/Core/Src/HW/SI.cpp | 28 +- Source/Core/Core/Src/HW/SI.h | 5 +- Source/Core/Core/Src/HW/SI_Device.h | 3 +- Source/Core/Core/Src/IOSync.cpp | 6 +- Source/Core/Core/Src/IOSync.h | 17 +- Source/Core/Core/Src/IOSyncBackends.cpp | 188 +++++++--- Source/Core/Core/Src/IOSyncBackends.h | 12 +- Source/Core/Core/Src/Movie.cpp | 2 +- Source/Core/Core/Src/NetPlayClient.cpp | 31 +- Source/Core/Core/Src/NetPlayClient.h | 2 +- Source/Core/Core/Src/NetPlayProto.h | 10 + Source/Core/Core/Src/NetPlayServer.cpp | 442 +++++++++++++++++++---- Source/Core/Core/Src/NetPlayServer.h | 44 ++- Source/Core/DolphinWX/Src/ConfigMain.cpp | 7 +- Source/Core/DolphinWX/Src/NetWindow.cpp | 9 +- 18 files changed, 658 insertions(+), 165 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index 3173840fc149..a3da67d5a389 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -508,9 +508,9 @@ class Packet : public PointerWrap // Write an rvalue. template - void W(T t) + void W(const T& t) { - PointerWrap::Do(t); + PointerWrap::Do((T&) t); } private: diff --git a/Source/Core/Common/Src/NetHost.cpp b/Source/Core/Common/Src/NetHost.cpp index 0d1d3719a977..f5e98c2db92d 100644 --- a/Source/Core/Common/Src/NetHost.cpp +++ b/Source/Core/Common/Src/NetHost.cpp @@ -112,6 +112,18 @@ void NetHost::RunOnThreadSync(std::function func) evt.Wait(); } +void NetHost::RunOnThisThreadSync(std::function func) +{ + Common::Event evt, evt2; + RunOnThread([&]() { + evt.Set(); + evt2.Wait(); + }); + evt.Wait(); + func(); + evt2.Set(); +} + void NetHost::Reset() { // Sync up with the thread and disconnect everyone. diff --git a/Source/Core/Common/Src/NetHost.h b/Source/Core/Common/Src/NetHost.h index 4a7179282215..49b2c4a11380 100644 --- a/Source/Core/Common/Src/NetHost.h +++ b/Source/Core/Common/Src/NetHost.h @@ -86,6 +86,7 @@ class NetHost ~NetHost(); void RunOnThread(std::function func) NOT_ON(NET); void RunOnThreadSync(std::function func) NOT_ON(NET); + void RunOnThisThreadSync(std::function func) NOT_ON(NET); void CreateThread(); void Reset(); diff --git a/Source/Core/Core/Src/HW/SI.cpp b/Source/Core/Core/Src/HW/SI.cpp index c862e988c99c..d36889eff77c 100644 --- a/Source/Core/Core/Src/HW/SI.cpp +++ b/Source/Core/Core/Src/HW/SI.cpp @@ -210,9 +210,14 @@ static u8 g_SIBuffer[128]; static int changeDevice[4]; } -void SISyncClass::OnConnected(int channel, PWBuffer&& subtype) +void SISyncClass::PreInit() { - IOSync::Class::OnConnected(channel, std::move(subtype)); + SerialInterface::PreInit(); +} + +void SISyncClass::OnConnected(int channel, int localIndex, PWBuffer&& subtype) +{ + IOSync::Class::OnConnected(channel, localIndex, std::move(subtype)); SIDevices type = GrabSubtype(GetSubtype(channel)); CoreTiming::RemoveAllEvents(SerialInterface::changeDevice[channel]); @@ -262,9 +267,17 @@ void DoState(PointerWrap &p) p.Do(g_SIBuffer); } +void PreInit() +{ + for (int i = 0; i < NUMBER_OF_CHANNELS; i++) + { + ChangeLocalDevice(SConfig::GetInstance().m_SIDevice[i], i); + } +} void Init() { + PreInit(); for (int i = 0; i < 4; i++) { char buf[64]; @@ -279,7 +292,6 @@ void Init() g_Channel[i].m_InLo.Hex = 0; AddDevice(SIDEVICE_NONE, i); - ChangeDevice(SConfig::GetInstance().m_SIDevice[i], i); } g_Poll.Hex = 0; @@ -611,9 +623,17 @@ void ChangeDeviceCallback(u64 userdata, int cyclesLate) AddDevice((SIDevices)(u32)userdata, channel); } -void ChangeDevice(SIDevices device, int channel) +void ChangeLocalDevice(SIDevices device, int channel) { + SIDevices old; if (g_SISyncClass.LocalIsConnected(channel)) + old = g_SISyncClass.GrabSubtype(g_SISyncClass.GetLocalSubtype(channel)); + else + old = SIDEVICE_NONE; + + if (old == device) + return; + if (old != SIDEVICE_NONE) g_SISyncClass.DisconnectLocalDevice(channel); if (device != SIDEVICE_NONE) g_SISyncClass.ConnectLocalDevice(channel, g_SISyncClass.PushSubtype(device)); diff --git a/Source/Core/Core/Src/HW/SI.h b/Source/Core/Core/Src/HW/SI.h index c5c8b0dbca20..39c82209fe46 100644 --- a/Source/Core/Core/Src/HW/SI.h +++ b/Source/Core/Core/Src/HW/SI.h @@ -13,6 +13,9 @@ class ISIDevice; namespace SerialInterface { +// Run before the game starts. +void PreInit(); + void Init(); void Shutdown(); void DoState(PointerWrap &p); @@ -24,7 +27,7 @@ void AddDevice(const SIDevices _device, int _iDeviceNumber); void AddDevice(ISIDevice* pDevice); void ChangeDeviceCallback(u64 userdata, int cyclesLate); -void ChangeDevice(SIDevices device, int channel); +void ChangeLocalDevice(SIDevices device, int channel); void Read32(u32& _uReturnValue, const u32 _iAddress); void Write32(const u32 _iValue, const u32 _iAddress); diff --git a/Source/Core/Core/Src/HW/SI_Device.h b/Source/Core/Core/Src/HW/SI_Device.h index 212d1d6bc971..bdf4132f840d 100644 --- a/Source/Core/Core/Src/HW/SI_Device.h +++ b/Source/Core/Core/Src/HW/SI_Device.h @@ -60,7 +60,8 @@ class SISyncClass : public IOSync::Class { public: SISyncClass() : IOSync::Class(ClassSI) {} - virtual void OnConnected(int index, PWBuffer&& subtype) override; + virtual void PreInit() override; + virtual void OnConnected(int index, int localIndxe, PWBuffer&& subtype) override; virtual void OnDisconnected(int index) override; virtual int GetMaxDeviceIndex() override { return 4; } }; diff --git a/Source/Core/Core/Src/IOSync.cpp b/Source/Core/Core/Src/IOSync.cpp index acb215ddfecc..743ce7658837 100644 --- a/Source/Core/Core/Src/IOSync.cpp +++ b/Source/Core/Core/Src/IOSync.cpp @@ -22,15 +22,18 @@ void Class::SetIndex(int index, int localIndex) } } -void Class::OnConnected(int index, PWBuffer&& subtype) +void Class::OnConnected(int index, int localIndex, PWBuffer&& subtype) { + SetIndex(index, localIndex); m_Remote[index].m_Subtype = std::move(subtype); m_Remote[index].m_IsConnected = true; } void Class::OnDisconnected(int index) { + SetIndex(index, -1); m_Remote[index] = DeviceInfo(); + m_Remote[index].m_IsConnected = false; } void Class::DeviceInfo::DoState(PointerWrap& p) @@ -53,6 +56,7 @@ void Init() { if (!g_Backend) ResetBackend(); + g_Backend->StartGame(); } void ResetBackend() diff --git a/Source/Core/Core/Src/IOSync.h b/Source/Core/Core/Src/IOSync.h index 82c27ac37272..0231a491a1b7 100644 --- a/Source/Core/Core/Src/IOSync.h +++ b/Source/Core/Core/Src/IOSync.h @@ -28,6 +28,7 @@ class Backend virtual void OnPacketError() = 0; virtual u32 GetTime() = 0; virtual void DoState(PointerWrap& p) = 0; + virtual void StartGame() {} virtual void NewLocalSubframe() {} }; @@ -74,11 +75,14 @@ class Class // and is sent along with the connection notice. void ConnectLocalDevice(int localIndex, PWBuffer&& subtypeData) { + m_Local[localIndex].m_IsConnected = true; + m_Local[localIndex].m_Subtype = subtypeData.copy(); g_Backend->ConnectLocalDevice(m_ClassId, localIndex, std::move(subtypeData)); } void DisconnectLocalDevice(int localIndex) { + m_Local[localIndex].m_IsConnected = false; g_Backend->DisconnectLocalDevice(m_ClassId, localIndex); } @@ -106,14 +110,14 @@ class Class return &m_Local[index].m_Subtype; } - const bool& LocalIsConnected(int index) + const bool& IsConnected(int index) { - return m_Local[index].m_IsConnected; + return m_Remote[index].m_IsConnected; } - const bool& IsConnected(int index) + bool LocalIsConnected(int index) { - return m_Remote[index].m_IsConnected; + return m_Local[index].m_IsConnected; } template @@ -156,7 +160,8 @@ class Class } // These should be called on thread. - virtual void OnConnected(int index, PWBuffer&& subtype); + virtual void PreInit() {} + virtual void OnConnected(int index, int localIndex, PWBuffer&& subtype); virtual void OnDisconnected(int index); virtual void DoState(PointerWrap& p); virtual int GetMaxDeviceIndex() = 0; @@ -189,7 +194,7 @@ class EXISyncClass : public IOSync::Class { public: EXISyncClass() : IOSync::Class(ClassEXI) {} - virtual void OnConnected(int index, PWBuffer&& subtype) override {}; + virtual void OnConnected(int index, int localIndex, PWBuffer&& subtype) override {}; virtual void OnDisconnected(int index) override {}; virtual int GetMaxDeviceIndex() override { return 2; } }; diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index 8fdb878c8462..e543b5dc1820 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -8,13 +8,11 @@ namespace IOSync void BackendLocal::ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf) { - g_Classes[classId]->SetIndex(localIndex, localIndex); - g_Classes[classId]->OnConnected(localIndex, std::move(buf)); + g_Classes[classId]->OnConnected(localIndex, localIndex, std::move(buf)); } void BackendLocal::DisconnectLocalDevice(int classId, int localIndex) { - g_Classes[classId]->SetIndex(-1, localIndex); g_Classes[classId]->OnDisconnected(localIndex); } @@ -56,8 +54,7 @@ void BackendLocal::DoState(PointerWrap& p) cls->OnDisconnected(d); if (const PWBuffer* subtype = cls->GetLocalSubtype(d)) { - cls->SetIndex(d, d); - cls->OnConnected(d, subtype->copy()); + cls->OnConnected(d, d, subtype->copy()); } } } @@ -66,9 +63,34 @@ void BackendLocal::DoState(PointerWrap& p) BackendNetPlay::BackendNetPlay(NetPlayClient* client, u32 delay) { m_Client = client; - m_SubframeId = -1; m_Delay = delay; +} + +void BackendNetPlay::PreInitDevices() +{ + for (int c = 0; c < Class::NumClasses; c++) + g_Classes[c]->PreInit(); +} + +void BackendNetPlay::StartGame() +{ m_Abort = false; + m_HaveClearReservationPacket = false; + for (auto& dis : m_DeviceInfo) + for (auto& di : dis) + di = DeviceInfo(); + Packet p; + // Flush out any old stuff (but not new stuff!) + while (m_PacketsPendingProcessing.Pop(p)) + { + MessageId mid = 0; + p.Do(mid); + if (mid == NP_MSG_START_GAME) + break; + } + m_SubframeId = -1; + // Block on subframe 0. + m_ReservedSubframeId = 0; NewLocalSubframe(); } @@ -79,7 +101,7 @@ void BackendNetPlay::ConnectLocalDevice(int classId, int localIndex, PWBuffer&& pac.W((MessageId) NP_MSG_CONNECT_DEVICE); pac.W((u8) classId); pac.W((u8) localIndex); - pac.W((u8) 0); // flags + pac.W((u16) 0); // flags pac.W(std::move(buf)); m_Client->SendPacket(std::move(pac)); } @@ -93,7 +115,7 @@ void BackendNetPlay::DisconnectLocalDevice(int classId, int localIndex) pac.W((MessageId) NP_MSG_DISCONNECT_DEVICE); pac.W((u8) classId); pac.W((u8) localIndex); - pac.W((u8) 0); // flags + pac.W((u16) 0); // flags m_Client->SendPacket(std::move(pac)); } @@ -106,8 +128,8 @@ void BackendNetPlay::EnqueueLocalReport(int classId, int localIndex, PWBuffer&& pac.W((MessageId) NP_MSG_REPORT); pac.W((u8) classId); pac.W((u8) ri); - auto& last = m_DeviceInfo[classId][localIndex].m_LastSentSubframeId; - u8 skippedFrames = m_SubframeId - last; + auto& last = m_DeviceInfo[classId][ri].m_LastSentSubframeId; + u16 skippedFrames = m_SubframeId - last; last = m_SubframeId; pac.W(skippedFrames); pac.vec->append(buf); @@ -132,25 +154,48 @@ Packet BackendNetPlay::DequeueReport(int classId, int index, bool* keepGoing) if (!queue.empty()) { Packet& p = queue.front(); - u8 skippedFrames; - p.Do(skippedFrames); - if (deviceInfo.m_SubframeId + skippedFrames > m_PastSubframeId) + MessageId packetType = 0; + u8 _classId, _index; + u16 flags; + p.Do(packetType); + p.Do(_classId); + p.Do(_index); + p.Do(flags); + if (packetType == NP_MSG_DISCONNECT_DEVICE) { - p.readOff--; + WARN_LOG(NETPLAY, "Disconnecting remote class %u device %u", classId, index); + DoDisconnect(classId, index); *keepGoing = false; return PWBuffer(); } - deviceInfo.m_SubframeId += skippedFrames; - //printf("--> dev=%llu past=%llu ql=%zd\n", deviceInfo.m_SubframeId, m_PastSubframeId, queue.size()); - *keepGoing = deviceInfo.m_SubframeId < m_PastSubframeId; - Packet q = std::move(p); - queue.pop_front(); - return q; + else + { + u16 skippedFrames = flags; + if (deviceInfo.m_SubframeId + skippedFrames > m_PastSubframeId) + { + p.readOff -= 5; + *keepGoing = false; + return PWBuffer(); + } + deviceInfo.m_SubframeId += skippedFrames; + //printf("--> dev=%llu past=%llu ql=%zd\n", deviceInfo.m_SubframeId, m_PastSubframeId, queue.size()); + *keepGoing = deviceInfo.m_SubframeId < m_PastSubframeId; + Packet q = std::move(p); + queue.pop_front(); + return q; + } } ProcessIncomingPackets(); } } +void BackendNetPlay::DoDisconnect(int classId, int index) +{ + m_DeviceInfo[classId][index] = DeviceInfo(); + if (g_Classes[classId]->IsConnected(index)) + g_Classes[classId]->OnDisconnected(index); +} + void BackendNetPlay::OnPacketError() { WARN_LOG(NETPLAY, "NetPlay packet error"); @@ -190,20 +235,24 @@ void BackendNetPlay::ProcessIncomingPackets() void BackendNetPlay::ProcessPacket(Packet&& p) { MessageId packetType = 0; - u8 classId, index, flags; + u8 classId, index; + u16 flags; p.Do(packetType); - if (packetType != NP_MSG_PAD_BUFFER) + switch (packetType) { + case NP_MSG_CONNECT_DEVICE: + case NP_MSG_DISCONNECT_DEVICE: + case NP_MSG_REPORT: p.Do(classId); p.Do(index); p.Do(flags); if (p.failure || classId >= Class::NumClasses || index >= g_Classes[classId]->GetMaxDeviceIndex()) - { - OnPacketError(); - return; - } + goto failure; + break; + default: + break; } switch (packetType) { @@ -215,30 +264,27 @@ void BackendNetPlay::ProcessPacket(Packet&& p) p.Do(localPlayer); p.Do(localIndex); p.Do(subtype); - if (localIndex >= g_Classes[classId]->GetMaxDeviceIndex()) - { - OnPacketError(); - return; - } + if (p.failure || + localIndex >= g_Classes[classId]->GetMaxDeviceIndex()) + goto failure; + WARN_LOG(NETPLAY, "Connecting remote class %u device %u with local %u/pid%u", classId, index, localIndex, localPlayer); - auto& di = m_DeviceInfo[classId][index] = DeviceInfo(); - di.m_LastSentSubframeId = di.m_SubframeId = m_SubframeId; - g_Classes[classId]->SetIndex(index, localPlayer == m_Client->m_pid ? localIndex : -1); - g_Classes[classId]->OnConnected(index, std::move(subtype)); + // The disconnect might be queued. + DoDisconnect(classId, index); + auto& di = m_DeviceInfo[classId][index]; + di.m_SubframeId = di.m_LastSentSubframeId = m_PastSubframeId; + int myLocalIndex = localPlayer == m_Client->m_pid ? localIndex : -1; + g_Classes[classId]->OnConnected(index, myLocalIndex, std::move(subtype)); break; } case NP_MSG_DISCONNECT_DEVICE: { - WARN_LOG(NETPLAY, "Disconnecting remote class %u device %u", classId, index); - m_DeviceInfo[classId][index] = DeviceInfo(); g_Classes[classId]->SetIndex(index, -1); - if (g_Classes[classId]->IsConnected(index)) - g_Classes[classId]->OnDisconnected(index); - break; + // fall through } case NP_MSG_REPORT: { - p.readOff--; // go back to flags + p.readOff -= 5; // go back to type m_DeviceInfo[classId][index].m_IncomingQueue.push_back(std::move(p)); break; } @@ -246,21 +292,77 @@ void BackendNetPlay::ProcessPacket(Packet&& p) { u32 delay; p.Do(delay); + if (p.failure) + goto failure; m_Delay = delay; - break; + break; + } + case NP_MSG_SET_RESERVATION: + { + s64 subframe; + p.Do(subframe); + if (p.failure) + goto failure; + WARN_LOG(NETPLAY, "Client: reservation request for subframe %lld; current %lld (%s)", subframe, m_PastSubframeId, subframe > m_PastSubframeId ? "ok" : "too late"); + if (subframe > m_PastSubframeId) + { + m_ReservedSubframeId = subframe; + m_HaveClearReservationPacket = false; + } + Packet pac; + pac.W((MessageId) NP_MSG_RESERVATION_RESULT); + pac.W(subframe); + pac.W(m_PastSubframeId); + m_Client->SendPacket(std::move(pac)); } + break; + case NP_MSG_CLEAR_RESERVATION: + { + m_HaveClearReservationPacket = true; + m_ClearReservationPacket = std::move(p); + } + break; default: // can't happen break; } + return; +failure: + OnPacketError(); } void BackendNetPlay::NewLocalSubframe() { m_SubframeId++; m_PastSubframeId = m_SubframeId - m_Delay; + // If we have nothing connected, we need to process the queue here. ProcessIncomingPackets(); -} + if (m_PastSubframeId == m_ReservedSubframeId) + { + if (!m_HaveClearReservationPacket) + WARN_LOG(NETPLAY, "Client: blocking on reserved subframe %lld (bad estimate)", m_ReservedSubframeId); + while (!m_HaveClearReservationPacket) + { + // Ouch, the server didn't wait long enough and we have to block. + if (m_Abort) + return; + ProcessIncomingPackets(); + } + std::vector messages; + m_ClearReservationPacket.Do(messages); + m_ClearReservationPacket = Packet(); + WARN_LOG(NETPLAY, "Client: reservation complete"); + for (auto& message : messages) + { + ProcessPacket(std::move(message)); + } + m_ReservedSubframeId--; // dummy value + Packet pac; + pac.W((MessageId) NP_MSG_RESERVATION_DONE); + m_Client->SendPacket(std::move(pac)); + } } + +} // namespace diff --git a/Source/Core/Core/Src/IOSyncBackends.h b/Source/Core/Core/Src/IOSyncBackends.h index 54c90a0282b7..ef9a35305017 100644 --- a/Source/Core/Core/Src/IOSyncBackends.h +++ b/Source/Core/Core/Src/IOSyncBackends.h @@ -39,8 +39,10 @@ class BackendNetPlay : public Backend virtual void OnPacketError() override; virtual u32 GetTime() override; virtual void DoState(PointerWrap& p) override; + virtual void StartGame() override; // from netplay + void PreInitDevices() ON(NET); void OnPacketReceived(Packet&& packet) ON(NET); void Abort() ON(NET); // from (arbitrarily-ish) SI @@ -60,14 +62,18 @@ class BackendNetPlay : public Backend void ProcessIncomingPackets(); void ProcessPacket(Packet&& p); - void UpdateDelay(u32 delay); + void DoDisconnect(int classId, int index); NetPlayClient* m_Client; - // this is split up to avoid unnecessary copying in The Future Common::FifoQueue m_PacketsPendingProcessing; s64 m_SubframeId; - // We accept packets sent before this frame. + // We accept packets sent before this frame. i.e. this is the subframe + // represented in emulation. s64 m_PastSubframeId; + // We will wait for this frame. + s64 m_ReservedSubframeId; + Packet m_ClearReservationPacket; + bool m_HaveClearReservationPacket; u32 m_Delay; // indexed by remote device DeviceInfo m_DeviceInfo[Class::NumClasses][Class::MaxDeviceIndex]; diff --git a/Source/Core/Core/Src/Movie.cpp b/Source/Core/Core/Src/Movie.cpp index cc5b099ee0b1..967458b11723 100644 --- a/Source/Core/Core/Src/Movie.cpp +++ b/Source/Core/Core/Src/Movie.cpp @@ -383,7 +383,7 @@ void ChangePads(bool instantly) if (instantly) // Changes from savestates need to be instantaneous SerialInterface::AddDevice(IsUsingPad(i) ? (IsUsingBongo(i) ? SIDEVICE_GC_TARUKONGA : SIDEVICE_GC_CONTROLLER) : SIDEVICE_NONE, i); else - SerialInterface::ChangeDevice(IsUsingPad(i) ? (IsUsingBongo(i) ? SIDEVICE_GC_TARUKONGA : SIDEVICE_GC_CONTROLLER) : SIDEVICE_NONE, i); + SerialInterface::ChangeLocalDevice(IsUsingPad(i) ? (IsUsingBongo(i) ? SIDEVICE_GC_TARUKONGA : SIDEVICE_GC_CONTROLLER) : SIDEVICE_NONE, i); } void ChangeWiiPads(bool instantly) diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 7341f9f19a36..dde20394c234 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -44,7 +44,7 @@ NetPlayClient::NetPlayClient(const std::string& hostSpec, const std::string& nam m_net_host = NULL; m_state_callback = stateCallback; m_local_name = name; - m_backend = NULL; + IOSync::g_Backend.reset(m_backend = new IOSync::BackendNetPlay(this, m_delay)); m_received_stop_request = false; size_t pos = hostSpec.find(':'); @@ -141,6 +141,7 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) if (m_state_callback) m_state_callback(this); m_have_dialog_event.Wait(); + m_backend->PreInitDevices(); return; } else if(m_state != Connected) @@ -238,16 +239,6 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) return OnDisconnect(InvalidPacket); /* fall through */ } - case NP_MSG_DISCONNECT_DEVICE: - case NP_MSG_CONNECT_DEVICE: - case NP_MSG_REPORT: - { - if (!m_backend) - break; - packet.readOff = oldOff; - m_backend->OnPacketReceived(std::move(packet)); - break; - } case NP_MSG_CHANGE_GAME : { @@ -281,8 +272,20 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) m_received_stop_request = false; m_dialog->OnMsgStartGame(); + // fall through + } + case NP_MSG_DISCONNECT_DEVICE: + case NP_MSG_CONNECT_DEVICE: + case NP_MSG_REPORT: + case NP_MSG_SET_RESERVATION: + case NP_MSG_CLEAR_RESERVATION: + { + if (!m_backend) + break; + packet.readOff = oldOff; + m_backend->OnPacketReceived(std::move(packet)); + break; } - break; case NP_MSG_STOP_GAME : { @@ -339,6 +342,8 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) void NetPlayClient::OnDisconnect(int reason) { + if (m_state == Failure) + return; std::lock_guard lk(m_crit); if (m_state == Connected) { @@ -513,7 +518,6 @@ bool NetPlayClient::StartGame(const std::string &path) m_dialog->AppendChat(" -- STARTING GAME -- "); - IOSync::g_Backend.reset(m_backend = new IOSync::BackendNetPlay(this, m_delay)); // boot game m_dialog->BootGame(path); @@ -539,6 +543,7 @@ void NetPlayClient::GameStopped() std::lock_guard lk(m_crit); m_net_host->RunOnThreadSync([=]() { + ASSUME_ON(NET); IOSync::ResetBackend(); m_backend = NULL; }); diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index be051823c1ff..6bfa322ec5c8 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -120,7 +120,7 @@ class NetPlayClient : public NetHostClient, public TraversalClientClient Player* m_local_player GUARDED_BY(m_crit); std::string m_local_name ACCESS_ON(NET); - IOSync::BackendNetPlay* m_backend; + IOSync::BackendNetPlay* m_backend ACCESS_ON(NET); u32 m_current_game; bool m_received_stop_request; diff --git a/Source/Core/Core/Src/NetPlayProto.h b/Source/Core/Core/Src/NetPlayProto.h index c9e2a1d21ae9..f62b59178d90 100644 --- a/Source/Core/Core/Src/NetPlayProto.h +++ b/Source/Core/Core/Src/NetPlayProto.h @@ -45,6 +45,16 @@ enum NP_MSG_DISCONNECT_DEVICE = 0x62, NP_MSG_REPORT = 0x63, + // Todo: explain why this is a good idea. + // [s->c] Block on a specific subframe in the future. + NP_MSG_SET_RESERVATION = 0x64, + // [s->c] Clear the reservation and optionally run some commands. + NP_MSG_CLEAR_RESERVATION = 0x65, + // [c->s] Reservation was either OK or failed because that frame already started. + NP_MSG_RESERVATION_RESULT = 0x66, + // [c->s] Reservation was executed. + NP_MSG_RESERVATION_DONE = 0x67, + NP_MSG_START_GAME = 0xA0, NP_MSG_CHANGE_GAME = 0xA1, NP_MSG_STOP_GAME = 0xA2, diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 5db76eed2e4b..fa75993e3216 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -18,6 +18,8 @@ #include #endif +const std::pair NetPlayServer::DeviceInfo::null_mapping(255, 255); + NetPlayServer::~NetPlayServer() { #ifdef __APPLE__ @@ -38,11 +40,9 @@ NetPlayServer::NetPlayServer() m_is_running = false; m_num_players = 0; m_dialog = NULL; -#if 0 - memset(m_pad_map, -1, sizeof(m_pad_map)); - memset(m_wiimote_map, -1, sizeof(m_wiimote_map)); -#endif + m_highest_known_subframe = 0; m_target_buffer_size = 20; + m_reservation_state = Inactive; #ifdef __APPLE__ m_dynamic_store = SCDynamicStoreCreate(NULL, CFSTR("NetPlayServer"), NULL, NULL); m_prefs = SCPreferencesCreate(NULL, CFSTR("NetPlayServer"), NULL); @@ -84,7 +84,7 @@ bool NetPlayServer::IsSpectator(PlayerId pid) { for (int i = 0; i < IOSync::Class::MaxDeviceIndex; i++) { - if (m_device_map[c][i].first == pid) + if (m_device_info[c][i].actual_mapping.first == pid) return false; } } @@ -94,7 +94,7 @@ bool NetPlayServer::IsSpectator(PlayerId pid) void NetPlayServer::OnENetEvent(ENetEvent* event) { // update pings every so many seconds - if (m_ping_timer.GetTimeElapsed() > (10 * 1000)) + if (m_ping_timer.GetTimeElapsed() > (2 * 1000)) UpdatePings(); PlayerId pid = event->peer - m_enet_host->peers; @@ -115,11 +115,6 @@ void NetPlayServer::OnTraversalStateChanged() m_dialog->Update(); } -void NetPlayServer::SetDesiredDeviceMapping(int classId, int index, PlayerId pid, int localIndex) ON(NET) -{ - WARN_LOG(NETPLAY, "SetDesiredDeviceMapping stub: class %d index %d -> player %d index %d", classId, index, pid, localIndex); -} - #if defined(__APPLE__) std::string CFStrToStr(CFStringRef cfstr) { @@ -226,6 +221,7 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) hello.Do(player.revision); hello.Do(player.name); player.ping = -1u; + player.reservation_ok = false; // dolphin netplay version if (hello.failure || npver != NETPLAY_VERSION) return CON_ERR_VERSION_MISMATCH; @@ -304,19 +300,29 @@ void NetPlayServer::OnDisconnect(PlayerId pid) return; player.connected = false; + enet_peer_disconnect_later(&m_enet_host->peers[pid], 0); + + // The last reservation holdout might have disconnected. + CheckReservationResults(); for (int c = 0; c < IOSync::Class::NumClasses; c++) { for (int i = 0; i < IOSync::Class::MaxDeviceIndex; i++) { - if (m_device_map[c][i].first == pid) + auto& di = m_device_info[c][i]; + if (di.actual_mapping.first == pid) { - Packet opacket; - opacket.W((MessageId)NP_MSG_DISCONNECT_DEVICE); - opacket.W((u8)c); - opacket.W((u8)i); - SendToClientsOnThread(std::move(opacket)); + ForceDisconnectDevice(c, i); } + else if (di.new_mapping.first == pid) + { + di.todo.push_back([=]() { + ASSUME_ON(NET); + ForceDisconnectDevice(c, i); + }); + } + if (di.desired_mapping.first == pid) + di.desired_mapping = DeviceInfo::null_mapping; } } @@ -328,6 +334,7 @@ void NetPlayServer::OnDisconnect(PlayerId pid) SendToClientsOnThread(std::move(opacket)); } + void NetPlayServer::AdjustPadBufferSize(unsigned int size) { m_target_buffer_size = size; @@ -364,6 +371,8 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) { //player.is_localhost = peer->address.host == 0x0100007f; player.connected = true; + // XXX allow connection during game + player.sitting_out_this_game = false; m_num_players++; } return; @@ -414,9 +423,9 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) case NP_MSG_CONNECT_DEVICE: case NP_MSG_DISCONNECT_DEVICE: { - u8 classId, localIndex, flags; + u8 classId, localIndex; + u16 flags; packet.Do(classId); - u8* indexP = (u8*) packet.vec->data() + packet.readOff; packet.Do(localIndex); packet.Do(flags); int limit; @@ -426,41 +435,30 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) { return OnDisconnect(pid); } - // todo: bring customization back - std::pair* map = m_device_map[classId]; if (mid == NP_MSG_CONNECT_DEVICE) { - PlayerId dummy1; - u8 dummy2; - PlayerId* localPlayerP = (PlayerId*) packet.vec->data() + packet.readOff; - packet.Do(dummy1); - u8* localIndexP = (u8*) packet.vec->data() + packet.readOff; - packet.Do(dummy2); + PWBuffer subtype; + packet.Do(subtype); if (packet.failure) return OnDisconnect(pid); WARN_LOG(NETPLAY, "Server: received CONNECT_DEVICE (%u/%u) from client %u", classId, localIndex, pid); - // stub - player.devices_present[classId | (localIndex << 8)] = PWBuffer(); + int idx = classId | (localIndex << 8); + if (player.devices_present.find(idx) != player.devices_present.end()) + return OnDisconnect(pid); + player.devices_present[idx] = std::move(subtype); int i; for (i = 0; i < limit; i++) { - if (map[i].second == -1) + auto& di = m_device_info[classId][i]; + if (di.desired_mapping.second == -1) { - map[i].first = pid; - map[i].second = localIndex; - *indexP = i; - *localPlayerP = pid; - *localIndexP = localIndex; - WARN_LOG(NETPLAY, " --> assigning %d", i); - SendToClientsOnThread(std::move(packet)); + SetDesiredDeviceMapping(classId, i, pid, localIndex); break; } } if (i == limit) WARN_LOG(NETPLAY, " --> no assignment"); - // todo: keep track of connected local devices so they can be - // assigned back later } else // DISCONNECT { @@ -468,13 +466,20 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) player.devices_present.erase(classId | (localIndex << 8)); for (int i = 0; i < IOSync::Class::MaxDeviceIndex; i++) { - if (map[i].first == pid && map[i].second == localIndex) + auto& di = m_device_info[classId][i]; + if (di.actual_mapping == std::make_pair(pid, (s8) localIndex)) { - map[i].first = map[i].second = -1; - *indexP = i; - SendToClientsOnThread(std::move(packet)); - break; + ForceDisconnectDevice(classId, i); } + else if (di.new_mapping == std::make_pair(pid, (s8) localIndex)) + { + di.todo.push_back([=]() { + ASSUME_ON(NET); + ForceDisconnectDevice(classId, i); + }); + } + if (di.desired_mapping == std::make_pair(pid, (s8) localIndex)) + di.desired_mapping = DeviceInfo::null_mapping; } } if (m_dialog) @@ -485,10 +490,11 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) case NP_MSG_REPORT: { - u8 classId, index, flags; + u8 classId, index; + u16 skippedFrames; packet.Do(classId); packet.Do(index); - packet.Do(flags); + packet.Do(skippedFrames); if (packet.failure || classId >= IOSync::Class::NumClasses || index >= IOSync::g_Classes[classId]->GetMaxDeviceIndex()) @@ -496,10 +502,23 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) return OnDisconnect(pid); } - if (m_device_map[classId][index].first == pid) + auto& di = m_device_info[classId][index]; + if (di.actual_mapping.first == pid) { + di.subframe += skippedFrames; + m_highest_known_subframe = std::max(m_highest_known_subframe, di.subframe); SendToClientsOnThread(std::move(packet), pid); } + else if (di.new_mapping.first == pid) + { + di.subframe += skippedFrames; + m_highest_known_subframe = std::max(m_highest_known_subframe, di.subframe); + CopyAsMove cpacket(std::move(packet)); + di.todo.push_back([=]() mutable { + ASSUME_ON(NET); + SendToClientsOnThread(std::move(*cpacket), pid); + }); + } else { WARN_LOG(NETPLAY, "Received spurious report for index %u from pid %u!", index, pid); @@ -543,19 +562,54 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) break; if (IsSpectator(pid)) { - // If they're a spectator, the game can keep going even after - // they stopped, but we need to note that they no longer can be - // assigned to controllers. - //UpdateDevicesPresent(); - break; + player.sitting_out_this_game = true; + if (m_dialog) + m_dialog->UpdateDevices(); } - // tell clients to stop game - Packet opacket; - opacket.W((MessageId)NP_MSG_STOP_GAME); + else + { + // tell clients to stop game + Packet opacket; + opacket.W((MessageId)NP_MSG_STOP_GAME); - SendToClientsOnThread(std::move(opacket)); + SendToClientsOnThread(std::move(opacket)); - m_is_running = false; + m_is_running = false; + m_reservation_state = Inactive; + } + } + break; + + case NP_MSG_RESERVATION_RESULT: + { + if (m_reservation_state != WaitingForResponses) + break; + s64 requested_subframe, actual_subframe; + packet.Do(requested_subframe); + packet.Do(actual_subframe); + m_highest_known_subframe = std::max(m_highest_known_subframe, actual_subframe); + if (requested_subframe != m_reserved_subframe) + break; + if (requested_subframe > actual_subframe) + { + // OK + player.reservation_ok = true; + CheckReservationResults(); + } + else + { + // Fail, try again + EndReservation(); + } + } + break; + + case NP_MSG_RESERVATION_DONE: + { + if (m_reservation_state != WaitingForChangeover) + break; + player.reservation_ok = false; + CheckReservationResults(); } break; @@ -586,31 +640,46 @@ void NetPlayServer::SetNetSettings(const NetSettings &settings) m_settings = settings; } -bool NetPlayServer::StartGame(const std::string &path) +void NetPlayServer::StartGame(const std::string &path) { - m_current_game = Common::Timer::GetTimeMs(); - - // no change, just update with clients - AdjustPadBufferSize(m_target_buffer_size); - - // tell clients to start game - Packet opacket; - opacket.W((MessageId)NP_MSG_START_GAME); - opacket.W(m_current_game); - opacket.W(m_settings.m_CPUthread); - opacket.W(m_settings.m_DSPEnableJIT); - opacket.W(m_settings.m_DSPHLE); - opacket.W(m_settings.m_WriteToMemcard); - opacket.W((int) m_settings.m_EXIDevice[0]); - opacket.W((int) m_settings.m_EXIDevice[1]); + m_net_host->RunOnThread([=]() { + ASSUME_ON(NET); + if (m_is_running) + return; + m_current_game = Common::Timer::GetTimeMs(); - SendToClients(std::move(opacket)); + // no change, just update with clients + AdjustPadBufferSize(m_target_buffer_size); - m_is_running = true; + // tell clients to start game + Packet opacket; + opacket.W((MessageId)NP_MSG_START_GAME); + opacket.W(m_current_game); + opacket.W(m_settings.m_CPUthread); + opacket.W(m_settings.m_DSPEnableJIT); + opacket.W(m_settings.m_DSPHLE); + opacket.W(m_settings.m_WriteToMemcard); + opacket.W((int) m_settings.m_EXIDevice[0]); + opacket.W((int) m_settings.m_EXIDevice[1]); + + SendToClientsOnThread(std::move(opacket)); + + bool change = false; + for (auto& player : m_players) + { + if (!player.connected) + break; + change = change || player.sitting_out_this_game; + player.sitting_out_this_game = false; + } + if (change && m_dialog) + m_dialog->UpdateDevices(); - memset(m_device_map, 0xff, sizeof(m_device_map)); + m_reserved_subframe = 0; + ExecuteReservation(); - return true; + m_is_running = true; + }); } void NetPlayServer::SendToClients(Packet&& packet, const PlayerId skip_pid) @@ -632,3 +701,222 @@ void NetPlayServer::SetDialog(NetPlayUI* dialog) { m_dialog = dialog; } + +void NetPlayServer::SetDesiredDeviceMapping(int classId, int index, PlayerId pid, int localIndex) +{ + auto& dis = m_device_info[classId]; + auto& di = dis[index]; + auto new_mapping = std::make_pair(pid, (s8) localIndex); + if (di.desired_mapping == new_mapping) + return; + if (new_mapping.first != 255) + { + // Anyone else using this mapping? + for (int i = 0; i < IOSync::Class::MaxDeviceIndex; i++) + { + if (i != index && dis[i].desired_mapping == new_mapping) + { + SetDesiredDeviceMapping(classId, i, 255, 255); + // ??? This should not be necessary (definitely being run on + // the right thread, not reentrantly, all that), but it seems + // to be. + Common::SleepCurrentThread(50); + if (m_dialog) + m_dialog->UpdateDevices(); + break; + } + } + } + di.desired_mapping = new_mapping; + AddReservation(); +} + +void NetPlayServer::AddReservation() +{ + if (m_reservation_state != Inactive) + return; + // We always pretend that a reservation exists on frame 0. + if (!m_is_running) + return; + m_reservation_state = WaitingForResponses; + + // In case our guessed ping is too low, we should get the ping result + // before the reservation failure, so it will be okay next time. + UpdatePings(); + // TODO: spectators don't need to be ok, *unless* everyone is a spectator + u32 highest_ping = 50; + for (size_t pid = 0; pid < m_players.size(); pid++) + { + Client& player = m_players[pid]; + if (!player.connected) + continue; + player.reservation_ok = false; + if (player.ping != -1u) + highest_ping = std::max(highest_ping, player.ping); + } + // now -> SET_RESERVATION -> RESERVATION_RESULT -> NP_MSG_CLEAR_RESERVATION + // = 3x latency (3/2x ping), hopefully without any clients blocking + m_reserved_subframe = std::max(m_highest_known_subframe + (highest_ping * 2) * 120 / 1000, 0ll); + WARN_LOG(NETPLAY, "Reserving frame %llu (hp=%u)", m_reserved_subframe, highest_ping); + Packet packet; + packet.W((MessageId)NP_MSG_SET_RESERVATION); + packet.W(m_reserved_subframe); + SendToClientsOnThread(std::move(packet)); +} + +void NetPlayServer::CheckReservationResults() +{ + if (m_reservation_state == WaitingForResponses) + { + size_t pid; + for (pid = 0; pid < m_players.size(); pid++) + { + Client& player = m_players[pid]; + if (player.connected && !player.reservation_ok) + return; + } + + // All OK + ExecuteReservation(); + } + else if (m_reservation_state == WaitingForChangeover) + { + size_t pid; + for (pid = 0; pid < m_players.size(); pid++) + { + Client& player = m_players[pid]; + if (player.connected && player.reservation_ok) + return; + } + + // All done + for (int c = 0; c < IOSync::Class::NumClasses; c++) + { + for (int i = 0; i < IOSync::Class::MaxDeviceIndex; i++) + { + auto& di = m_device_info[c][i]; + if (di.new_mapping != di.actual_mapping) + { + for (auto& todo : di.todo) + todo(); + di.todo.clear(); + di.actual_mapping = di.new_mapping; + WARN_LOG(NETPLAY, "Changing over class %d index %d -> pid %u local %d subframe %lld", c, i, di.new_mapping.first, di.new_mapping.second, di.subframe); + } + } + } + + EndReservation(); + } +} + +void NetPlayServer::ExecuteReservation() +{ + // Everyone is waiting for us! Hurry up. + std::vector messages; + bool had_disconnects = false; + for (int c = 0; c < IOSync::Class::NumClasses; c++) + { + for (int i = 0; i < IOSync::Class::MaxDeviceIndex; i++) + { + auto& di = m_device_info[c][i]; + if (!m_is_running) + di.actual_mapping = DeviceInfo::null_mapping; + if (di.desired_mapping != di.actual_mapping && di.actual_mapping.first != 255) + { + // Disconnect the previous assignment + Packet packet; + packet.W((MessageId)NP_MSG_DISCONNECT_DEVICE); + packet.W((u8) c); + packet.W((u8) i); + packet.W((u16) 0); + messages.push_back(std::move(*packet.vec)); + di.new_mapping = DeviceInfo::null_mapping; + di.todo.clear(); + had_disconnects = true; + } + } + } + // Strictly we only need to avoid the *same* device being disconnected and + // connected. Exercise: Why? + if (!had_disconnects) + { + for (int c = 0; c < IOSync::Class::NumClasses; c++) + { + for (int i = 0; i < IOSync::Class::MaxDeviceIndex; i++) + { + auto& di = m_device_info[c][i]; + if (di.desired_mapping != di.actual_mapping && di.desired_mapping.first != 255) + { + auto pid = di.desired_mapping.first; + auto local_index = di.desired_mapping.second; + auto& player = m_players[pid]; + if (!player.connected) + goto fail; + auto it = player.devices_present.find(c | (local_index << 8)); + if (it == player.devices_present.end()) + goto fail; + const PWBuffer& subtype = it->second; + // And get the new one! + Packet packet; + packet.W((MessageId)NP_MSG_CONNECT_DEVICE); + packet.W((u8) c); + packet.W((u8) i); + packet.W((u16) 0); + packet.W(pid); + packet.W((u8) local_index); + packet.W(subtype); + messages.push_back(std::move(*packet.vec)); + di.new_mapping = di.desired_mapping; + di.subframe = m_reserved_subframe; + di.todo.clear(); + } + } + } + } + + { + Packet packet; + packet.W((MessageId)NP_MSG_CLEAR_RESERVATION); + packet.W(messages); + SendToClientsOnThread(std::move(packet)); + } + m_reservation_state = WaitingForChangeover; + return; + +fail: + PanicAlert("ExecuteReservation is messed up"); +} + +void NetPlayServer::EndReservation() +{ + m_reservation_state = Inactive; + + // We might still not be up to date. + for (int c = 0; c < IOSync::Class::NumClasses; c++) + { + for (int i = 0; i < IOSync::Class::MaxDeviceIndex; i++) + { + auto& di = m_device_info[c][i]; + if (di.desired_mapping != di.actual_mapping) + { + AddReservation(); + return; + } + } + } +} + +// This is only safe if the player was expecting the disconnect: they sent a +// disconnect message or disconnected entirely. +void NetPlayServer::ForceDisconnectDevice(int classId, int index) +{ + Packet packet; + packet.W((MessageId)NP_MSG_DISCONNECT_DEVICE); + packet.W((u8)classId); + packet.W((u8)index); + packet.W((u8)0); + SendToClientsOnThread(std::move(packet)); + auto& di = m_device_info[classId][index]; + di.desired_mapping = di.actual_mapping = di.new_mapping = DeviceInfo::null_mapping; +} diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index d78ff0e2eeb7..16f91061a58a 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -31,7 +31,7 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient void SetNetSettings(const NetSettings &settings) /* ON(GUI) */; - bool StartGame(const std::string &path) /* ON(GUI) */; + void StartGame(const std::string &path) /* ON(GUI) */; void AdjustPadBufferSize(unsigned int size) /* multiple threads */; @@ -44,6 +44,11 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient virtual void OnConnectFailed(u8 reason) override ON(NET) {} void SetDesiredDeviceMapping(int classId, int index, PlayerId pid, int localIndex) ON(NET); + void AddReservation() ON(NET); + void CheckReservationResults() ON(NET); + void ExecuteReservation() ON(NET); + void EndReservation() ON(NET); + void ForceDisconnectDevice(int classId, int index) ON(NET); std::unordered_set GetInterfaceSet(); std::string GetInterfaceHost(std::string interface); @@ -58,14 +63,38 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient u32 ping; u32 current_game; bool connected; + bool reservation_ok; + bool sitting_out_this_game; // hit "stop" std::map devices_present; //bool is_localhost; }; std::vector m_players; - std::pair m_device_map[IOSync::Class::NumClasses][IOSync::Class::MaxDeviceIndex]; - std::pair m_desired_device_map[IOSync::Class::NumClasses][IOSync::Class::MaxDeviceIndex]; + struct DeviceInfo + { + DeviceInfo() + { + desired_mapping = actual_mapping = new_mapping = null_mapping; + } + const static std::pair null_mapping; + // The mapping configured in the dialog. + std::pair desired_mapping; + // The current mapping. + std::pair actual_mapping; + // In WaitingForChangeover mode, the mapping that we'll change over to + // when this device hits m_reserved_subframe or we switch to Inactive + // mode. The current approach (disconnect+connect requires two cycles, + // etc.) is suboptimal in general, but it simplifies the logic a bit + // (which is already rather complicated) and it's not like connecting + // and disconnecting devices is super latency sensitive. + std::pair new_mapping; + s64 subframe; + + std::vector> todo; + }; + + DeviceInfo m_device_info[IOSync::Class::NumClasses][IOSync::Class::MaxDeviceIndex]; private: @@ -100,6 +129,15 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient ENetHost* m_enet_host; NetPlayUI* m_dialog; + s64 m_highest_known_subframe; + enum + { + Inactive, + WaitingForResponses, + WaitingForChangeover + } m_reservation_state; + s64 m_reserved_subframe; + #if defined(__APPLE__) const void* m_dynamic_store; const void* m_prefs; diff --git a/Source/Core/DolphinWX/Src/ConfigMain.cpp b/Source/Core/DolphinWX/Src/ConfigMain.cpp index 4e883b1ee2c2..7a37c47a0cbb 100644 --- a/Source/Core/DolphinWX/Src/ConfigMain.cpp +++ b/Source/Core/DolphinWX/Src/ConfigMain.cpp @@ -1149,11 +1149,8 @@ void CConfigMain::ChooseSIDevice(wxString deviceName, int deviceNum) SConfig::GetInstance().m_SIDevice[deviceNum] = tempType; - if (Core::GetState() != Core::CORE_UNINITIALIZED) - { - // Change plugged device! :D - SerialInterface::ChangeDevice(tempType, deviceNum); - } + // Change plugged device! :D + SerialInterface::ChangeLocalDevice(tempType, deviceNum); } void CConfigMain::ChooseEXIDevice(wxString deviceName, int deviceNum) diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index fb4134b41371..c714d7bc6539 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -497,6 +497,7 @@ void NetPlayDiag::OnThread(wxCommandEvent& event) else { PanicAlertT("The host chose a game that was not found locally."); + netplay_client->GameStopped(); } if (m_start_btn) m_start_btn->Disable(); @@ -516,7 +517,7 @@ void NetPlayDiag::OnThread(wxCommandEvent& event) case NP_GUI_EVT_UPDATE_DEVICES: { if (m_device_map_diag && netplay_server) - g_MainNetHost->RunOnThreadSync([&]() { + g_MainNetHost->RunOnThisThreadSync([&]() { m_device_map_diag->UpdateDeviceMap(); }); } @@ -733,7 +734,7 @@ void DeviceMapDiag::UpdateDeviceMap() m_pos_to_pid_local_idx[classId].push_back(std::make_pair(255, 255)); for (auto& player : m_server->m_players) { - if (!player.connected) + if (!player.connected || player.sitting_out_this_game) continue; for (const auto& p : player.devices_present) { @@ -752,7 +753,7 @@ void DeviceMapDiag::UpdateDeviceMap() wxChoice* choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, options); choice->Bind(wxEVT_COMMAND_CHOICE_SELECTED, &DeviceMapDiag::OnAdjust, this); m_choice_to_cls_idx[choice] = std::make_pair(classId, idx); - auto cur = m_server->m_device_map[classId][idx]; + auto cur = m_server->m_device_info[classId][idx].desired_mapping; if (cur.first == 255) choice->Select(0); else @@ -782,7 +783,7 @@ void DeviceMapDiag::OnAdjust(wxCommandEvent& event) auto q = m_pos_to_pid_local_idx[classId][pos]; PlayerId pid = q.first; int local_index = q.second; - g_MainNetHost->RunOnThreadSync([=]() { + g_MainNetHost->RunOnThread([=]() { ASSUME_ON(NET); m_server->SetDesiredDeviceMapping(classId, index, pid, local_index); }); From 75f60e19afa5ce781d83c08d28289438162ea2c8 Mon Sep 17 00:00:00 2001 From: comex Date: Mon, 25 Nov 2013 17:02:33 -0500 Subject: [PATCH 141/202] Fix Linux build. --- Source/Core/Core/Src/IOSyncBackends.cpp | 4 ++-- Source/Core/Core/Src/NetPlayServer.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index e543b5dc1820..5debbc6d625f 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -303,7 +303,7 @@ void BackendNetPlay::ProcessPacket(Packet&& p) p.Do(subframe); if (p.failure) goto failure; - WARN_LOG(NETPLAY, "Client: reservation request for subframe %lld; current %lld (%s)", subframe, m_PastSubframeId, subframe > m_PastSubframeId ? "ok" : "too late"); + WARN_LOG(NETPLAY, "Client: reservation request for subframe %lld; current %lld (%s)", (long long) subframe, (long long) m_PastSubframeId, subframe > m_PastSubframeId ? "ok" : "too late"); if (subframe > m_PastSubframeId) { m_ReservedSubframeId = subframe; @@ -342,7 +342,7 @@ void BackendNetPlay::NewLocalSubframe() if (m_PastSubframeId == m_ReservedSubframeId) { if (!m_HaveClearReservationPacket) - WARN_LOG(NETPLAY, "Client: blocking on reserved subframe %lld (bad estimate)", m_ReservedSubframeId); + WARN_LOG(NETPLAY, "Client: blocking on reserved subframe %lld (bad estimate)", (long long) m_ReservedSubframeId); while (!m_HaveClearReservationPacket) { // Ouch, the server didn't wait long enough and we have to block. diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index fa75993e3216..0d422e9b0c42 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -756,8 +756,8 @@ void NetPlayServer::AddReservation() } // now -> SET_RESERVATION -> RESERVATION_RESULT -> NP_MSG_CLEAR_RESERVATION // = 3x latency (3/2x ping), hopefully without any clients blocking - m_reserved_subframe = std::max(m_highest_known_subframe + (highest_ping * 2) * 120 / 1000, 0ll); - WARN_LOG(NETPLAY, "Reserving frame %llu (hp=%u)", m_reserved_subframe, highest_ping); + m_reserved_subframe = std::max(m_highest_known_subframe + (highest_ping * 2) * 120 / 1000, (s64) 0); + WARN_LOG(NETPLAY, "Reserving frame %lld (hp=%u)", (long long) m_reserved_subframe, highest_ping); Packet packet; packet.W((MessageId)NP_MSG_SET_RESERVATION); packet.W(m_reserved_subframe); @@ -801,7 +801,7 @@ void NetPlayServer::CheckReservationResults() todo(); di.todo.clear(); di.actual_mapping = di.new_mapping; - WARN_LOG(NETPLAY, "Changing over class %d index %d -> pid %u local %d subframe %lld", c, i, di.new_mapping.first, di.new_mapping.second, di.subframe); + WARN_LOG(NETPLAY, "Changing over class %d index %d -> pid %u local %d subframe %lld", c, i, di.new_mapping.first, di.new_mapping.second, (long long) di.subframe); } } } From 7f85c3215bb0671f2607204196bcfed77ddc544b Mon Sep 17 00:00:00 2001 From: Lioncash Date: Mon, 25 Nov 2013 17:18:31 -0500 Subject: [PATCH 142/202] [Android] Add Javadoc to InputOverlayDrawableJoystick. --- .../emulation/overlay/InputOverlayDrawableJoystick.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java index 9b5dd6b86033..582a8c3f8033 100644 --- a/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java +++ b/Source/Android/src/org/dolphinemu/dolphinemu/emulation/overlay/InputOverlayDrawableJoystick.java @@ -28,8 +28,11 @@ public final class InputOverlayDrawableJoystick extends BitmapDrawable * Constructor * * @param res {@link Resources} instance. - * @param bitmapOuter {@link Bitmap} to use with this Drawable. - * @param joystick Identifier for which joystick this is. + * @param bitmapOuter {@link Bitmap} which represents the outer non-movable part of the joystick. + * @param bitmapInner {@link Bitmap} which represents the inner movable part of the joystick. + * @param rectOuter {@link Rect} which represents the outer joystick bounds. + * @param rectInner {@link Rect} which represents the inner joystick bounds. + * @param joystick Identifier for which joystick this is. */ public InputOverlayDrawableJoystick(Resources res, Bitmap bitmapOuter, Bitmap bitmapInner, @@ -139,6 +142,5 @@ private void SetInnerBounds() int height = this.ringInner.getBounds().height() / 2; this.ringInner.setBounds(X - width, Y - height, X + width, Y + height); - } } From 7469d7b029020101d7a6640ad997c7aaa773c87a Mon Sep 17 00:00:00 2001 From: comex Date: Mon, 25 Nov 2013 17:44:11 -0500 Subject: [PATCH 143/202] More MSVC stupidity. --- Source/Core/Common/Src/ChunkFile.h | 2 +- Source/Core/Common/Src/Common.h | 6 +++--- Source/Core/Common/Src/NetHost.cpp | 2 +- Source/Core/Core/Src/IOSyncBackends.cpp | 4 ++-- Source/Core/Core/Src/IOSyncBackends.h | 7 ++++++- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index a3da67d5a389..a58087ec5caa 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -481,7 +481,7 @@ class PointerWrap }; // Convenience methods for packets. -class Packet : public PointerWrap +class Packet : public PointerWrap, public NonCopyable { public: Packet() : PointerWrap(NULL, MODE_WRITE) diff --git a/Source/Core/Common/Src/Common.h b/Source/Core/Common/Src/Common.h index 6f113c30828a..a0a1cb45a1cc 100644 --- a/Source/Core/Common/Src/Common.h +++ b/Source/Core/Common/Src/Common.h @@ -40,9 +40,9 @@ class NonCopyable NonCopyable() {} NonCopyable(const NonCopyable&&) {} void operator=(const NonCopyable&&) {} -private: - NonCopyable(const NonCopyable&); - NonCopyable& operator=(const NonCopyable& other); + + NonCopyable(const NonCopyable&) = delete; + NonCopyable& operator=(const NonCopyable& other) = delete; }; #endif diff --git a/Source/Core/Common/Src/NetHost.cpp b/Source/Core/Common/Src/NetHost.cpp index f5e98c2db92d..fa2f66ec3905 100644 --- a/Source/Core/Common/Src/NetHost.cpp +++ b/Source/Core/Common/Src/NetHost.cpp @@ -143,7 +143,7 @@ void NetHost::BroadcastPacket(Packet&& packet, ENetPeer* except) if (packet.vec->size() < MaxShortPacketLength) { u16 seq = m_GlobalSequenceNumber++; - m_OutgoingPacketInfo.push_back(OutgoingPacketInfo(std::move(packet), except, seq, m_GlobalTicker++)); + m_OutgoingPacketInfo.emplace_back(std::move(packet), except, seq, m_GlobalTicker++); size_t peer = 0; for (auto& pi : m_PeerInfo) { diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index 5debbc6d625f..d54f29977a46 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -78,7 +78,7 @@ void BackendNetPlay::StartGame() m_HaveClearReservationPacket = false; for (auto& dis : m_DeviceInfo) for (auto& di : dis) - di = DeviceInfo(); + di.Reset(); Packet p; // Flush out any old stuff (but not new stuff!) while (m_PacketsPendingProcessing.Pop(p)) @@ -191,7 +191,7 @@ Packet BackendNetPlay::DequeueReport(int classId, int index, bool* keepGoing) void BackendNetPlay::DoDisconnect(int classId, int index) { - m_DeviceInfo[classId][index] = DeviceInfo(); + m_DeviceInfo[classId][index].Reset(); if (g_Classes[classId]->IsConnected(index)) g_Classes[classId]->OnDisconnected(index); } diff --git a/Source/Core/Core/Src/IOSyncBackends.h b/Source/Core/Core/Src/IOSyncBackends.h index ef9a35305017..40524ff04ecd 100644 --- a/Source/Core/Core/Src/IOSyncBackends.h +++ b/Source/Core/Core/Src/IOSyncBackends.h @@ -48,12 +48,17 @@ class BackendNetPlay : public Backend // from (arbitrarily-ish) SI virtual void NewLocalSubframe() override; private: - struct DeviceInfo + struct DeviceInfo : public NonCopyable { DeviceInfo() + { + Reset(); + } + void Reset() { m_SubframeId = 0; m_LastSentSubframeId = 0; + m_IncomingQueue.clear(); } std::deque m_IncomingQueue; s64 m_SubframeId; From f121958cff3a47a0722a71ec6116b43e993be07f Mon Sep 17 00:00:00 2001 From: comex Date: Mon, 25 Nov 2013 17:54:05 -0500 Subject: [PATCH 144/202] Fix the select box not selecting anything by default on Windows. --- Source/Core/DolphinWX/Src/NetWindow.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index c714d7bc6539..a26974838d79 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -157,6 +157,7 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const m_host_type_choice = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxSize(60, -1)); m_host_type_choice->Bind(wxEVT_COMMAND_CHOICE_SELECTED, &NetPlayDiag::OnChoice, this); m_host_type_choice->Append(_("ID:")); + m_host_type_choice->Select(0); host_szr->Add(m_host_type_choice); // The initial label is for sizing... m_host_label = new wxStaticText(panel, wxID_ANY, "555.555.555.555:55555", wxDefaultPosition, wxDefaultSize, wxST_NO_AUTORESIZE | wxALIGN_LEFT); From db9c586356d402d3bf8ce70ece54526f8fcd9fc3 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Mon, 25 Nov 2013 16:56:04 -0600 Subject: [PATCH 145/202] Revert "jit: change our linking module to be able to handle arbitrary exit addresses" This shouldn't cause issues, but does in Windows. Revert for now. This reverts commit 1aa06b8fa4105d22cac0dd847230215099bbeb78. --- Source/Core/Core/Src/PowerPC/Jit64/Jit.cpp | 15 ++++------ Source/Core/Core/Src/PowerPC/Jit64/Jit.h | 2 +- .../Core/Src/PowerPC/Jit64/Jit_Branch.cpp | 10 +++---- .../Core/Src/PowerPC/Jit64/Jit_Integer.cpp | 10 +++---- .../Core/Src/PowerPC/Jit64/Jit_LoadStore.cpp | 2 +- .../Src/PowerPC/Jit64/Jit_SystemRegisters.cpp | 2 +- .../Core/Core/Src/PowerPC/Jit64IL/IR_X86.cpp | 13 ++++----- .../Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp | 18 +++++------- Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.h | 4 +-- Source/Core/Core/Src/PowerPC/JitArm32/Jit.cpp | 13 ++++----- Source/Core/Core/Src/PowerPC/JitArm32/Jit.h | 2 +- .../Src/PowerPC/JitArm32/JitArm_Branch.cpp | 10 +++---- .../Src/PowerPC/JitArm32/JitArm_LoadStore.cpp | 2 +- .../JitArm32/JitArm_SystemRegisters.cpp | 2 +- .../Core/Core/Src/PowerPC/JitArmIL/IR_Arm.cpp | 11 ++++--- .../Core/Core/Src/PowerPC/JitArmIL/JitIL.cpp | 15 ++++------ Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.h | 4 +-- .../Core/Src/PowerPC/JitCommon/JitCache.cpp | 29 ++++++++++++------- .../Core/Src/PowerPC/JitCommon/JitCache.h | 11 +++---- 19 files changed, 83 insertions(+), 92 deletions(-) diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit.cpp b/Source/Core/Core/Src/PowerPC/Jit64/Jit.cpp index 87fb248c1f7e..394c7bc86ae3 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit.cpp @@ -276,7 +276,7 @@ void Jit64::Cleanup() ABI_CallFunctionCCC((void *)&PowerPC::UpdatePerformanceMonitor, js.downcountAmount, jit->js.numLoadStoreInst, jit->js.numFloatingPointInst); } -void Jit64::WriteExit(u32 destination) +void Jit64::WriteExit(u32 destination, int exit_num) { Cleanup(); @@ -284,9 +284,8 @@ void Jit64::WriteExit(u32 destination) //If nobody has taken care of this yet (this can be removed when all branches are done) JitBlock *b = js.curBlock; - JitBlock::LinkData linkData; - linkData.exitAddress = destination; - linkData.exitPtrs = GetWritableCodePtr(); + b->exitAddress[exit_num] = destination; + b->exitPtrs[exit_num] = GetWritableCodePtr(); // Link opportunity! if (jo.enableBlocklink) @@ -296,14 +295,12 @@ void Jit64::WriteExit(u32 destination) { // It exists! Joy of joy! JMP(blocks.GetBlock(block)->checkedEntry, true); - linkData.linkStatus = true; + b->linkStatus[exit_num] = true; return; } } MOV(32, M(&PC), Imm32(destination)); JMP(asm_routines.dispatcher, true); - - b->linkData.push_back(linkData); } void Jit64::WriteExitDestInEAX() @@ -628,7 +625,7 @@ const u8* Jit64::DoJit(u32 em_address, PPCAnalyst::CodeBuffer *code_buf, JitBloc TEST(32, M((void*)PowerPC::GetStatePtr()), Imm32(0xFFFFFFFF)); FixupBranch noBreakpoint = J_CC(CC_Z); - WriteExit(ops[i].address); + WriteExit(ops[i].address, 0); SetJumpTarget(noBreakpoint); } @@ -710,7 +707,7 @@ const u8* Jit64::DoJit(u32 em_address, PPCAnalyst::CodeBuffer *code_buf, JitBloc { gpr.Flush(FLUSH_ALL); fpr.Flush(FLUSH_ALL); - WriteExit(nextPC); + WriteExit(nextPC, 0); } b->flags = js.block_flags; diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit.h b/Source/Core/Core/Src/PowerPC/Jit64/Jit.h index 1adbdfaac493..139414a1035d 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit.h +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit.h @@ -99,7 +99,7 @@ class Jit64 : public Jitx86Base // Utilities for use by opcodes - void WriteExit(u32 destination); + void WriteExit(u32 destination, int exit_num); void WriteExitDestInEAX(); void WriteExceptionExit(); void WriteExternalExceptionExit(); diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit_Branch.cpp b/Source/Core/Core/Src/PowerPC/Jit64/Jit_Branch.cpp index a1e28ac6ea80..b0f3f1cd1853 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit_Branch.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit_Branch.cpp @@ -91,7 +91,7 @@ void Jit64::bx(UGeckoInstruction inst) // make idle loops go faster js.downcountAmount += 8; } - WriteExit(destination); + WriteExit(destination, 0); } // TODO - optimize to hell and beyond @@ -136,13 +136,13 @@ void Jit64::bcx(UGeckoInstruction inst) destination = SignExt16(inst.BD << 2); else destination = js.compilerPC + SignExt16(inst.BD << 2); - WriteExit(destination); + WriteExit(destination, 0); if ((inst.BO & BO_DONT_CHECK_CONDITION) == 0) SetJumpTarget( pConditionDontBranch ); if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) SetJumpTarget( pCTRDontBranch ); - WriteExit(js.compilerPC + 4); + WriteExit(js.compilerPC + 4, 1); } void Jit64::bcctrx(UGeckoInstruction inst) @@ -190,7 +190,7 @@ void Jit64::bcctrx(UGeckoInstruction inst) WriteExitDestInEAX(); // Would really like to continue the block here, but it ends. TODO. SetJumpTarget(b); - WriteExit(js.compilerPC + 4); + WriteExit(js.compilerPC + 4, 1); } } @@ -245,5 +245,5 @@ void Jit64::bclrx(UGeckoInstruction inst) SetJumpTarget( pConditionDontBranch ); if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) SetJumpTarget( pCTRDontBranch ); - WriteExit(js.compilerPC + 4); + WriteExit(js.compilerPC + 4, 1); } diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/Src/PowerPC/Jit64/Jit_Integer.cpp index 8299f2b044ff..244a051aaa93 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit_Integer.cpp @@ -400,7 +400,7 @@ void Jit64::cmpXX(UGeckoInstruction inst) destination = SignExt16(js.next_inst.BD << 2); else destination = js.next_compilerPC + SignExt16(js.next_inst.BD << 2); - WriteExit(destination); + WriteExit(destination, 0); } else if ((js.next_inst.OPCD == 19) && (js.next_inst.SUBOP10 == 528)) // bcctrx { @@ -424,7 +424,7 @@ void Jit64::cmpXX(UGeckoInstruction inst) } else { - WriteExit(js.next_compilerPC + 4); + WriteExit(js.next_compilerPC + 4, 0); } js.cancel = true; @@ -507,7 +507,7 @@ void Jit64::cmpXX(UGeckoInstruction inst) destination = SignExt16(js.next_inst.BD << 2); else destination = js.next_compilerPC + SignExt16(js.next_inst.BD << 2); - WriteExit(destination); + WriteExit(destination, 0); } else if ((js.next_inst.OPCD == 19) && (js.next_inst.SUBOP10 == 528)) // bcctrx { @@ -534,7 +534,7 @@ void Jit64::cmpXX(UGeckoInstruction inst) if (!!(4 & test_bit) == condition) SetJumpTarget(continue2); if (!!(2 & test_bit) == condition) SetJumpTarget(continue1); - WriteExit(js.next_compilerPC + 4); + WriteExit(js.next_compilerPC + 4, 1); js.cancel = true; } @@ -2221,5 +2221,5 @@ void Jit64::twx(UGeckoInstruction inst) SetJumpTarget(exit3); SetJumpTarget(exit4); SetJumpTarget(exit5); - WriteExit(js.compilerPC + 4); + WriteExit(js.compilerPC + 4, 1); } diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit_LoadStore.cpp b/Source/Core/Core/Src/PowerPC/Jit64/Jit_LoadStore.cpp index 1b41c2f1e93b..a1c3bd971c11 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit_LoadStore.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit_LoadStore.cpp @@ -480,5 +480,5 @@ void Jit64::stmw(UGeckoInstruction inst) void Jit64::icbi(UGeckoInstruction inst) { Default(inst); - WriteExit(js.compilerPC + 4); + WriteExit(js.compilerPC + 4, 0); } diff --git a/Source/Core/Core/Src/PowerPC/Jit64/Jit_SystemRegisters.cpp b/Source/Core/Core/Src/PowerPC/Jit64/Jit_SystemRegisters.cpp index 8282758a74c1..573feb375690 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64/Jit_SystemRegisters.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64/Jit_SystemRegisters.cpp @@ -137,7 +137,7 @@ void Jit64::mtmsr(UGeckoInstruction inst) SetJumpTarget(noExceptionsPending); SetJumpTarget(eeDisabled); - WriteExit(js.compilerPC + 4); + WriteExit(js.compilerPC + 4, 0); js.firstFPInstructionFound = false; } diff --git a/Source/Core/Core/Src/PowerPC/Jit64IL/IR_X86.cpp b/Source/Core/Core/Src/PowerPC/Jit64IL/IR_X86.cpp index ef1db98de1d6..7a37a1cb438f 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64IL/IR_X86.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64IL/IR_X86.cpp @@ -552,8 +552,7 @@ static void regEmitICmpInst(RegInfo& RI, InstLoc I, CCFlags flag) { static void regWriteExit(RegInfo& RI, InstLoc dest) { if (isImm(*dest)) { - RI.exitNumber++; - RI.Jit->WriteExit(RI.Build->GetImmValue(dest)); + RI.Jit->WriteExit(RI.Build->GetImmValue(dest), RI.exitNumber++); } else { RI.Jit->WriteExitDestInOpArg(regLocForInst(RI, dest)); } @@ -565,7 +564,7 @@ static bool checkIsSNAN() { return MathUtil::IsSNAN(isSNANTemp[0][0]) || MathUtil::IsSNAN(isSNANTemp[1][0]); } -static void DoWriteCode(IRBuilder* ibuild, JitIL* Jit, u32 exitAddress) { +static void DoWriteCode(IRBuilder* ibuild, JitIL* Jit) { //printf("Writing block: %x\n", js.blockStart); RegInfo RI(Jit, ibuild->getFirstInst(), ibuild->getNumInsts()); RI.Build = ibuild; @@ -1792,7 +1791,7 @@ static void DoWriteCode(IRBuilder* ibuild, JitIL* Jit, u32 exitAddress) { Jit->ABI_CallFunction(reinterpret_cast(&PowerPC::CheckBreakPoints)); Jit->TEST(32, M((void*)PowerPC::GetStatePtr()), Imm32(0xFFFFFFFF)); FixupBranch noBreakpoint = Jit->J_CC(CC_Z); - Jit->WriteExit(InstLoc); + Jit->WriteExit(InstLoc, 0); Jit->SetJumpTarget(noBreakpoint); break; } @@ -1820,10 +1819,10 @@ static void DoWriteCode(IRBuilder* ibuild, JitIL* Jit, u32 exitAddress) { } } - Jit->WriteExit(exitAddress); + Jit->WriteExit(jit->js.curBlock->exitAddress[0], 0); Jit->UD2(); } -void JitIL::WriteCode(u32 exitAddress) { - DoWriteCode(&ibuild, this, exitAddress); +void JitIL::WriteCode() { + DoWriteCode(&ibuild, this); } diff --git a/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp b/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp index 1518bc57bb7d..b15ffbaccac2 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp +++ b/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.cpp @@ -381,7 +381,7 @@ void JitIL::Cleanup() ABI_CallFunctionCCC((void *)&PowerPC::UpdatePerformanceMonitor, js.downcountAmount, jit->js.numLoadStoreInst, jit->js.numFloatingPointInst); } -void JitIL::WriteExit(u32 destination) +void JitIL::WriteExit(u32 destination, int exit_num) { Cleanup(); if (SConfig::GetInstance().m_LocalCoreStartupParameter.bJITILTimeProfiling) { @@ -391,9 +391,8 @@ void JitIL::WriteExit(u32 destination) //If nobody has taken care of this yet (this can be removed when all branches are done) JitBlock *b = js.curBlock; - JitBlock::LinkData linkData; - linkData.exitAddress = destination; - linkData.exitPtrs = GetWritableCodePtr(); + b->exitAddress[exit_num] = destination; + b->exitPtrs[exit_num] = GetWritableCodePtr(); // Link opportunity! int block = blocks.GetBlockNumberFromStartAddress(destination); @@ -401,14 +400,13 @@ void JitIL::WriteExit(u32 destination) { // It exists! Joy of joy! JMP(blocks.GetBlock(block)->checkedEntry, true); - linkData.linkStatus = true; + b->linkStatus[exit_num] = true; } else { MOV(32, M(&PC), Imm32(destination)); JMP(asm_routines.dispatcher, true); } - b->linkData.push_back(linkData); } void JitIL::WriteExitDestInOpArg(const Gen::OpArg& arg) @@ -543,16 +541,14 @@ const u8* JitIL::DoJit(u32 em_address, PPCAnalyst::CodeBuffer *code_buf, JitBloc // Analyze the block, collect all instructions it is made of (including inlining, // if that is enabled), reorder instructions for optimal performance, and join joinable instructions. - u32 exitAddress = em_address; - + b->exitAddress[0] = em_address; u32 merged_addresses[32]; const int capacity_of_merged_addresses = sizeof(merged_addresses) / sizeof(merged_addresses[0]); int size_of_merged_addresses = 0; if (!memory_exception) { // If there is a memory exception inside a block (broken_block==true), compile up to that instruction. - // TODO - exitAddress = PPCAnalyst::Flatten(em_address, &size, &js.st, &js.gpa, &js.fpa, broken_block, code_buf, blockSize, merged_addresses, capacity_of_merged_addresses, size_of_merged_addresses); + b->exitAddress[0] = PPCAnalyst::Flatten(em_address, &size, &js.st, &js.gpa, &js.fpa, broken_block, code_buf, blockSize, merged_addresses, capacity_of_merged_addresses, size_of_merged_addresses); } PPCAnalyst::CodeOp *ops = code_buf->codebuffer; @@ -711,7 +707,7 @@ const u8* JitIL::DoJit(u32 em_address, PPCAnalyst::CodeBuffer *code_buf, JitBloc } // Perform actual code generation - WriteCode(exitAddress); + WriteCode(); b->codeSize = (u32)(GetCodePtr() - normalEntry); b->originalSize = size; diff --git a/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.h b/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.h index 305a96015f01..e56a56c8156c 100644 --- a/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.h +++ b/Source/Core/Core/Src/PowerPC/Jit64IL/JitIL.h @@ -105,7 +105,7 @@ class JitIL : public JitILBase, public EmuCodeBlock // Utilities for use by opcodes - void WriteExit(u32 destination); + void WriteExit(u32 destination, int exit_num); void WriteExitDestInOpArg(const Gen::OpArg& arg); void WriteExceptionExit(); void WriteRfiExitDestInOpArg(const Gen::OpArg& arg); @@ -121,7 +121,7 @@ class JitIL : public JitILBase, public EmuCodeBlock void regimmop(int d, int a, bool binary, u32 value, Operation doop, void (Gen::XEmitter::*op)(int, const Gen::OpArg&, const Gen::OpArg&), bool Rc = false, bool carry = false); void fp_tri_op(int d, int a, int b, bool reversible, bool dupe, void (Gen::XEmitter::*op)(Gen::X64Reg, Gen::OpArg)); - void WriteCode(u32 exitAddress); + void WriteCode(); // OPCODES void unknown_instruction(UGeckoInstruction _inst) override; diff --git a/Source/Core/Core/Src/PowerPC/JitArm32/Jit.cpp b/Source/Core/Core/Src/PowerPC/JitArm32/Jit.cpp index eab7f3711a30..4083a383e976 100644 --- a/Source/Core/Core/Src/PowerPC/JitArm32/Jit.cpp +++ b/Source/Core/Core/Src/PowerPC/JitArm32/Jit.cpp @@ -186,16 +186,15 @@ void JitArm::WriteExceptionExit() MOVI2R(A, (u32)asm_routines.testExceptions); B(A); } -void JitArm::WriteExit(u32 destination) +void JitArm::WriteExit(u32 destination, int exit_num) { Cleanup(); DoDownCount(); //If nobody has taken care of this yet (this can be removed when all branches are done) JitBlock *b = js.curBlock; - JitBlock::LinkData linkData; - linkData.exitAddress = destination; - linkData.exitPtrs = GetWritableCodePtr(); + b->exitAddress[exit_num] = destination; + b->exitPtrs[exit_num] = GetWritableCodePtr(); // Link opportunity! int block = blocks.GetBlockNumberFromStartAddress(destination); @@ -203,7 +202,7 @@ void JitArm::WriteExit(u32 destination) { // It exists! Joy of joy! B(blocks.GetBlock(block)->checkedEntry); - linkData.linkStatus = true; + b->linkStatus[exit_num] = true; } else { @@ -213,8 +212,6 @@ void JitArm::WriteExit(u32 destination) MOVI2R(A, (u32)asm_routines.dispatcher); B(A); } - - b->linkData.push_back(linkData); } void STACKALIGN JitArm::Run() @@ -499,7 +496,7 @@ const u8* JitArm::DoJit(u32 em_address, PPCAnalyst::CodeBuffer *code_buf, JitBlo if (broken_block) { printf("Broken Block going to 0x%08x\n", nextPC); - WriteExit(nextPC); + WriteExit(nextPC, 0); } b->flags = js.block_flags; diff --git a/Source/Core/Core/Src/PowerPC/JitArm32/Jit.h b/Source/Core/Core/Src/PowerPC/JitArm32/Jit.h index 4d1d2d8a4e60..fc1911c5bfcc 100644 --- a/Source/Core/Core/Src/PowerPC/JitArm32/Jit.h +++ b/Source/Core/Core/Src/PowerPC/JitArm32/Jit.h @@ -109,7 +109,7 @@ class JitArm : public JitBase, public ArmGen::ARMXCodeBlock // Utilities for use by opcodes - void WriteExit(u32 destination); + void WriteExit(u32 destination, int exit_num); void WriteExitDestInR(ARMReg Reg); void WriteRfiExitDestInR(ARMReg Reg); void WriteExceptionExit(); diff --git a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_Branch.cpp b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_Branch.cpp index eac488a5b060..4e9101f7b20e 100644 --- a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_Branch.cpp +++ b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_Branch.cpp @@ -154,7 +154,7 @@ void JitArm::bx(UGeckoInstruction inst) MOVI2R(R14, (u32)asm_routines.testExceptions); B(R14); } - WriteExit(destination); + WriteExit(destination, 0); } void JitArm::bcx(UGeckoInstruction inst) @@ -209,14 +209,14 @@ void JitArm::bcx(UGeckoInstruction inst) destination = SignExt16(inst.BD << 2); else destination = js.compilerPC + SignExt16(inst.BD << 2); - WriteExit(destination); + WriteExit(destination, 0); if ((inst.BO & BO_DONT_CHECK_CONDITION) == 0) SetJumpTarget( pConditionDontBranch ); if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) SetJumpTarget( pCTRDontBranch ); - WriteExit(js.compilerPC + 4); + WriteExit(js.compilerPC + 4, 1); } void JitArm::bcctrx(UGeckoInstruction inst) { @@ -278,7 +278,7 @@ void JitArm::bcctrx(UGeckoInstruction inst) WriteExitDestInR(rA); SetJumpTarget(b); - WriteExit(js.compilerPC + 4); + WriteExit(js.compilerPC + 4, 1); } } void JitArm::bclrx(UGeckoInstruction inst) @@ -355,5 +355,5 @@ void JitArm::bclrx(UGeckoInstruction inst) SetJumpTarget( pConditionDontBranch ); if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) SetJumpTarget( pCTRDontBranch ); - WriteExit(js.compilerPC + 4); + WriteExit(js.compilerPC + 4, 1); } diff --git a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_LoadStore.cpp b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_LoadStore.cpp index 3983772aa6df..f04c88d6c313 100644 --- a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_LoadStore.cpp +++ b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_LoadStore.cpp @@ -531,6 +531,6 @@ void JitArm::dcbst(UGeckoInstruction inst) void JitArm::icbi(UGeckoInstruction inst) { Default(inst); - WriteExit(js.compilerPC + 4); + WriteExit(js.compilerPC + 4, 0); } diff --git a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_SystemRegisters.cpp b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_SystemRegisters.cpp index 62b15e1d5e72..e4ab630fce31 100644 --- a/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_SystemRegisters.cpp +++ b/Source/Core/Core/Src/PowerPC/JitArm32/JitArm_SystemRegisters.cpp @@ -205,7 +205,7 @@ void JitArm::mtmsr(UGeckoInstruction inst) gpr.Flush(); fpr.Flush(); - WriteExit(js.compilerPC + 4); + WriteExit(js.compilerPC + 4, 0); } void JitArm::mfmsr(UGeckoInstruction inst) diff --git a/Source/Core/Core/Src/PowerPC/JitArmIL/IR_Arm.cpp b/Source/Core/Core/Src/PowerPC/JitArmIL/IR_Arm.cpp index c1bee507fc4f..10620d918313 100644 --- a/Source/Core/Core/Src/PowerPC/JitArmIL/IR_Arm.cpp +++ b/Source/Core/Core/Src/PowerPC/JitArmIL/IR_Arm.cpp @@ -156,8 +156,7 @@ static ARMReg regEnsureInReg(RegInfo& RI, InstLoc I) { static void regWriteExit(RegInfo& RI, InstLoc dest) { if (isImm(*dest)) { - RI.exitNumber++; - RI.Jit->WriteExit(RI.Build->GetImmValue(dest)); + RI.Jit->WriteExit(RI.Build->GetImmValue(dest), RI.exitNumber++); } else { RI.Jit->WriteExitDestInReg(regLocForInst(RI, dest)); } @@ -282,7 +281,7 @@ static void regEmitCmp(RegInfo& RI, InstLoc I) { } } -static void DoWriteCode(IRBuilder* ibuild, JitArmIL* Jit, u32 exitAddress) { +static void DoWriteCode(IRBuilder* ibuild, JitArmIL* Jit) { RegInfo RI(Jit, ibuild->getFirstInst(), ibuild->getNumInsts()); RI.Build = ibuild; @@ -734,10 +733,10 @@ static void DoWriteCode(IRBuilder* ibuild, JitArmIL* Jit, u32 exitAddress) { } } - Jit->WriteExit(exitAddress); + Jit->WriteExit(jit->js.curBlock->exitAddress[0], 0); Jit->BKPT(0x111); } -void JitArmIL::WriteCode(u32 exitAddress) { - DoWriteCode(&ibuild, this, exitAddress); +void JitArmIL::WriteCode() { + DoWriteCode(&ibuild, this); } diff --git a/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.cpp b/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.cpp index 44ebc8b6eb24..0aac4d6722e2 100644 --- a/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.cpp +++ b/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.cpp @@ -117,14 +117,13 @@ void JitArmIL::WriteExceptionExit() MOVI2R(R14, (u32)asm_routines.testExceptions); B(R14); } -void JitArmIL::WriteExit(u32 destination) +void JitArmIL::WriteExit(u32 destination, int exit_num) { DoDownCount(); //If nobody has taken care of this yet (this can be removed when all branches are done) JitBlock *b = js.curBlock; - JitBlock::LinkData linkData; - linkData.exitAddress = destination; - linkData.exitPtrs = GetWritableCodePtr(); + b->exitAddress[exit_num] = destination; + b->exitPtrs[exit_num] = GetWritableCodePtr(); // Link opportunity! int block = blocks.GetBlockNumberFromStartAddress(destination); @@ -132,7 +131,7 @@ void JitArmIL::WriteExit(u32 destination) { // It exists! Joy of joy! B(blocks.GetBlock(block)->checkedEntry); - linkData.linkStatus = true; + b->linkStatus[exit_num] = true; } else { @@ -141,8 +140,6 @@ void JitArmIL::WriteExit(u32 destination) MOVI2R(R14, (u32)asm_routines.dispatcher); B(R14); } - - b->linkData.push_back(linkData); } void JitArmIL::PrintDebug(UGeckoInstruction inst, u32 level) { @@ -350,12 +347,12 @@ const u8* JitArmIL::DoJit(u32 em_address, PPCAnalyst::CodeBuffer *code_buf, JitB if (broken_block) { printf("Broken Block going to 0x%08x\n", nextPC); - WriteExit(nextPC); + WriteExit(nextPC, 0); } // Perform actual code generation - WriteCode(nextPC); + WriteCode(); b->flags = js.block_flags; b->codeSize = (u32)(GetCodePtr() - normalEntry); b->originalSize = size; diff --git a/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.h b/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.h index 71b09a32510d..4dec87ddeba6 100644 --- a/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.h +++ b/Source/Core/Core/Src/PowerPC/JitArmIL/JitIL.h @@ -64,8 +64,8 @@ class JitArmIL : public JitILBase, public ArmGen::ARMXCodeBlock void Run(); void SingleStep(); // - void WriteCode(u32 exitAddress); - void WriteExit(u32 destination); + void WriteCode(); + void WriteExit(u32 destination, int exit_num); void WriteExitDestInReg(ARMReg Reg); void WriteRfiExitDestInR(ARMReg Reg); void WriteExceptionExit(); diff --git a/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.cpp b/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.cpp index b81dadd39440..d7c78d9d174f 100644 --- a/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.cpp +++ b/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.cpp @@ -35,6 +35,8 @@ op_agent_t agent; using namespace Gen; +#define INVALID_EXIT 0xFFFFFFFF + bool JitBaseBlockCache::IsFull() const { return GetNumBlocks() >= MAX_NUM_BLOCKS - 1; @@ -165,6 +167,12 @@ using namespace Gen; JitBlock &b = blocks[num_blocks]; b.invalid = false; b.originalAddress = em_address; + b.exitAddress[0] = INVALID_EXIT; + b.exitAddress[1] = INVALID_EXIT; + b.exitPtrs[0] = 0; + b.exitPtrs[1] = 0; + b.linkStatus[0] = false; + b.linkStatus[1] = false; num_blocks++; //commit the current block return num_blocks - 1; } @@ -185,9 +193,10 @@ using namespace Gen; block_map[std::make_pair(pAddr + 4 * b.originalSize - 1, pAddr)] = block_num; if (block_link) { - for (const auto& e : b.linkData) + for (int i = 0; i < 2; i++) { - links_to.insert(std::pair(e.exitAddress, block_num)); + if (b.exitAddress[i] != INVALID_EXIT) + links_to.insert(std::pair(b.exitAddress[i], block_num)); } LinkBlock(block_num); @@ -266,15 +275,15 @@ using namespace Gen; // This block is dead. Don't relink it. return; } - for (auto& e : b.linkData) + for (int e = 0; e < 2; e++) { - if (!e.linkStatus) + if (b.exitAddress[e] != INVALID_EXIT && !b.linkStatus[e]) { - int destinationBlock = GetBlockNumberFromStartAddress(e.exitAddress); + int destinationBlock = GetBlockNumberFromStartAddress(b.exitAddress[e]); if (destinationBlock != -1) { - WriteLinkBlock(e.exitPtrs, blocks[destinationBlock].checkedEntry); - e.linkStatus = true; + WriteLinkBlock(b.exitPtrs[e], blocks[destinationBlock].checkedEntry); + b.linkStatus[e] = true; } } } @@ -307,10 +316,10 @@ using namespace Gen; return; for (multimap::iterator iter = ppp.first; iter != ppp.second; ++iter) { JitBlock &sourceBlock = blocks[iter->second]; - for (auto& e : sourceBlock.linkData) + for (int e = 0; e < 2; e++) { - if (e.exitAddress == b.originalAddress) - e.linkStatus = false; + if (sourceBlock.exitAddress[e] == b.originalAddress) + sourceBlock.linkStatus[e] = false; } } } diff --git a/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.h b/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.h index ffbffaadb575..b81c5d837a23 100644 --- a/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.h +++ b/Source/Core/Core/Src/PowerPC/JitCommon/JitCache.h @@ -35,6 +35,9 @@ struct JitBlock const u8 *checkedEntry; const u8 *normalEntry; + u8 *exitPtrs[2]; // to be able to rewrite the exit jum + u32 exitAddress[2]; // 0xFFFFFFFF == unknown + u32 originalAddress; u32 codeSize; u32 originalSize; @@ -42,13 +45,7 @@ struct JitBlock int flags; bool invalid; - - struct LinkData { - u8 *exitPtrs; // to be able to rewrite the exit jum - u32 exitAddress; - bool linkStatus; // is it already linked? - }; - std::vector linkData; + bool linkStatus[2]; #ifdef _WIN32 // we don't really need to save start and stop From e99f94e56cd3b53259f17ee530b6012554fb1c35 Mon Sep 17 00:00:00 2001 From: comex Date: Mon, 25 Nov 2013 18:04:53 -0500 Subject: [PATCH 146/202] As shuffle2 noted... four years ago, the Update functions in EXI are unused. Remove them. --- Source/Core/Core/Src/HW/EXI.cpp | 8 -------- Source/Core/Core/Src/HW/EXI_Channel.cpp | 7 ------- Source/Core/Core/Src/HW/EXI_Channel.h | 1 - Source/Core/Core/Src/HW/EXI_Device.h | 3 --- .../Core/Core/Src/HW/EXI_DeviceMemoryCard.cpp | 18 ------------------ Source/Core/Core/Src/HW/EXI_DeviceMemoryCard.h | 2 -- 6 files changed, 39 deletions(-) diff --git a/Source/Core/Core/Src/HW/EXI.cpp b/Source/Core/Core/Src/HW/EXI.cpp index 30b7728fe91d..884eccf86171 100644 --- a/Source/Core/Core/Src/HW/EXI.cpp +++ b/Source/Core/Core/Src/HW/EXI.cpp @@ -96,14 +96,6 @@ IEXIDevice* FindDevice(TEXIDevices device_type, int customIndex) return NULL; } -// Unused (?!) -void Update() -{ - g_Channels[0]->Update(); - g_Channels[1]->Update(); - g_Channels[2]->Update(); -} - void Read32(u32& _uReturnValue, const u32 _iAddress) { // TODO 0xfff00000 is mapped to EXI -> mapped to first MB of maskrom diff --git a/Source/Core/Core/Src/HW/EXI_Channel.cpp b/Source/Core/Core/Src/HW/EXI_Channel.cpp index a57b06914c77..bc3232b4f8c7 100644 --- a/Source/Core/Core/Src/HW/EXI_Channel.cpp +++ b/Source/Core/Core/Src/HW/EXI_Channel.cpp @@ -108,13 +108,6 @@ IEXIDevice* CEXIChannel::GetDevice(const u8 chip_select) return NULL; } -void CEXIChannel::Update() -{ - // start the transfer - for (auto& device : m_pDevices) - device->Update(); -} - void CEXIChannel::Read32(u32& _uReturnValue, const u32 _iRegister) { switch (_iRegister) diff --git a/Source/Core/Core/Src/HW/EXI_Channel.h b/Source/Core/Core/Src/HW/EXI_Channel.h index 9ede316f0e9b..a2dad1528fa8 100644 --- a/Source/Core/Core/Src/HW/EXI_Channel.h +++ b/Source/Core/Core/Src/HW/EXI_Channel.h @@ -118,7 +118,6 @@ class CEXIChannel void Read32(u32& _uReturnValue, const u32 _iRegister); void Write32(const u32 _iValue, const u32 _iRegister); - void Update(); bool IsCausingInterrupt(); void DoState(PointerWrap &p); void PauseAndLock(bool doLock, bool unpauseOnUnlock); diff --git a/Source/Core/Core/Src/HW/EXI_Device.h b/Source/Core/Core/Src/HW/EXI_Device.h index 0b4840503389..578159c0f18a 100644 --- a/Source/Core/Core/Src/HW/EXI_Device.h +++ b/Source/Core/Core/Src/HW/EXI_Device.h @@ -43,9 +43,6 @@ class IEXIDevice virtual void PauseAndLock(bool doLock, bool unpauseOnUnlock=true) {} virtual IEXIDevice* FindDevice(TEXIDevices device_type, int customIndex=-1) { return (device_type == m_deviceType) ? this : NULL; } - // Update - virtual void Update() {} - // Is generating interrupt ? virtual bool IsInterruptSet() {return false;} virtual ~IEXIDevice() {} diff --git a/Source/Core/Core/Src/HW/EXI_DeviceMemoryCard.cpp b/Source/Core/Core/Src/HW/EXI_DeviceMemoryCard.cpp index 2d1fb56c2ac4..93682bb40aec 100644 --- a/Source/Core/Core/Src/HW/EXI_DeviceMemoryCard.cpp +++ b/Source/Core/Core/Src/HW/EXI_DeviceMemoryCard.cpp @@ -60,7 +60,6 @@ CEXIMemoryCard::CEXIMemoryCard(const int index) status = MC_STATUS_BUSY | MC_STATUS_UNLOCKED | MC_STATUS_READY; m_uPosition = 0; memset(programming_buffer, 0, sizeof(programming_buffer)); - formatDelay = 0; //Nintendo Memory Card EXI IDs //0x00000004 Memory Card 59 4Mbit @@ -258,22 +257,6 @@ void CEXIMemoryCard::SetCS(int cs) } } -void CEXIMemoryCard::Update() -{ - if (formatDelay) - { - formatDelay--; - - if (!formatDelay) - { - status |= MC_STATUS_READY; - status &= ~MC_STATUS_BUSY; - - m_bInterruptSet = 1; - } - } -} - bool CEXIMemoryCard::IsInterruptSet() { if (interruptSwitch) @@ -467,7 +450,6 @@ void CEXIMemoryCard::DoState(PointerWrap &p) p.Do(status); p.Do(m_uPosition); p.Do(programming_buffer); - p.Do(formatDelay); p.Do(m_bDirty); p.Do(address); diff --git a/Source/Core/Core/Src/HW/EXI_DeviceMemoryCard.h b/Source/Core/Core/Src/HW/EXI_DeviceMemoryCard.h index 2651ca08f0a2..6b9c6b8c74dd 100644 --- a/Source/Core/Core/Src/HW/EXI_DeviceMemoryCard.h +++ b/Source/Core/Core/Src/HW/EXI_DeviceMemoryCard.h @@ -22,7 +22,6 @@ class CEXIMemoryCard : public IEXIDevice CEXIMemoryCard(const int index); virtual ~CEXIMemoryCard(); void SetCS(int cs) override; - void Update() override; bool IsInterruptSet() override; bool IsPresent() override; void DoState(PointerWrap &p) override; @@ -77,7 +76,6 @@ class CEXIMemoryCard : public IEXIDevice int status; u32 m_uPosition; u8 programming_buffer[128]; - u32 formatDelay; bool m_bDirty; //! memory card parameters unsigned int nintendo_card_id, card_id; From 1138c2e155f0db6736dfc00b439fb510b2d6d05f Mon Sep 17 00:00:00 2001 From: degasus Date: Tue, 26 Nov 2013 04:07:59 +0100 Subject: [PATCH 147/202] OpenGL: reset EFB after efb2ram FB initialization --- Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index d2cb5b0414d0..d2e295c4f371 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -176,6 +176,7 @@ void Init() FramebufferManager::SetFramebuffer(s_texConvFrameBuffer[0]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, s_dstTexture, 0); + FramebufferManager::SetFramebuffer(0); CreatePrograms(); } From 95aeedec19db25f7de3bd37cc7281f6f70e0ab58 Mon Sep 17 00:00:00 2001 From: degasus Date: Tue, 26 Nov 2013 20:05:49 +0100 Subject: [PATCH 148/202] OpenGL: readback efb2ram with different strides at once This is done with a pixel buffer object. We still have to stall the GPU, but we only do it once per efb2ram call. As the cpu can't access the vram, it has to queue a memcpy for the gpu and wait for the gpu to finish this copy. We did this for every cache line which is just stupid. Now we copy the complete texture into a pbo and readback this at once. So we don't have to wait for lots of round-trip-times. --- .../OGL/Src/TextureConverter.cpp | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp index f653dfbf019f..290dc510cf6c 100644 --- a/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp +++ b/Source/Core/VideoBackends/OGL/Src/TextureConverter.cpp @@ -44,6 +44,8 @@ static GLuint s_encode_VBO = 0; static GLuint s_encode_VAO = 0; static TargetRectangle s_cached_sourceRc; +static GLuint s_PBO = 0; // for readback with different strides + static const char *VProgram = "ATTRIN vec2 rawpos;\n" "ATTRIN vec2 tex0;\n" @@ -186,6 +188,8 @@ void Init() glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, renderBufferWidth, renderBufferHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glGenBuffers(1, &s_PBO); + CreatePrograms(); } @@ -196,6 +200,7 @@ void Shutdown() glDeleteFramebuffers(1, &s_texConvFrameBuffer); glDeleteBuffers(1, &s_encode_VBO ); glDeleteVertexArrays(1, &s_encode_VAO ); + glDeleteBuffers(1, &s_PBO); s_rgbToYuyvProgram.Destroy(); s_yuyvToRgbProgram.Destroy(); @@ -206,6 +211,7 @@ void Shutdown() s_srcTexture = 0; s_dstTexture = 0; s_texConvFrameBuffer = 0; + s_PBO = 0; } void EncodeToRamUsingShader(GLuint srcTexture, const TargetRectangle& sourceRc, @@ -267,25 +273,37 @@ void EncodeToRamUsingShader(GLuint srcTexture, const TargetRectangle& sourceRc, // TODO: make this less slow. int writeStride = bpmem.copyMipMapStrideChannels * 32; + int dstSize = dstWidth*dstHeight*4; + int readHeight = readStride / dstWidth / 4; // 4 bytes per pixel + int readLoops = dstHeight / readHeight; - if (writeStride != readStride && toTexture) + if (writeStride != readStride && readLoops > 1 && toTexture) { // writing to a texture of a different size - - int readHeight = readStride / dstWidth; - readHeight /= 4; // 4 bytes per pixel - - int readStart = 0; - int readLoops = dstHeight / readHeight; + // also copy more then one block line, so the different strides matters + // copy into one pbo first, map this buffer, and then memcpy into gc memory + // in this way, we only have one vram->ram transfer, but maybe a bigger + // cpu overhead because of the pbo + glBindBuffer(GL_PIXEL_PACK_BUFFER, s_PBO); + glBufferData(GL_PIXEL_PACK_BUFFER, dstSize, NULL, GL_STREAM_READ); + glReadPixels(0, 0, (GLsizei)dstWidth, (GLsizei)dstHeight, GL_BGRA, GL_UNSIGNED_BYTE, 0); + u8* pbo = (u8*)glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, dstSize, GL_MAP_READ_BIT); + + //int readStart = 0; for (int i = 0; i < readLoops; i++) { - glReadPixels(0, readStart, (GLsizei)dstWidth, (GLsizei)readHeight, GL_BGRA, GL_UNSIGNED_BYTE, destAddr); - readStart += readHeight; + memcpy(destAddr, pbo, readStride); + pbo += readStride; destAddr += writeStride; } + + glUnmapBuffer(GL_PIXEL_PACK_BUFFER); + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); } else + { glReadPixels(0, 0, (GLsizei)dstWidth, (GLsizei)dstHeight, GL_BGRA, GL_UNSIGNED_BYTE, destAddr); + } GL_REPORT_ERRORD(); From 4ee64faecfa87887b70c831347a62c1ec51b6ecc Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 26 Nov 2013 20:42:44 -0500 Subject: [PATCH 149/202] Small fixes. --- Source/Core/Common/Src/NetHost.cpp | 27 +++++++++++++++++--------- Source/Core/Common/Src/NetHost.h | 3 +++ Source/Core/Core/Src/NetPlayClient.cpp | 26 ++++++++++++------------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/Source/Core/Common/Src/NetHost.cpp b/Source/Core/Common/Src/NetHost.cpp index fa2f66ec3905..606e71ecf769 100644 --- a/Source/Core/Common/Src/NetHost.cpp +++ b/Source/Core/Common/Src/NetHost.cpp @@ -104,24 +104,33 @@ void NetHost::RunOnThread(std::function func) void NetHost::RunOnThreadSync(std::function func) { - Common::Event evt; + volatile bool done = false; RunOnThread([&]() { func(); - evt.Set(); + std::unique_lock lk(m_SyncMutex); + done = true; + m_SyncCond.notify_all(); }); - evt.Wait(); + std::unique_lock lk(m_SyncMutex); + m_SyncCond.wait(lk, [&]{ return done; }); } void NetHost::RunOnThisThreadSync(std::function func) { - Common::Event evt, evt2; - RunOnThread([&]() { - evt.Set(); - evt2.Wait(); + volatile bool* flag = new bool; + *flag = false; + RunOnThread([=]() { + std::unique_lock lk(m_SyncMutex); + *flag = true; + m_SyncCond.notify_all(); + m_SyncCond.wait(lk, [&]{ return !*flag; }); + delete flag; }); - evt.Wait(); + std::unique_lock lk(m_SyncMutex); + m_SyncCond.wait(lk, [&]{ return *flag; }); func(); - evt2.Set(); + *flag = false; + m_SyncCond.notify_all(); } void NetHost::Reset() diff --git a/Source/Core/Common/Src/NetHost.h b/Source/Core/Common/Src/NetHost.h index 49b2c4a11380..c1afc8737a54 100644 --- a/Source/Core/Common/Src/NetHost.h +++ b/Source/Core/Common/Src/NetHost.h @@ -142,5 +142,8 @@ class NetHost std::vector m_PeerInfo ACCESS_ON(NET); u16 m_GlobalSequenceNumber ACCESS_ON(NET); u64 m_GlobalTicker ACCESS_ON(NET); + + std::mutex m_SyncMutex; + std::condition_variable m_SyncCond; }; diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index dde20394c234..f98b070ceb79 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -31,6 +31,8 @@ NetPlayClient::~NetPlayClient() if (m_net_host) m_net_host->Reset(); + IOSync::ResetBackend(); + if (!m_direct_connection) ReleaseTraversalClient(); } @@ -232,13 +234,6 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) } break; - case NP_MSG_PAD_BUFFER: - { - packet.Do(m_delay); - if (packet.failure) - return OnDisconnect(InvalidPacket); - /* fall through */ - } case NP_MSG_CHANGE_GAME : { @@ -249,6 +244,14 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) } break; + case NP_MSG_PAD_BUFFER: + { + packet.Do(m_delay); + if (packet.failure) + return OnDisconnect(InvalidPacket); + goto forward_to_iosync; + } + case NP_MSG_START_GAME : { if (m_is_running) @@ -272,13 +275,15 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) m_received_stop_request = false; m_dialog->OnMsgStartGame(); - // fall through + goto forward_to_iosync; } + case NP_MSG_DISCONNECT_DEVICE: case NP_MSG_CONNECT_DEVICE: case NP_MSG_REPORT: case NP_MSG_SET_RESERVATION: case NP_MSG_CLEAR_RESERVATION: + forward_to_iosync: { if (!m_backend) break; @@ -542,11 +547,6 @@ void NetPlayClient::GameStopped() { std::lock_guard lk(m_crit); - m_net_host->RunOnThreadSync([=]() { - ASSUME_ON(NET); - IOSync::ResetBackend(); - m_backend = NULL; - }); g_is_running = false; m_is_running = false; From f40d979f213cba76fd74fe1b0c093a7f4bf8e588 Mon Sep 17 00:00:00 2001 From: comex Date: Tue, 26 Nov 2013 21:53:09 -0500 Subject: [PATCH 150/202] Various fixes. --- Source/Core/Common/Src/TraversalClient.cpp | 1 + Source/Core/Common/Src/TraversalServer.cpp | 9 +++++++- Source/Core/Core/Src/HW/SI.cpp | 3 ++- Source/Core/Core/Src/IOSync.cpp | 26 ++++++++++++++++------ Source/Core/Core/Src/IOSync.h | 6 +++-- Source/Core/Core/Src/NetPlayClient.cpp | 4 ++-- Source/Core/Core/Src/NetPlayServer.cpp | 1 + Source/Core/Core/Src/NetPlayServer.h | 7 +++--- Source/Core/DolphinWX/Src/NetWindow.cpp | 4 +++- 9 files changed, 43 insertions(+), 18 deletions(-) diff --git a/Source/Core/Common/Src/TraversalClient.cpp b/Source/Core/Common/Src/TraversalClient.cpp index 1bfa48d6e068..b0d860c9dc39 100644 --- a/Source/Core/Common/Src/TraversalClient.cpp +++ b/Source/Core/Common/Src/TraversalClient.cpp @@ -268,6 +268,7 @@ TraversalRequestId TraversalClient::SendTraversalPacket(const TraversalPacket& p void TraversalClient::Reset() { m_PendingConnect = false; + m_Client = NULL; } std::unique_ptr g_TraversalClient; diff --git a/Source/Core/Common/Src/TraversalServer.cpp b/Source/Core/Common/Src/TraversalServer.cpp index 459b099ac84f..31aa2940b69f 100644 --- a/Source/Core/Common/Src/TraversalServer.cpp +++ b/Source/Core/Common/Src/TraversalServer.cpp @@ -73,6 +73,13 @@ EvictFindResult EvictFind(typename std::unordered_map>& map, } } } +#if DEBUG + printf("failed to find key '"); + for (size_t i = 0; i < sizeof(key); i++) { + printf("%02x", ((u8 *) &key)[i]); + } + printf("'\n"); +#endif result.found = false; return result; } @@ -433,7 +440,7 @@ int main() currentTime = (u64) tv.tv_sec * 1000000 + tv.tv_usec; if (rv < 0) { - if (errno != EAGAIN) + if (errno != EINTR && errno != EAGAIN) { perror("recvfrom"); return 1; diff --git a/Source/Core/Core/Src/HW/SI.cpp b/Source/Core/Core/Src/HW/SI.cpp index d36889eff77c..465c8366bd43 100644 --- a/Source/Core/Core/Src/HW/SI.cpp +++ b/Source/Core/Core/Src/HW/SI.cpp @@ -277,7 +277,6 @@ void PreInit() void Init() { - PreInit(); for (int i = 0; i < 4; i++) { char buf[64]; @@ -285,6 +284,8 @@ void Init() changeDevice[i] = CoreTiming::RegisterEvent(strdup(buf), ChangeDeviceCallback); } + PreInit(); + for (int i = 0; i < NUMBER_OF_CHANNELS; i++) { g_Channel[i].m_Out.Hex = 0; diff --git a/Source/Core/Core/Src/IOSync.cpp b/Source/Core/Core/Src/IOSync.cpp index 743ce7658837..84edee6b7d33 100644 --- a/Source/Core/Core/Src/IOSync.cpp +++ b/Source/Core/Core/Src/IOSync.cpp @@ -4,6 +4,14 @@ namespace IOSync { +Backend::Backend() +{ + for (int classId = 0; classId < Class::NumClasses; classId++) + { + g_Classes[classId]->ResetRemote(); + } +} + void Class::SetIndex(int index, int localIndex) { if (localIndex != -1) @@ -22,6 +30,16 @@ void Class::SetIndex(int index, int localIndex) } } +void Class::ResetRemote() +{ + for (int i = 0; i < MaxDeviceIndex; i++) + { + m_Remote[i] = DeviceInfo(); + m_Local[i].m_OtherIndex = -1; + m_Local[i].m_IsConnected = false; + } +} + void Class::OnConnected(int index, int localIndex, PWBuffer&& subtype) { SetIndex(index, localIndex); @@ -33,7 +51,6 @@ void Class::OnDisconnected(int index) { SetIndex(index, -1); m_Remote[index] = DeviceInfo(); - m_Remote[index].m_IsConnected = false; } void Class::DeviceInfo::DoState(PointerWrap& p) @@ -55,15 +72,10 @@ void Class::DoState(PointerWrap& p) void Init() { if (!g_Backend) - ResetBackend(); + g_Backend.reset(new BackendLocal()); g_Backend->StartGame(); } -void ResetBackend() -{ - g_Backend.reset(new BackendLocal()); -} - void DoState(PointerWrap& p) { for (int c = 0; c < Class::NumClasses; c++) diff --git a/Source/Core/Core/Src/IOSync.h b/Source/Core/Core/Src/IOSync.h index 0231a491a1b7..c7609d41ed9d 100644 --- a/Source/Core/Core/Src/IOSync.h +++ b/Source/Core/Core/Src/IOSync.h @@ -21,6 +21,7 @@ extern Class* g_Classes[]; class Backend { public: + Backend(); virtual void ConnectLocalDevice(int classId, int localIndex, PWBuffer&& buf) = 0; virtual void DisconnectLocalDevice(int classId, int localIndex) = 0; virtual void EnqueueLocalReport(int classId, int localIndex, PWBuffer&& buf) = 0; @@ -70,6 +71,8 @@ class Class void SetIndex(int index, int localIndex); + void ResetRemote(); + // Make a local device available. // subtypeData is data that does not change during the life of the device, // and is sent along with the connection notice. @@ -115,7 +118,7 @@ class Class return m_Remote[index].m_IsConnected; } - bool LocalIsConnected(int index) + const bool& LocalIsConnected(int index) { return m_Local[index].m_IsConnected; } @@ -184,7 +187,6 @@ class Class }; void Init(); -void ResetBackend(); void DoState(PointerWrap& p); } diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index f98b070ceb79..86051c7e133b 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -31,7 +31,7 @@ NetPlayClient::~NetPlayClient() if (m_net_host) m_net_host->Reset(); - IOSync::ResetBackend(); + IOSync::g_Backend.reset(); if (!m_direct_connection) ReleaseTraversalClient(); @@ -424,7 +424,7 @@ void NetPlayClient::OnConnectReady(ENetAddress addr) void NetPlayClient::OnConnectFailed(u8 reason) { m_state = Failure; - m_failure_reason = ServerError + reason; + m_failure_reason = TraversalClient::ConnectFailedError + reason; if (m_state_callback) m_state_callback(this); } diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 0d422e9b0c42..4b3d05ca6c6c 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -300,6 +300,7 @@ void NetPlayServer::OnDisconnect(PlayerId pid) return; player.connected = false; + m_num_players--; enet_peer_disconnect_later(&m_enet_host->peers[pid], 0); // The last reservation holdout might have disconnected. diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 16f91061a58a..888f808fa128 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -108,16 +108,15 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient std::vector> GetInterfaceListInternal(); NetSettings m_settings; - - bool m_is_running; - Common::Timer m_ping_timer; + bool m_is_running ACCESS_ON(NET); + Common::Timer m_ping_timer ACCESS_ON(NET); u32 m_ping_key; bool m_update_pings; u32 m_current_game; u32 m_target_buffer_size; - unsigned m_num_players; + unsigned m_num_players ACCESS_ON(NET); // only protects m_selected_game std::recursive_mutex m_crit; diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index a26974838d79..26c3a1c40a6c 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -157,7 +157,6 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const m_host_type_choice = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxSize(60, -1)); m_host_type_choice->Bind(wxEVT_COMMAND_CHOICE_SELECTED, &NetPlayDiag::OnChoice, this); m_host_type_choice->Append(_("ID:")); - m_host_type_choice->Select(0); host_szr->Add(m_host_type_choice); // The initial label is for sizing... m_host_label = new wxStaticText(panel, wxID_ANY, "555.555.555.555:55555", wxDefaultPosition, wxDefaultSize, wxST_NO_AUTORESIZE | wxALIGN_LEFT); @@ -169,6 +168,9 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const m_host_copy_btn->Disable(); host_szr->Add(m_host_copy_btn, 0, wxLEFT | wxCENTER, 5); player_szr->Add(host_szr, 0, wxEXPAND | wxBOTTOM, 5); + + m_host_type_choice->Select(0); + UpdateHostLabel(); } player_szr->Add(m_player_lbox, 1, wxEXPAND); From 30e3784148bf1861319e0111b34f2e0273e72026 Mon Sep 17 00:00:00 2001 From: comex Date: Wed, 27 Nov 2013 14:24:41 -0500 Subject: [PATCH 151/202] Better packet send synchronization. --- Source/Core/Common/Src/NetHost.cpp | 21 ++++++++++----------- Source/Core/Common/Src/NetHost.h | 7 ++++--- Source/Core/Core/Src/HW/SI.cpp | 1 - Source/Core/Core/Src/IOSyncBackends.cpp | 2 ++ Source/Core/Core/Src/NetPlayClient.cpp | 16 ++++++++++++++++ Source/Core/Core/Src/NetPlayClient.h | 2 ++ Source/Core/Core/Src/NetPlayServer.cpp | 12 +++++++++++- Source/Core/Core/Src/NetPlayServer.h | 3 ++- 8 files changed, 47 insertions(+), 17 deletions(-) diff --git a/Source/Core/Common/Src/NetHost.cpp b/Source/Core/Common/Src/NetHost.cpp index 606e71ecf769..16a49b01f01a 100644 --- a/Source/Core/Common/Src/NetHost.cpp +++ b/Source/Core/Common/Src/NetHost.cpp @@ -55,6 +55,7 @@ NetHost::NetHost(size_t peerCount, u16 port) m_GlobalSequenceNumber = 0; m_GlobalTicker = 0; m_TraversalClient = NULL; + m_AutoSend = true; ENetAddress addr = { ENET_HOST_ANY, port }; m_Host = enet_host_create( @@ -208,15 +209,6 @@ void NetHost::SendPacket(ENetPeer* peer, Packet&& packet) pi.m_SentPackets++; } -void NetHost::MaybeProcessPacketQueue() -{ - if (m_SendTimer.GetTimeDifference() > 6) - { - ProcessPacketQueue(); - m_SendTimer.Update(); - } -} - void NetHost::ProcessPacketQueue() { // The idea is that we send packets n-1 times unreliably and n times @@ -261,6 +253,7 @@ void NetHost::ProcessPacketQueue() } while (numToRemove--) m_OutgoingPacketInfo.pop_front(); + m_SendTimer.Update(); } void NetHost::PrintStats() @@ -321,7 +314,12 @@ void NetHost::ThreadFunc() PanicAlert("enet_socket_get_address failed."); continue; } - int count = enet_host_service(m_Host, &event, m_Host->connectedPeers > 0 ? 5 : 300); + u32 wait; + if (m_Host->connectedPeers > 0 || !m_AutoSend) + wait = 300; + else + wait = std::max((s64) 1, AutoSendDelay - (s64) m_SendTimer.GetTimeDifference()); + int count = enet_host_service(m_Host, &event, wait); if (count < 0) { PanicAlert("enet_host_service failed... do something about this."); @@ -358,7 +356,8 @@ void NetHost::ThreadFunc() m_Client->OnENetEvent(&event); } } - MaybeProcessPacketQueue(); + if (m_AutoSend && m_SendTimer.GetTimeDifference() >= AutoSendDelay) + ProcessPacketQueue(); } } diff --git a/Source/Core/Common/Src/NetHost.h b/Source/Core/Common/Src/NetHost.h index c1afc8737a54..f3b4fb364b66 100644 --- a/Source/Core/Common/Src/NetHost.h +++ b/Source/Core/Common/Src/NetHost.h @@ -79,7 +79,8 @@ class NetHost // peerCount peers; probably doesn't matter in practice, but it might // cause a problem if sending to thousands of clients were ever desired // (and "DolphinTV" would be nice to have!). - DefaultPeerCount = 50 + DefaultPeerCount = 50, + AutoSendDelay = 7 }; NetHost(size_t peerCount, u16 port); @@ -94,12 +95,14 @@ class NetHost void SendPacket(ENetPeer* peer, Packet&& packet) ON(NET); void PrintStats() ON(NET); u16 GetPort(); + void ProcessPacketQueue() ON(NET); NetHostClient* m_Client; // The traversal client needs to be on the same socket. TraversalClient* m_TraversalClient ACCESS_ON(NET); std::function m_InterceptCallback ACCESS_ON(NET); ENetHost* m_Host; + volatile bool m_AutoSend; private: struct OutgoingPacketInfo { @@ -127,8 +130,6 @@ class NetHost void ThreadFunc() /* ON(NET) */; void OnReceive(ENetEvent* event, Packet&& packet) ON(NET); - void MaybeProcessPacketQueue() ON(NET); - void ProcessPacketQueue() ON(NET); static int ENET_CALLBACK InterceptCallback(ENetHost* host, ENetEvent* event) /* ON(NET) */; Common::FifoQueue, false> m_RunQueue; diff --git a/Source/Core/Core/Src/HW/SI.cpp b/Source/Core/Core/Src/HW/SI.cpp index 465c8366bd43..d1387ff71b09 100644 --- a/Source/Core/Core/Src/HW/SI.cpp +++ b/Source/Core/Core/Src/HW/SI.cpp @@ -660,7 +660,6 @@ void UpdateDevices() // This doesn't really have to be synced with SI, but we do it here to // minimize the time before packets are sent out IOSync::g_Backend->NewLocalSubframe(); - } void RunSIBuffer() diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index d54f29977a46..2f6922d79c14 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -363,6 +363,8 @@ void BackendNetPlay::NewLocalSubframe() pac.W((MessageId) NP_MSG_RESERVATION_DONE); m_Client->SendPacket(std::move(pac)); } + + m_Client->ProcessPacketQueue(); } } // namespace diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 86051c7e133b..ec016085fd79 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -115,6 +115,15 @@ void NetPlayClient::OnPacketErrorFromIOSync() }); } +void NetPlayClient::ProcessPacketQueue() +{ + auto host = m_net_host; + host->RunOnThread([=] { + ASSUME_ON(NET); + host->ProcessPacketQueue(); + }); +} + void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) { if (m_state == WaitingForHelloResponse) @@ -316,6 +325,7 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) std::lock_guard lk(m_crit); m_net_host->BroadcastPacket(std::move(pong)); + m_net_host->ProcessPacketQueue(); } break; @@ -533,6 +543,10 @@ bool NetPlayClient::StartGame(const std::string &path) m_is_running = true; } + // Rely on SI to trigger sends - better to be synchronized with + // something than nothing. Might be better to synchronize everything. + m_net_host->m_AutoSend = false; + m_game_started_evt.Set(); return true; @@ -547,6 +561,8 @@ void NetPlayClient::GameStopped() { std::lock_guard lk(m_crit); + m_net_host->m_AutoSend = true; + g_is_running = false; m_is_running = false; diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 6bfa322ec5c8..7ca136711300 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -101,6 +101,8 @@ class NetPlayClient : public NetHostClient, public TraversalClientClient void SendPacket(Packet&& packet); void OnPacketErrorFromIOSync(); + void ProcessPacketQueue(); + std::function m_state_callback; PlayerId m_pid; protected: diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 4b3d05ca6c6c..cf0891475c84 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -39,6 +39,7 @@ NetPlayServer::NetPlayServer() { m_is_running = false; m_num_players = 0; + m_num_nonlocal_players = 0; m_dialog = NULL; m_highest_known_subframe = 0; m_target_buffer_size = 20; @@ -300,6 +301,8 @@ void NetPlayServer::OnDisconnect(PlayerId pid) return; player.connected = false; + if (!player.is_localhost) + m_num_nonlocal_players--; m_num_players--; enet_peer_disconnect_later(&m_enet_host->peers[pid], 0); @@ -370,11 +373,13 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) } else { - //player.is_localhost = peer->address.host == 0x0100007f; + player.is_localhost = peer->address.host == 0x0100007f; player.connected = true; // XXX allow connection during game player.sitting_out_this_game = false; m_num_players++; + if (!player.is_localhost) + m_num_nonlocal_players++; } return; } @@ -546,6 +551,7 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) opacket.W(player.ping); SendToClientsOnThread(std::move(opacket)); + m_net_host->ProcessPacketQueue(); } break; @@ -696,6 +702,10 @@ void NetPlayServer::SendToClients(Packet&& packet, const PlayerId skip_pid) void NetPlayServer::SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid) { m_net_host->BroadcastPacket(std::move(packet), skip_pid >= m_enet_host->peerCount ? NULL : &m_enet_host->peers[skip_pid]); + // If there's no more than one nonlocal player, no point trying to + // coalesce + if (m_num_players <= 1) + m_net_host->ProcessPacketQueue(); } void NetPlayServer::SetDialog(NetPlayUI* dialog) diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 888f808fa128..98baed452acf 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -66,7 +66,7 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient bool reservation_ok; bool sitting_out_this_game; // hit "stop" std::map devices_present; - //bool is_localhost; + bool is_localhost; }; std::vector m_players; @@ -117,6 +117,7 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient unsigned m_num_players ACCESS_ON(NET); + unsigned m_num_nonlocal_players ACCESS_ON(NET); // only protects m_selected_game std::recursive_mutex m_crit; From a244e1ff8a7944571531298e8bf71d9d4f3dd5bb Mon Sep 17 00:00:00 2001 From: comex Date: Wed, 27 Nov 2013 16:37:45 -0500 Subject: [PATCH 152/202] Fix game stop behavior. --- Source/Core/Core/Src/Core.cpp | 2 ++ Source/Core/Core/Src/IOSync.cpp | 5 +++++ Source/Core/Core/Src/IOSync.h | 2 ++ Source/Core/Core/Src/IOSyncBackends.cpp | 2 +- Source/Core/Core/Src/IOSyncBackends.h | 2 +- Source/Core/Core/Src/NetPlayClient.cpp | 2 -- 6 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/Src/Core.cpp b/Source/Core/Core/Src/Core.cpp index ea54163c301c..7aea0e7df883 100644 --- a/Source/Core/Core/Src/Core.cpp +++ b/Source/Core/Core/Src/Core.cpp @@ -239,6 +239,8 @@ void Stop() // - Hammertime! INFO_LOG(CONSOLE, "Stop [Main Thread]\t\t---- Shutting down ----"); + IOSync::Stop(); + // Stop the CPU INFO_LOG(CONSOLE, "%s", StopMessage(true, "Stop CPU").c_str()); PowerPC::Stop(); diff --git a/Source/Core/Core/Src/IOSync.cpp b/Source/Core/Core/Src/IOSync.cpp index 84edee6b7d33..68bcb7e4bf8d 100644 --- a/Source/Core/Core/Src/IOSync.cpp +++ b/Source/Core/Core/Src/IOSync.cpp @@ -86,6 +86,11 @@ void DoState(PointerWrap& p) g_Backend->DoState(p); } +void Stop() +{ + g_Backend->StopGame(); +} + std::unique_ptr g_Backend; Class* g_Classes[Class::NumClasses]; diff --git a/Source/Core/Core/Src/IOSync.h b/Source/Core/Core/Src/IOSync.h index c7609d41ed9d..68c5528c660d 100644 --- a/Source/Core/Core/Src/IOSync.h +++ b/Source/Core/Core/Src/IOSync.h @@ -30,6 +30,7 @@ class Backend virtual u32 GetTime() = 0; virtual void DoState(PointerWrap& p) = 0; virtual void StartGame() {} + virtual void StopGame() {} virtual void NewLocalSubframe() {} }; @@ -188,6 +189,7 @@ class Class void Init(); void DoState(PointerWrap& p); +void Stop(); } diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index 2f6922d79c14..0f2aa1a68d93 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -218,7 +218,7 @@ void BackendNetPlay::OnPacketReceived(Packet&& packet) m_PacketsPendingProcessing.Push(std::move(packet)); } -void BackendNetPlay::Abort() +void BackendNetPlay::StopGame() { m_Abort = true; } diff --git a/Source/Core/Core/Src/IOSyncBackends.h b/Source/Core/Core/Src/IOSyncBackends.h index 40524ff04ecd..266b94643eed 100644 --- a/Source/Core/Core/Src/IOSyncBackends.h +++ b/Source/Core/Core/Src/IOSyncBackends.h @@ -40,11 +40,11 @@ class BackendNetPlay : public Backend virtual u32 GetTime() override; virtual void DoState(PointerWrap& p) override; virtual void StartGame() override; + virtual void StopGame() override; // from netplay void PreInitDevices() ON(NET); void OnPacketReceived(Packet&& packet) ON(NET); - void Abort() ON(NET); // from (arbitrarily-ish) SI virtual void NewLocalSubframe() override; private: diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index ec016085fd79..6fa1a85b1ee9 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -304,8 +304,6 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) case NP_MSG_STOP_GAME : { m_received_stop_request = true; - if (m_backend) - m_backend->Abort(); m_dialog->OnMsgStopGame(); } break; From e82808a5154f4d349eea25b7a3c22dbd03d271c3 Mon Sep 17 00:00:00 2001 From: comex Date: Wed, 27 Nov 2013 21:55:03 -0500 Subject: [PATCH 153/202] Fix IsNetPlayRunning at boot time. --- Source/Core/Core/Src/NetPlayClient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 6fa1a85b1ee9..14e224c05fc6 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -532,12 +532,12 @@ bool NetPlayClient::StartGame(const std::string &path) m_dialog->AppendChat(" -- STARTING GAME -- "); // boot game + g_is_running = true; m_dialog->BootGame(path); if (Core::IsRunningAndStarted()) { - g_is_running = true; m_is_running = true; } From 73c7f5dc0f1ebd93b84e16a07901b118883a5e57 Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 28 Nov 2013 15:23:29 -0500 Subject: [PATCH 154/202] Add a notice for who's lagging. --- Source/Core/Core/Src/IOSyncBackends.cpp | 5 +++ Source/Core/Core/Src/IOSyncBackends.h | 1 + Source/Core/Core/Src/NetPlayClient.cpp | 41 +++++++++++++++++++++++++ Source/Core/Core/Src/NetPlayClient.h | 13 +++++--- Source/Core/DolphinWX/Src/NetWindow.cpp | 40 ++++++++++++++++++++++-- Source/Core/DolphinWX/Src/NetWindow.h | 10 +++++- 6 files changed, 103 insertions(+), 7 deletions(-) diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index 0f2aa1a68d93..8b1c324d44c0 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -142,6 +142,7 @@ Packet BackendNetPlay::DequeueReport(int classId, int index, bool* keepGoing) { auto& deviceInfo = m_DeviceInfo[classId][index]; const bool& isConnected = g_Classes[classId]->IsConnected(index); + bool alreadyProcessed = false; while (1) { //printf("dev=%llu past=%llu\n", deviceInfo.m_SubframeId, m_PastSubframeId); @@ -185,7 +186,10 @@ Packet BackendNetPlay::DequeueReport(int classId, int index, bool* keepGoing) return q; } } + if (alreadyProcessed) + m_Client->WarnLagging(deviceInfo.m_OwnerId); ProcessIncomingPackets(); + alreadyProcessed = true; } } @@ -273,6 +277,7 @@ void BackendNetPlay::ProcessPacket(Packet&& p) DoDisconnect(classId, index); auto& di = m_DeviceInfo[classId][index]; di.m_SubframeId = di.m_LastSentSubframeId = m_PastSubframeId; + di.m_OwnerId = localPlayer; int myLocalIndex = localPlayer == m_Client->m_pid ? localIndex : -1; g_Classes[classId]->OnConnected(index, myLocalIndex, std::move(subtype)); break; diff --git a/Source/Core/Core/Src/IOSyncBackends.h b/Source/Core/Core/Src/IOSyncBackends.h index 266b94643eed..4a0c789fab9f 100644 --- a/Source/Core/Core/Src/IOSyncBackends.h +++ b/Source/Core/Core/Src/IOSyncBackends.h @@ -63,6 +63,7 @@ class BackendNetPlay : public Backend std::deque m_IncomingQueue; s64 m_SubframeId; s64 m_LastSentSubframeId; + PlayerId m_OwnerId; }; void ProcessIncomingPackets(); diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 14e224c05fc6..1360fbbb4ca3 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -115,6 +115,47 @@ void NetPlayClient::OnPacketErrorFromIOSync() }); } +void NetPlayClient::WarnLagging(PlayerId pid) +{ + bool was_lagging; + { + std::lock_guard lk(m_crit); + auto& player = m_players[pid]; + was_lagging = player.lagging; + player.lagging = true; + player.lagging_at = Common::Timer::GetTimeMs(); + } + if (!was_lagging && m_dialog) + m_dialog->UpdateLagWarning(); +} + +std::pair NetPlayClient::GetLaggardNamesAndTimer() +{ + std::lock_guard lk(m_crit); + u32 time = Common::Timer::GetTimeMs(); + bool first = true; + std::stringstream ss; + u32 min_diff = -1u; + for (auto& p : m_players) + { + auto& player = p.second; + if (!player.lagging) + continue; + u32 diff = player.lagging_at - time; + if (diff >= 1000) + { + player.lagging = false; + continue; + } + min_diff = std::min(min_diff, 1000 - diff); + if (!first) + ss << ", "; + first = false; + ss << player.name; + } + return std::make_pair(ss.str(), min_diff + 50); +} + void NetPlayClient::ProcessPacketQueue() { auto host = m_net_host; diff --git a/Source/Core/Core/Src/NetPlayClient.h b/Source/Core/Core/Src/NetPlayClient.h index 7ca136711300..716f7d544a2f 100644 --- a/Source/Core/Core/Src/NetPlayClient.h +++ b/Source/Core/Core/Src/NetPlayClient.h @@ -43,15 +43,18 @@ class NetPlayUI virtual void OnMsgStopGame() = 0; virtual void UpdateDevices() = 0; virtual bool IsRecording() = 0; + virtual void UpdateLagWarning() = 0; }; class Player { public: - PlayerId pid; - std::string name; - std::string revision; - u32 ping; + PlayerId pid; + std::string name; + std::string revision; + u32 ping; + bool lagging; + u32 lagging_at; }; class NetPlayClient : public NetHostClient, public TraversalClientClient @@ -100,6 +103,8 @@ class NetPlayClient : public NetHostClient, public TraversalClientClient void SendPacket(Packet&& packet); void OnPacketErrorFromIOSync(); + void WarnLagging(PlayerId pid) /* ON(CPU) */; + std::pair GetLaggardNamesAndTimer() /* ON(GUI) */; void ProcessPacketQueue(); diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 26c3a1c40a6c..344940d13e47 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -114,9 +114,16 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const npd = this; wxPanel* const panel = new wxPanel(this); m_device_map_diag = NULL; + m_lag_timer.Bind(wxEVT_TIMER, &NetPlayDiag::LagWarningTimerHit, this); // top crap - m_game_label = new wxStaticText(panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize); + m_top_szr = new wxBoxSizer(wxHORIZONTAL); + m_game_label = new wxStaticText(panel, wxID_ANY, ""); + m_top_szr->Add(m_game_label, 0, wxEXPAND); + m_warn_label = new wxStaticText(panel, wxID_ANY, ""); + m_warn_label->SetForegroundColour(*wxRED); + m_top_szr->AddStretchSpacer(); + m_top_szr->Add(m_warn_label, 0, wxEXPAND | wxRIGHT, 3); UpdateGameName(); // middle crap @@ -215,7 +222,7 @@ NetPlayDiag::NetPlayDiag(wxWindow* const parent, const std::string& game, const // main sizer wxBoxSizer* const main_szr = new wxBoxSizer(wxVERTICAL); - main_szr->Add(m_game_label, 0, wxEXPAND | wxALL, 5); + main_szr->Add(m_top_szr, 0, wxEXPAND | wxALL, 5); main_szr->Add(mid_szr, 1, wxEXPAND | wxLEFT | wxRIGHT, 5); main_szr->Add(bottom_szr, 0, wxEXPAND | wxALL, 5); @@ -270,6 +277,30 @@ void NetPlayDiag::UpdateGameName() } +void NetPlayDiag::UpdateLagWarning() +{ + wxCommandEvent evt(wxEVT_THREAD, NP_GUI_EVT_WARN_LAGGING); + GetEventHandler()->AddPendingEvent(evt); +} + +void NetPlayDiag::DoUpdateLagWarning() +{ + auto p = netplay_client->GetLaggardNamesAndTimer(); + wxString label = ""; + if (!p.first.empty()) + { + label = _("Waiting for: ") + WxStrToStr(p.first); + m_lag_timer.StartOnce(p.second); + } + m_warn_label->SetLabel(label); + m_top_szr->Layout(); +} + +void NetPlayDiag::LagWarningTimerHit(wxTimerEvent&) +{ + DoUpdateLagWarning(); +} + NetPlayDiag::~NetPlayDiag() { if (m_device_map_diag) @@ -525,6 +556,11 @@ void NetPlayDiag::OnThread(wxCommandEvent& event) }); } break; + case NP_GUI_EVT_WARN_LAGGING: + { + DoUpdateLagWarning(); + } + break; } // chat messages diff --git a/Source/Core/DolphinWX/Src/NetWindow.h b/Source/Core/DolphinWX/Src/NetWindow.h index 55b2d43665aa..c8749e517563 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.h +++ b/Source/Core/DolphinWX/Src/NetWindow.h @@ -34,7 +34,8 @@ enum NP_GUI_EVT_START_GAME, NP_GUI_EVT_STOP_GAME, NP_GUI_EVT_FAILURE, - NP_GUI_EVT_UPDATE_DEVICES + NP_GUI_EVT_UPDATE_DEVICES, + NP_GUI_EVT_WARN_LAGGING }; class DeviceMapDiag; @@ -68,6 +69,7 @@ class NetPlayDiag : public wxFrame, public NetPlayUI static const GameListItem* FindISO(const std::string& id); void UpdateGameName(); + virtual void UpdateLagWarning() override; private: DECLARE_EVENT_TABLE() @@ -85,6 +87,9 @@ class NetPlayDiag : public wxFrame, public NetPlayUI void GetNetSettings(NetSettings &settings); void OnErrorClosed(wxCommandEvent& event); + void DoUpdateLagWarning(); + void LagWarningTimerHit(wxTimerEvent& event); + wxTextCtrl* m_name_text; wxListBox* m_player_lbox; wxTextCtrl* m_chat_text; @@ -98,9 +103,12 @@ class NetPlayDiag : public wxFrame, public NetPlayUI std::string m_selected_game; wxStaticText* m_game_label; + wxBoxSizer* m_top_szr; + wxStaticText* m_warn_label; wxButton* m_start_btn; bool m_is_hosting; DeviceMapDiag* m_device_map_diag; + wxTimer m_lag_timer; std::vector m_playerids; From 32d8bd6183ae06bb6ea53bbaa72cd2cf9e789f72 Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 28 Nov 2013 15:30:17 -0500 Subject: [PATCH 155/202] More fixes. --- Source/Core/Common/Src/ChunkFile.h | 7 ++++++ Source/Core/Common/Src/NetHost.cpp | 27 ++++++++++++++++---- Source/Core/Common/Src/NetHost.h | 8 ++++-- Source/Core/Core/Src/HW/HW.cpp | 2 ++ Source/Core/Core/Src/IOSync.cpp | 4 +++ Source/Core/Core/Src/IOSync.h | 3 +++ Source/Core/Core/Src/IOSyncBackends.cpp | 28 ++++++++++----------- Source/Core/Core/Src/IOSyncBackends.h | 6 ++--- Source/Core/Core/Src/NetPlayClient.cpp | 3 +++ Source/Core/Core/Src/NetPlayServer.cpp | 33 +++++++++++++------------ Source/Core/Core/Src/NetPlayServer.h | 2 +- Source/Core/DolphinWX/Src/NetWindow.cpp | 6 +++-- 12 files changed, 86 insertions(+), 43 deletions(-) diff --git a/Source/Core/Common/Src/ChunkFile.h b/Source/Core/Common/Src/ChunkFile.h index a58087ec5caa..8bce23e8883d 100644 --- a/Source/Core/Common/Src/ChunkFile.h +++ b/Source/Core/Common/Src/ChunkFile.h @@ -265,7 +265,14 @@ class PointerWrap u32 size = (u32)x.size(); Do(size); if (mode == MODE_READ) + { + if (size >= 1000000) + { + failure = true; + return; + } x.resize(size); + } for (auto& elem : x) Do(elem); diff --git a/Source/Core/Common/Src/NetHost.cpp b/Source/Core/Common/Src/NetHost.cpp index 16a49b01f01a..9a264164ab51 100644 --- a/Source/Core/Common/Src/NetHost.cpp +++ b/Source/Core/Common/Src/NetHost.cpp @@ -142,7 +142,7 @@ void NetHost::Reset() { ENetPeer* peer = &m_Host->peers[i]; if (peer->state != ENET_PEER_STATE_DISCONNECTED) - enet_peer_disconnect_later(peer, 0); + enet_peer_disconnect_now(peer, 0); } }); m_Client = NULL; @@ -157,7 +157,7 @@ void NetHost::BroadcastPacket(Packet&& packet, ENetPeer* except) size_t peer = 0; for (auto& pi : m_PeerInfo) { - if (&m_Host->peers[peer++] == except) + if (!pi.m_Connected || &m_Host->peers[peer++] == except) continue; pi.m_GlobalSeqToSeq[seq] = pi.m_OutgoingSequenceNumber++; } @@ -192,7 +192,8 @@ void NetHost::BroadcastPacket(Packet&& packet, ENetPeer* except) } u16* oseqp = (u16 *) epacket->data; *oseqp = seq; - enet_peer_send(peer, 0, epacket); + if (enet_peer_send(peer, 0, epacket) < 0) + ERROR_LOG(NETPLAY, "enet_peer_send failed"); } if (epacket && epacket->referenceCount == 0) enet_packet_destroy(epacket); @@ -345,15 +346,25 @@ void NetHost::ThreadFunc() if (pid >= m_PeerInfo.size()) m_PeerInfo.resize(pid + 1); auto& pi = m_PeerInfo[pid]; - pi.m_IncomingPackets.clear(); pi.m_IncomingSequenceNumber = 0; pi.m_OutgoingSequenceNumber = 0; pi.m_ConnectTicker = m_GlobalTicker++; pi.m_SentPackets = 0; + m_Client->OnENetEvent(&event); + break; + } + case ENET_EVENT_TYPE_DISCONNECT: + { + size_t pid = event.peer - m_Host->peers; + auto& pi = m_PeerInfo[pid]; + pi.m_Connected = false; + pi.m_IncomingPackets.clear(); + m_Client->OnENetEvent(&event); } - /* fall through */ + break; default: m_Client->OnENetEvent(&event); + break; } } if (m_AutoSend && m_SendTimer.GetTimeDifference() >= AutoSendDelay) @@ -361,6 +372,11 @@ void NetHost::ThreadFunc() } } +void NetHost::MarkConnected(size_t pid) +{ + m_PeerInfo[pid].m_Connected = true; +} + void NetHost::OnReceive(ENetEvent* event, Packet&& packet) { auto& pi = m_PeerInfo[event->peer - m_Host->peers]; @@ -415,6 +431,7 @@ void NetHost::OnReceive(ENetEvent* event, Packet&& packet) { // strange WARN_LOG(NETPLAY, "Failure splitting packet - truncation?"); + return; } } diff --git a/Source/Core/Common/Src/NetHost.h b/Source/Core/Common/Src/NetHost.h index f3b4fb364b66..d8378816cb7d 100644 --- a/Source/Core/Common/Src/NetHost.h +++ b/Source/Core/Common/Src/NetHost.h @@ -13,7 +13,7 @@ DEFINE_THREAD_HAT(NET); -/* +#if 0 static inline void DumpBuf(PWBuffer& buf) { printf("+00:"); @@ -26,7 +26,7 @@ static inline void DumpBuf(PWBuffer& buf) } printf("\n"); } -*/ +#endif // Some trivial utilities that should be moved: @@ -96,6 +96,8 @@ class NetHost void PrintStats() ON(NET); u16 GetPort(); void ProcessPacketQueue() ON(NET); + // This pid will be a target for broadcasts + void MarkConnected(size_t pid) ON(NET); NetHostClient* m_Client; // The traversal client needs to be on the same socket. @@ -119,6 +121,7 @@ class NetHost struct PeerInfo { + PeerInfo() { m_Connected = false; } std::deque m_IncomingPackets; // the sequence number of the first element of m_IncomingPackets u16 m_IncomingSequenceNumber; @@ -126,6 +129,7 @@ class NetHost u16 m_GlobalSeqToSeq[65536]; u64 m_ConnectTicker; int m_SentPackets; + bool m_Connected; }; void ThreadFunc() /* ON(NET) */; diff --git a/Source/Core/Core/Src/HW/HW.cpp b/Source/Core/Core/Src/HW/HW.cpp index e23588eddfb9..5430b6002b24 100644 --- a/Source/Core/Core/Src/HW/HW.cpp +++ b/Source/Core/Core/Src/HW/HW.cpp @@ -52,6 +52,8 @@ namespace HW WII_IPCInterface::Init(); WII_IPC_HLE_Interface::Init(); } + + IOSync::PostInit(); } void Shutdown() diff --git a/Source/Core/Core/Src/IOSync.cpp b/Source/Core/Core/Src/IOSync.cpp index 68bcb7e4bf8d..014f69413040 100644 --- a/Source/Core/Core/Src/IOSync.cpp +++ b/Source/Core/Core/Src/IOSync.cpp @@ -73,6 +73,10 @@ void Init() { if (!g_Backend) g_Backend.reset(new BackendLocal()); +} + +void PostInit() +{ g_Backend->StartGame(); } diff --git a/Source/Core/Core/Src/IOSync.h b/Source/Core/Core/Src/IOSync.h index 68c5528c660d..ccd9f702ec26 100644 --- a/Source/Core/Core/Src/IOSync.h +++ b/Source/Core/Core/Src/IOSync.h @@ -187,7 +187,10 @@ class Class int m_ClassId; }; +// Called before the devices are set up, to init the backend. void Init(); +// Called after, to connect devices. +void PostInit(); void DoState(PointerWrap& p); void Stop(); diff --git a/Source/Core/Core/Src/IOSyncBackends.cpp b/Source/Core/Core/Src/IOSyncBackends.cpp index 8b1c324d44c0..c772b8d60e5b 100644 --- a/Source/Core/Core/Src/IOSyncBackends.cpp +++ b/Source/Core/Core/Src/IOSyncBackends.cpp @@ -129,8 +129,8 @@ void BackendNetPlay::EnqueueLocalReport(int classId, int localIndex, PWBuffer&& pac.W((u8) classId); pac.W((u8) ri); auto& last = m_DeviceInfo[classId][ri].m_LastSentSubframeId; - u16 skippedFrames = m_SubframeId - last; - last = m_SubframeId; + s16 skippedFrames = m_FutureSubframeId - last; + last = m_FutureSubframeId; pac.W(skippedFrames); pac.vec->append(buf); // server won't send our own reports back to us @@ -145,8 +145,8 @@ Packet BackendNetPlay::DequeueReport(int classId, int index, bool* keepGoing) bool alreadyProcessed = false; while (1) { - //printf("dev=%llu past=%llu\n", deviceInfo.m_SubframeId, m_PastSubframeId); - if (!isConnected || m_Abort || deviceInfo.m_SubframeId > m_PastSubframeId) + //printf("dev=%llu past=%llu\n", deviceInfo.m_SubframeId, m_SubframeId); + if (!isConnected || m_Abort || deviceInfo.m_SubframeId > m_SubframeId) { *keepGoing = false; return PWBuffer(); @@ -171,16 +171,16 @@ Packet BackendNetPlay::DequeueReport(int classId, int index, bool* keepGoing) } else { - u16 skippedFrames = flags; - if (deviceInfo.m_SubframeId + skippedFrames > m_PastSubframeId) + s16 skippedFrames = flags; + if (deviceInfo.m_SubframeId + skippedFrames > m_SubframeId) { p.readOff -= 5; *keepGoing = false; return PWBuffer(); } deviceInfo.m_SubframeId += skippedFrames; - //printf("--> dev=%llu past=%llu ql=%zd\n", deviceInfo.m_SubframeId, m_PastSubframeId, queue.size()); - *keepGoing = deviceInfo.m_SubframeId < m_PastSubframeId; + //printf("--> dev=%llu past=%llu ql=%zd\n", deviceInfo.m_SubframeId, m_SubframeId, queue.size()); + *keepGoing = deviceInfo.m_SubframeId < m_SubframeId; Packet q = std::move(p); queue.pop_front(); return q; @@ -276,7 +276,7 @@ void BackendNetPlay::ProcessPacket(Packet&& p) // The disconnect might be queued. DoDisconnect(classId, index); auto& di = m_DeviceInfo[classId][index]; - di.m_SubframeId = di.m_LastSentSubframeId = m_PastSubframeId; + di.m_SubframeId = di.m_LastSentSubframeId = m_SubframeId; di.m_OwnerId = localPlayer; int myLocalIndex = localPlayer == m_Client->m_pid ? localIndex : -1; g_Classes[classId]->OnConnected(index, myLocalIndex, std::move(subtype)); @@ -308,8 +308,8 @@ void BackendNetPlay::ProcessPacket(Packet&& p) p.Do(subframe); if (p.failure) goto failure; - WARN_LOG(NETPLAY, "Client: reservation request for subframe %lld; current %lld (%s)", (long long) subframe, (long long) m_PastSubframeId, subframe > m_PastSubframeId ? "ok" : "too late"); - if (subframe > m_PastSubframeId) + WARN_LOG(NETPLAY, "Client: reservation request for subframe %lld; current %lld (%s)", (long long) subframe, (long long) m_SubframeId, subframe > m_SubframeId ? "ok" : "too late"); + if (subframe > m_SubframeId) { m_ReservedSubframeId = subframe; m_HaveClearReservationPacket = false; @@ -317,7 +317,7 @@ void BackendNetPlay::ProcessPacket(Packet&& p) Packet pac; pac.W((MessageId) NP_MSG_RESERVATION_RESULT); pac.W(subframe); - pac.W(m_PastSubframeId); + pac.W(m_SubframeId); m_Client->SendPacket(std::move(pac)); } break; @@ -339,12 +339,12 @@ void BackendNetPlay::ProcessPacket(Packet&& p) void BackendNetPlay::NewLocalSubframe() { m_SubframeId++; - m_PastSubframeId = m_SubframeId - m_Delay; + m_FutureSubframeId = m_SubframeId + m_Delay; // If we have nothing connected, we need to process the queue here. ProcessIncomingPackets(); - if (m_PastSubframeId == m_ReservedSubframeId) + if (m_SubframeId == m_ReservedSubframeId) { if (!m_HaveClearReservationPacket) WARN_LOG(NETPLAY, "Client: blocking on reserved subframe %lld (bad estimate)", (long long) m_ReservedSubframeId); diff --git a/Source/Core/Core/Src/IOSyncBackends.h b/Source/Core/Core/Src/IOSyncBackends.h index 4a0c789fab9f..f8b1f68ce933 100644 --- a/Source/Core/Core/Src/IOSyncBackends.h +++ b/Source/Core/Core/Src/IOSyncBackends.h @@ -72,10 +72,10 @@ class BackendNetPlay : public Backend NetPlayClient* m_Client; Common::FifoQueue m_PacketsPendingProcessing; + // The subframe we're sending packets for. + s64 m_FutureSubframeId; + // The subframe emulation is currently on. s64 m_SubframeId; - // We accept packets sent before this frame. i.e. this is the subframe - // represented in emulation. - s64 m_PastSubframeId; // We will wait for this frame. s64 m_ReservedSubframeId; Packet m_ClearReservationPacket; diff --git a/Source/Core/Core/Src/NetPlayClient.cpp b/Source/Core/Core/Src/NetPlayClient.cpp index 1360fbbb4ca3..07f45ba8c9ce 100644 --- a/Source/Core/Core/Src/NetPlayClient.cpp +++ b/Source/Core/Core/Src/NetPlayClient.cpp @@ -172,6 +172,7 @@ void NetPlayClient::OnData(ENetEvent* event, Packet&& packet) std::lock_guard lk(m_crit); MessageId server_error; + packet.Do(server_error); packet.Do(m_pid); @@ -424,6 +425,7 @@ void NetPlayClient::OnENetEvent(ENetEvent* event) { if (m_state != Connecting) break; + m_net_host->MarkConnected(event->peer - event->peer->host->peers); // send connect message Packet hello; hello.W(std::string(NETPLAY_VERSION)); @@ -436,6 +438,7 @@ void NetPlayClient::OnENetEvent(ENetEvent* event) break; } case ENET_EVENT_TYPE_DISCONNECT: + WARN_LOG(NETPLAY, "EVENT_TYPE_DISCONNECT"); OnDisconnect(ReceivedENetDisconnect); break; default: diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index cf0891475c84..7ab5aa888a18 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -295,12 +295,16 @@ MessageId NetPlayServer::OnConnect(PlayerId pid, Packet& hello) void NetPlayServer::OnDisconnect(PlayerId pid) { + WARN_LOG(NETPLAY, "Disconnecting player %d", pid); Client& player = m_players[pid]; if (!player.connected) return; player.connected = false; + player.devices_present.clear(); + player.name = ""; + player.revision = ""; if (!player.is_localhost) m_num_nonlocal_players--; m_num_players--; @@ -381,6 +385,7 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) if (!player.is_localhost) m_num_nonlocal_players++; } + m_net_host->MarkConnected(pid); return; } @@ -583,6 +588,17 @@ void NetPlayServer::OnData(ENetEvent* event, Packet&& packet) m_is_running = false; m_reservation_state = Inactive; + + bool change = false; + for (auto& oplayer : m_players) + { + if (!player.connected) + break; + change = change || oplayer.sitting_out_this_game; + oplayer.sitting_out_this_game = false; + } + if (change && m_dialog) + m_dialog->UpdateDevices(); } } break; @@ -671,17 +687,6 @@ void NetPlayServer::StartGame(const std::string &path) SendToClientsOnThread(std::move(opacket)); - bool change = false; - for (auto& player : m_players) - { - if (!player.connected) - break; - change = change || player.sitting_out_this_game; - player.sitting_out_this_game = false; - } - if (change && m_dialog) - m_dialog->UpdateDevices(); - m_reserved_subframe = 0; ExecuteReservation(); @@ -728,10 +733,6 @@ void NetPlayServer::SetDesiredDeviceMapping(int classId, int index, PlayerId pid if (i != index && dis[i].desired_mapping == new_mapping) { SetDesiredDeviceMapping(classId, i, 255, 255); - // ??? This should not be necessary (definitely being run on - // the right thread, not reentrantly, all that), but it seems - // to be. - Common::SleepCurrentThread(50); if (m_dialog) m_dialog->UpdateDevices(); break; @@ -926,7 +927,7 @@ void NetPlayServer::ForceDisconnectDevice(int classId, int index) packet.W((MessageId)NP_MSG_DISCONNECT_DEVICE); packet.W((u8)classId); packet.W((u8)index); - packet.W((u8)0); + packet.W((u16)0); SendToClientsOnThread(std::move(packet)); auto& di = m_device_info[classId][index]; di.desired_mapping = di.actual_mapping = di.new_mapping = DeviceInfo::null_mapping; diff --git a/Source/Core/Core/Src/NetPlayServer.h b/Source/Core/Core/Src/NetPlayServer.h index 98baed452acf..4808f9d1cbd9 100644 --- a/Source/Core/Core/Src/NetPlayServer.h +++ b/Source/Core/Core/Src/NetPlayServer.h @@ -69,7 +69,7 @@ class NetPlayServer : public NetHostClient, public TraversalClientClient bool is_localhost; }; - std::vector m_players; + std::vector m_players ACCESS_ON(NET); struct DeviceInfo { diff --git a/Source/Core/DolphinWX/Src/NetWindow.cpp b/Source/Core/DolphinWX/Src/NetWindow.cpp index 344940d13e47..2707c2cab530 100644 --- a/Source/Core/DolphinWX/Src/NetWindow.cpp +++ b/Source/Core/DolphinWX/Src/NetWindow.cpp @@ -75,7 +75,7 @@ static wxString FailureReasonStringForDialog(int reason) case NetPlayClient::ReceivedENetDisconnect: return _("Disconnected"); default: - return _("Unknown error"); + return wxString::Format(_("Unknown error %x"), reason); } } @@ -752,7 +752,9 @@ DeviceMapDiag::DeviceMapDiag(wxWindow* parent, NetPlayServer* server) void DeviceMapDiag::UpdateDeviceMap() { - DestroyChildren(); + // It's unsafe to use DestroyChildren here! + for (auto& child : GetChildren()) + child->Destroy(); m_choice_to_cls_idx.clear(); auto main_szr = new wxBoxSizer(wxVERTICAL); From 1827e58e3b944307877bc51dfbc00a290ea79f98 Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 28 Nov 2013 21:48:57 -0500 Subject: [PATCH 156/202] Don't bother with the timer; TCP sends several times as many packets anyway. --- Source/Core/Common/Src/NetHost.cpp | 12 +++--------- Source/Core/Common/Src/NetHost.h | 1 - Source/Core/Core/Src/NetPlayServer.cpp | 4 ---- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/Source/Core/Common/Src/NetHost.cpp b/Source/Core/Common/Src/NetHost.cpp index 9a264164ab51..a43d8ce98061 100644 --- a/Source/Core/Common/Src/NetHost.cpp +++ b/Source/Core/Common/Src/NetHost.cpp @@ -254,7 +254,6 @@ void NetHost::ProcessPacketQueue() } while (numToRemove--) m_OutgoingPacketInfo.pop_front(); - m_SendTimer.Update(); } void NetHost::PrintStats() @@ -302,6 +301,8 @@ void NetHost::ThreadFunc() Common::SetCurrentThreadName(m_TraversalClient ? "TraversalClient thread" : "NetHost thread"); while (1) { + if (m_AutoSend) + ProcessPacketQueue(); while (!m_RunQueue.Empty()) { m_RunQueue.Front()(); @@ -315,12 +316,7 @@ void NetHost::ThreadFunc() PanicAlert("enet_socket_get_address failed."); continue; } - u32 wait; - if (m_Host->connectedPeers > 0 || !m_AutoSend) - wait = 300; - else - wait = std::max((s64) 1, AutoSendDelay - (s64) m_SendTimer.GetTimeDifference()); - int count = enet_host_service(m_Host, &event, wait); + int count = enet_host_service(m_Host, &event, 4); if (count < 0) { PanicAlert("enet_host_service failed... do something about this."); @@ -367,8 +363,6 @@ void NetHost::ThreadFunc() break; } } - if (m_AutoSend && m_SendTimer.GetTimeDifference() >= AutoSendDelay) - ProcessPacketQueue(); } } diff --git a/Source/Core/Common/Src/NetHost.h b/Source/Core/Common/Src/NetHost.h index d8378816cb7d..17d344ba161d 100644 --- a/Source/Core/Common/Src/NetHost.h +++ b/Source/Core/Common/Src/NetHost.h @@ -142,7 +142,6 @@ class NetHost bool m_ShouldEndThread ACCESS_ON(NET); std::deque m_OutgoingPacketInfo ACCESS_ON(NET); - Common::Timer m_SendTimer ACCESS_ON(NET); Common::Timer m_StatsTimer ACCESS_ON(NET); std::vector m_PeerInfo ACCESS_ON(NET); u16 m_GlobalSequenceNumber ACCESS_ON(NET); diff --git a/Source/Core/Core/Src/NetPlayServer.cpp b/Source/Core/Core/Src/NetPlayServer.cpp index 7ab5aa888a18..fc54afe38ecc 100644 --- a/Source/Core/Core/Src/NetPlayServer.cpp +++ b/Source/Core/Core/Src/NetPlayServer.cpp @@ -707,10 +707,6 @@ void NetPlayServer::SendToClients(Packet&& packet, const PlayerId skip_pid) void NetPlayServer::SendToClientsOnThread(Packet&& packet, const PlayerId skip_pid) { m_net_host->BroadcastPacket(std::move(packet), skip_pid >= m_enet_host->peerCount ? NULL : &m_enet_host->peers[skip_pid]); - // If there's no more than one nonlocal player, no point trying to - // coalesce - if (m_num_players <= 1) - m_net_host->ProcessPacketQueue(); } void NetPlayServer::SetDialog(NetPlayUI* dialog) From 69137cff4c58fbdfcb2b67ad51d2d68cc8ca0cbc Mon Sep 17 00:00:00 2001 From: degasus Date: Fri, 29 Nov 2013 06:09:54 +0100 Subject: [PATCH 157/202] Merge X11+D3D FreeLook feature into DolphinWX This removes the redundant code and also implements this feature for OSX and Wayland. But so it's dropped for non-wx builds... imo DolphinWX still isn't the best place for this, but now it's in the same file as all other hotkeys. Maybe they'll be moved to InputCommon sometimes at once ... --- Source/Core/DolphinWX/Src/Frame.cpp | 53 +++++++++++++++++ .../DolphinWX/Src/GLInterface/X11_Util.cpp | 57 ------------------- Source/Core/VideoCommon/Src/EmuWindow.cpp | 53 ----------------- 3 files changed, 53 insertions(+), 110 deletions(-) diff --git a/Source/Core/DolphinWX/Src/Frame.cpp b/Source/Core/DolphinWX/Src/Frame.cpp index 1ca16ff1ac83..5a255567d5e3 100644 --- a/Source/Core/DolphinWX/Src/Frame.cpp +++ b/Source/Core/DolphinWX/Src/Frame.cpp @@ -1028,6 +1028,59 @@ void CFrame::OnMouse(wxMouseEvent& event) event.GetPosition().x, event.GetPosition().y, event.ButtonDown()); } #endif + + // next handlers are all for FreeLook, so we don't need to check them if disabled + if(!g_Config.bFreeLook) + { + event.Skip(); + return; + } + + // Free look variables + static bool mouseLookEnabled = false; + static bool mouseMoveEnabled = false; + static float lastMouse[2]; + + if(event.MiddleDown()) + { + lastMouse[0] = event.GetX(); + lastMouse[1] = event.GetY(); + mouseMoveEnabled = true; + } + else if(event.RightDown()) + { + lastMouse[0] = event.GetX(); + lastMouse[1] = event.GetY(); + mouseLookEnabled = true; + } + else if(event.MiddleUp()) + { + mouseMoveEnabled = false; + } + else if(event.RightUp()) + { + mouseLookEnabled = false; + } + // no button, so it's a move event + else if(event.GetButton() == wxMOUSE_BTN_NONE) + { + if (mouseLookEnabled) + { + VertexShaderManager::RotateView((event.GetX() - lastMouse[0]) / 200.0f, + (event.GetY() - lastMouse[1]) / 200.0f); + lastMouse[0] = event.GetX(); + lastMouse[1] = event.GetY(); + } + + if (mouseMoveEnabled) + { + VertexShaderManager::TranslateView((event.GetX() - lastMouse[0]) / 50.0f, + (event.GetY() - lastMouse[1]) / 50.0f); + lastMouse[0] = event.GetX(); + lastMouse[1] = event.GetY(); + } + } + event.Skip(); } diff --git a/Source/Core/DolphinWX/Src/GLInterface/X11_Util.cpp b/Source/Core/DolphinWX/Src/GLInterface/X11_Util.cpp index e57c095d8668..e3ea3f3c90af 100644 --- a/Source/Core/DolphinWX/Src/GLInterface/X11_Util.cpp +++ b/Source/Core/DolphinWX/Src/GLInterface/X11_Util.cpp @@ -18,7 +18,6 @@ #include "Host.h" #include "VideoConfig.h" #include "../GLInterface.h" -#include "VertexShaderManager.h" #if USE_EGL bool cXInterface::ServerConnect(void) @@ -166,10 +165,6 @@ void cX11Window::DestroyXWindow(void) void cX11Window::XEventThread() #endif { - // Free look variables - static bool mouseLookEnabled = false; - static bool mouseMoveEnabled = false; - static float lastMouse[2]; while (GLWin.win) { XEvent event; @@ -177,58 +172,6 @@ void cX11Window::XEventThread() { XNextEvent(GLWin.evdpy, &event); switch(event.type) { - case ButtonPress: - if (g_Config.bFreeLook) - { - switch (event.xbutton.button) - { - case 2: // Middle button - lastMouse[0] = event.xbutton.x; - lastMouse[1] = event.xbutton.y; - mouseMoveEnabled = true; - break; - case 3: // Right button - lastMouse[0] = event.xbutton.x; - lastMouse[1] = event.xbutton.y; - mouseLookEnabled = true; - break; - } - } - break; - case ButtonRelease: - if (g_Config.bFreeLook) - { - switch (event.xbutton.button) - { - case 2: // Middle button - mouseMoveEnabled = false; - break; - case 3: // Right button - mouseLookEnabled = false; - break; - } - } - break; - case MotionNotify: - if (g_Config.bFreeLook) - { - if (mouseLookEnabled) - { - VertexShaderManager::RotateView((event.xmotion.x - lastMouse[0]) / 200.0f, - (event.xmotion.y - lastMouse[1]) / 200.0f); - lastMouse[0] = event.xmotion.x; - lastMouse[1] = event.xmotion.y; - } - - if (mouseMoveEnabled) - { - VertexShaderManager::TranslateView((event.xmotion.x - lastMouse[0]) / 50.0f, - (event.xmotion.y - lastMouse[1]) / 50.0f); - lastMouse[0] = event.xmotion.x; - lastMouse[1] = event.xmotion.y; - } - } - break; case ConfigureNotify: GLInterface->SetBackBufferDimensions(event.xconfigure.width, event.xconfigure.height); break; diff --git a/Source/Core/VideoCommon/Src/EmuWindow.cpp b/Source/Core/VideoCommon/Src/EmuWindow.cpp index 9b8cde517816..7bb675789ed6 100644 --- a/Source/Core/VideoCommon/Src/EmuWindow.cpp +++ b/Source/Core/VideoCommon/Src/EmuWindow.cpp @@ -7,7 +7,6 @@ #include "VideoConfig.h" #include "EmuWindow.h" #include "Fifo.h" -#include "VertexShaderManager.h" #include "VideoBackendBase.h" #include "Core.h" #include "Host.h" @@ -41,60 +40,8 @@ HWND GetParentWnd() return m_hParent; } -void FreeLookInput( UINT iMsg, WPARAM wParam ) -{ - static bool mouseLookEnabled = false; - static bool mouseMoveEnabled = false; - static float lastMouse[2]; - POINT point; - - switch(iMsg) - { - case WM_MOUSEMOVE: - if (mouseLookEnabled) - { - GetCursorPos(&point); - VertexShaderManager::RotateView((point.x - lastMouse[0]) / 200.0f, (point.y - lastMouse[1]) / 200.0f); - lastMouse[0] = (float)point.x; - lastMouse[1] = (float)point.y; - } - - if (mouseMoveEnabled) - { - GetCursorPos(&point); - VertexShaderManager::TranslateView((point.x - lastMouse[0]) / 50.0f, (point.y - lastMouse[1]) / 50.0f); - lastMouse[0] = (float)point.x; - lastMouse[1] = (float)point.y; - } - break; - - case WM_RBUTTONDOWN: - GetCursorPos(&point); - lastMouse[0] = (float)point.x; - lastMouse[1] = (float)point.y; - mouseLookEnabled= true; - break; - case WM_MBUTTONDOWN: - GetCursorPos(&point); - lastMouse[0] = (float)point.x; - lastMouse[1] = (float)point.y; - mouseMoveEnabled= true; - break; - case WM_RBUTTONUP: - mouseLookEnabled = false; - break; - case WM_MBUTTONUP: - mouseMoveEnabled = false; - break; - } -} - - LRESULT CALLBACK WndProc( HWND hWnd, UINT iMsg, WPARAM wParam, LPARAM lParam ) { - if (g_ActiveConfig.bFreeLook) - FreeLookInput( iMsg, wParam ); - switch( iMsg ) { case WM_PAINT: From 49eef423a84c7f9ce3a89eb10b8a43e8416361b4 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Fri, 29 Nov 2013 18:37:33 -0600 Subject: [PATCH 158/202] [Android-overlay] Add the new overlay icons. Support configuring them. Disable hardfloat since it has issues since Dalvik doesn't understand passing floats due to ABI differences. --- CMakeLists.txt | 4 -- Source/Android/assets/GCPadNew.ini | 4 +- Source/Android/res/drawable/button_a.png | Bin 5178 -> 0 bytes Source/Android/res/drawable/button_b.png | Bin 4971 -> 0 bytes Source/Android/res/drawable/button_start.png | Bin 1511 -> 0 bytes Source/Android/res/drawable/gcpad_a.png | Bin 0 -> 170200 bytes .../Android/res/drawable/gcpad_a_pressed.png | Bin 0 -> 127729 bytes Source/Android/res/drawable/gcpad_b.png | Bin 0 -> 171702 bytes .../Android/res/drawable/gcpad_b_pressed.png | Bin 0 -> 126035 bytes Source/Android/res/drawable/gcpad_c.png | Bin 0 -> 171841 bytes .../Android/res/drawable/gcpad_c_pressed.png | Bin 0 -> 130763 bytes Source/Android/res/drawable/gcpad_dpad.png | Bin 0 -> 100422 bytes .../res/drawable/gcpad_dpad_pressed_down.png | Bin 0 -> 93187 bytes .../drawable/gcpad_dpad_pressed_downleft.png | Bin 0 -> 98656 bytes .../drawable/gcpad_dpad_pressed_downright.png | Bin 0 -> 97518 bytes .../res/drawable/gcpad_dpad_pressed_left.png | Bin 0 -> 93574 bytes .../res/drawable/gcpad_dpad_pressed_right.png | Bin 0 -> 93954 bytes .../res/drawable/gcpad_dpad_pressed_up.png | Bin 0 -> 93133 bytes .../drawable/gcpad_dpad_pressed_upleft.png | Bin 0 -> 96620 bytes .../res/drawable/gcpad_dpad_upright.png | Bin 0 -> 97579 bytes .../Android/res/drawable/gcpad_joystick.png | Bin 0 -> 178852 bytes .../res/drawable/gcpad_joystick_pressed.png | Bin 0 -> 150018 bytes .../res/drawable/gcpad_joystick_range.png | Bin 0 -> 70537 bytes Source/Android/res/drawable/gcpad_l.png | Bin 0 -> 100769 bytes .../Android/res/drawable/gcpad_l_pressed.png | Bin 0 -> 80900 bytes Source/Android/res/drawable/gcpad_r.png | Bin 0 -> 103351 bytes .../Android/res/drawable/gcpad_r_pressed.png | Bin 0 -> 83819 bytes Source/Android/res/drawable/gcpad_start.png | Bin 0 -> 175622 bytes .../res/drawable/gcpad_start_pressed.png | Bin 0 -> 133783 bytes Source/Android/res/drawable/gcpad_x.png | Bin 0 -> 106229 bytes .../Android/res/drawable/gcpad_x_pressed.png | Bin 0 -> 84332 bytes Source/Android/res/drawable/gcpad_y.png | Bin 0 -> 97383 bytes .../Android/res/drawable/gcpad_y_pressed.png | Bin 0 -> 77398 bytes Source/Android/res/drawable/gcpad_z.png | Bin 0 -> 75310 bytes .../Android/res/drawable/gcpad_z_pressed.png | Bin 0 -> 60839 bytes Source/Android/res/drawable/joy_inner.png | Bin 1125 -> 0 bytes Source/Android/res/drawable/joy_outer.png | Bin 1690 -> 0 bytes .../emulation/EmulationActivity.java | 3 +- .../emulation/overlay/InputOverlay.java | 66 +++++++++--------- .../overlay/InputOverlayDrawableJoystick.java | 15 +--- .../overlayconfig/OverlayConfigActivity.java | 19 ++++- .../overlayconfig/OverlayConfigButton.java | 24 ++++++- .../DolphinWX/Src/Android/ButtonManager.cpp | 40 ++++++----- .../DolphinWX/Src/Android/ButtonManager.h | 32 ++++----- Source/Core/DolphinWX/Src/MainAndroid.cpp | 4 +- 45 files changed, 116 insertions(+), 95 deletions(-) delete mode 100644 Source/Android/res/drawable/button_a.png delete mode 100644 Source/Android/res/drawable/button_b.png delete mode 100644 Source/Android/res/drawable/button_start.png create mode 100644 Source/Android/res/drawable/gcpad_a.png create mode 100644 Source/Android/res/drawable/gcpad_a_pressed.png create mode 100644 Source/Android/res/drawable/gcpad_b.png create mode 100644 Source/Android/res/drawable/gcpad_b_pressed.png create mode 100644 Source/Android/res/drawable/gcpad_c.png create mode 100644 Source/Android/res/drawable/gcpad_c_pressed.png create mode 100644 Source/Android/res/drawable/gcpad_dpad.png create mode 100644 Source/Android/res/drawable/gcpad_dpad_pressed_down.png create mode 100644 Source/Android/res/drawable/gcpad_dpad_pressed_downleft.png create mode 100644 Source/Android/res/drawable/gcpad_dpad_pressed_downright.png create mode 100644 Source/Android/res/drawable/gcpad_dpad_pressed_left.png create mode 100644 Source/Android/res/drawable/gcpad_dpad_pressed_right.png create mode 100644 Source/Android/res/drawable/gcpad_dpad_pressed_up.png create mode 100644 Source/Android/res/drawable/gcpad_dpad_pressed_upleft.png create mode 100644 Source/Android/res/drawable/gcpad_dpad_upright.png create mode 100644 Source/Android/res/drawable/gcpad_joystick.png create mode 100644 Source/Android/res/drawable/gcpad_joystick_pressed.png create mode 100644 Source/Android/res/drawable/gcpad_joystick_range.png create mode 100644 Source/Android/res/drawable/gcpad_l.png create mode 100644 Source/Android/res/drawable/gcpad_l_pressed.png create mode 100644 Source/Android/res/drawable/gcpad_r.png create mode 100644 Source/Android/res/drawable/gcpad_r_pressed.png create mode 100644 Source/Android/res/drawable/gcpad_start.png create mode 100644 Source/Android/res/drawable/gcpad_start_pressed.png create mode 100644 Source/Android/res/drawable/gcpad_x.png create mode 100644 Source/Android/res/drawable/gcpad_x_pressed.png create mode 100644 Source/Android/res/drawable/gcpad_y.png create mode 100644 Source/Android/res/drawable/gcpad_y_pressed.png create mode 100644 Source/Android/res/drawable/gcpad_z.png create mode 100644 Source/Android/res/drawable/gcpad_z_pressed.png delete mode 100644 Source/Android/res/drawable/joy_inner.png delete mode 100644 Source/Android/res/drawable/joy_outer.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 74c254b68b56..6ea0510baf99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -289,10 +289,6 @@ if(ANDROID) set(USE_WAYLAND 0) set(USE_UPNP 0) set(USE_GLES3 1) - if(ANDROID_NDK_ABI_NAME STREQUAL "armeabi-v7a") - message("Enabling hard-float") - add_definitions(-mhard-float) - endif() endif() # For now GLES and EGL are tied to each other. diff --git a/Source/Android/assets/GCPadNew.ini b/Source/Android/assets/GCPadNew.ini index 77e790d0fa6d..eb0c8077712e 100644 --- a/Source/Android/assets/GCPadNew.ini +++ b/Source/Android/assets/GCPadNew.ini @@ -18,8 +18,8 @@ C-Stick/Left = `Axis 17` C-Stick/Right = `Axis 18` C-Stick/Modifier = Control_L C-Stick/Modifier/Range = 50.000000 -Triggers/L = `Axis 18` -Triggers/R = `Axis 19` +Triggers/L = `Button 18` +Triggers/R = `Button 19` D-Pad/Up = `Button 6` D-Pad/Down = `Button 7` D-Pad/Left = `Button 8` diff --git a/Source/Android/res/drawable/button_a.png b/Source/Android/res/drawable/button_a.png deleted file mode 100644 index 7e685324ae8a4ad016eb1033d64b659966cfcd0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5178 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 diff --git a/Source/Android/res/drawable/button_b.png b/Source/Android/res/drawable/button_b.png deleted file mode 100644 index 21da4c20f123fe6409b57cc6d837fd0f60786098..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4971 zcmV-x6O`Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000u&Nkl#ha<5{UH9txuIOkZs809n`&%Ts0+5dL7Mp+hx=8#iZN56lus~=G z^GCTaaPJYVwOL@?*!;U~#ueG%;YhSLCE<7mhNB0Na5}l)^@HUA@@`!uHsv#ADq?zo zpYg=zwNdDhX!8d-c_aTCE$TUE&YT=*_Uzdn@p|suxjFOZ>sC!i70?@py>=vnsf* zABcEaGs-XfrT!6#bQYs~VAgKTFnuJ!bnJ?0)28K2ojNsV%9JU&(B#RJ)5w0N`*OK| z`t<2JGiT1ssjRH@!1;S5@3b(JN@!YiKc8p6`(ykD&dIg$6ywpV4`Zf9vFvPwk$;b-RBzM@e$?6 z$Wr6Kj#sx*H;kk5H85_N&9zL99XmEUX3Ut_op;_D8$EiorMO5+{3x7k%M;3nl;MCf)`3~PTphN*gfO?uFFfb?2Oo%oz0URm<_{0chZ(StP zy7A)7;!Z-5eOQ9|=>GQ#3c47zMPf055Qn6~`Fg)VYF()Ji^Krb0@CNj4)MQ7+@pI7 z#dtm>AoE0Vv_hZ9&?>Fx0ipFh-vYDFzD(4K);2o`R`S-nqw;^~(4k11VN)^LYMvyM zuZ)J|mf>gJyuI(hX_Ilj`(*GUpYu(#k%(N4=m6L3(|4-05#k#i8`h}8u zfdSTnU`@l*24pB09UW`tTteFdVUU+03lu$0AWuS z3Lbu|@%COl)MI>*T7o)|Z6GZLgu{+M6KfjBpyQr7wWYKAh7A-nm)K{_o3~0fE z1(rUi|N97(Q$;5Uuu@$CtVBR|1WY{#ZE7J<3Z*Iem>W*gm_!b~0S+8tc|L)t(PR+n zBKf6(P`B^H7A*?plyG=aZ6uEkX@fA|T8wuPQzc^dL{aa>ix;0JIzx2!k|j&d6I~#> z=>|;HqW+?D1jOe>eF02B^kyxzh_$5|ZO|}*Q6y8zM-xmqWj{Fr z!+;P~0)=CYAN?>A`-t;MlR&UZj!~<333GBto~P&G*gR4}@`ib&UMdhyg>f->ju^gB z%wD!^*|IN+1}tB`{94f<(U(Qnnb!k#&o%n|N`2>2z9TwE+Hxj9XuVznqbr)FXSD?c zfX5sp3_@qFt~qj&9st-r2~sLSiyp|@A0$%IR6hQ`vN}B|N8UMCL~M^s64*U9-ww%( zdLngg{46o>1;aQ@Uw`kt_ug{PJ@*XOb;!zeo7h^qzSB?97He1`#{o6&^U))Y*l%WeWx;F}hfb8oVs5k;o^b#D_fKE5X3cET{IzS>E)*@& z>qWfQbzW6fRi*Bkrh5To3_wUDZqZtp$7T2sqY1tBtS&fP0nm~VNmj|eBp?q2mr4I& zDq7Vj0D@n#($_Z@WH#+ zuV24PR3%!sVZ(+8MC}JzzH;5Vb<1?`0s%5ZU`!AYBL(8^_zgh7r(j}e8_y-j zAkhKR$)1%j|TsiBx z8*chZHUPNyzS*_=zaCw%P)f9VlFVU*wCX0Uf0dqdzCh?hcqCaOV-(~04geu$5G!{g z!v2hAkO&{Kx)cD3_-XAQ^%wITZ1VM9vCwZ%7AJ}Hm3m$z=?_BslH7PCh+4nhwfnz= zAZ*>b^}E})ZTp_C-{1b&V?TId(c)Kse`4v!!TVnB+wTLJ&@6#aE)c$IOaU!8QyS1y z8qiT*zm)(eU|;MHL9t(#NmI8-Mud#@1Hbx557UQO-;>Dw{PG6Gw8`DpZuxlN4u&w?tiak z8!vlv>9N^!)?*3+pp5l-juNG_$rz;pC7d0knH~UWr(P4LeE^sY(JS24n;rTO6e*9% zf-DRG!-o%#iwb0G#Nkc?fNTx>$NAkV0LI9P&64!**|>4zrngIbrvdQPQ&0Uu^opTh z?AWp6B|v%YimUz+1Ylc5#a012Lk{sy!XpV45rMQ>;kS!Spp7J6WRBV%0AYS0a83w6 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%0D?zZEuVSx!F5P&eY{qf=RF1TlMYWmF2 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 U!j$5>Q~&?~07*qoM6N<$f)yET=>Px# literal 0 HcmV?d00001 diff --git a/Source/Android/res/drawable/gcpad_a_pressed.png b/Source/Android/res/drawable/gcpad_a_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..d1012a76921468565548894e415d2b23e18604bf GIT binary patch literal 127729 zcmX_ncQ~8x|9ud9)1vlPd)26#rS_^?Ma8UDdlM3?s7+C$R$HZJ)n2hjDcagAYVW;8 z@_W2L-|P2}$Q5$k&-1+Q^E&6e&K;|-t42yhPXq#iNHx?|3_u`k;8$!A0Uq#j8Zdqh zygcxEX6p08{k4z3wU<3e$jxu+QHty-r6qEbI@KEgms*zp`v6MFuU7E2xojY zBX243L_BzP+9H|d;=Ur562U`kqKDD&5-1;g3_fc#dpP4`jmA%m%PdzE&kjK-6-9j| zN)^ST$bkpR%50H}tZb{(!47`KJCKQ26Ssb6=KZ^Ftb_}*+J41fzqT~DG@p=-@FIWHLd63j(|@E^O$7F7coXVO8cwzPTv>G7BUk@#EjW(2A3~h$`VD zUtUP&;Tf+6mF1wu)Am&@UC`Zs5f9N&E%9g(hf+Hgx`PCcE@qEgIdOzFQ>!h4Xu!_{ z#1ZzupLt;?3t!a;34Y?NhAD=(U0=jN9qG-CHN*-g``28!Sh9f&$px)ywYScVj^KAR zM?e?ESh6LWQTWN>8>>6SSJl90t6)~hvAvEU!_gUs*Ftnv@Vau%qyP>yB(lnTt<2P9 zwL4Y#N=zNQ!!$2HP59s+e#9RpuCX$ShTSPSz0U5`d#H#}2h6W%PT+ERKv${4GSgC8 z$tKY#&tZ+`~eOr@<_YCsmVqdlj*OyK>myY%Z+fW{q9&{gc zxD=sj<|cQ}4qKLyI5;@}&X2bH#}9T^pd*hrV(72B;l7x{ zy1md@r>`H?n=H7gd2~4$+#0Pf7whNTv}R{#uT_(nUo8ZzBxs>d+C^>q-m4|h8!g!y zMNh%=`B$b2U$eZQ7}4hFh9kYYC^ z*uWXQ`#t*d$BSgJ+Zj=~{=J<)U$pphS>MwNni)cmdTGrgd6Xgsko?yD#&! za&_K&^!@}cEDgB979L7+uy6lY#V*2|Fh%V44?PzlUqekyk6A7|@CV4A?6S_EO0*kG zkNx&)vY?)nzz@JWr637$p5Zx0F!`ByPi3tdxlBMKOltYPy1 zEeT%<77V&(u}DMIO&FV}$X(38fQMpM2h;E+fif21CR*#v&r_p=j&#)UdU{ z`-<(W%y0l~m4qDfL+aH<6@qla(Tl+cI`Ze=lWdIGSd+5>$3tSR%@4 zBy%1oIT++c4)4Yh3Ay%3Nf5NS=|4V8>kj#-GPn?Wv!_NTd)U#w;E!yRbRI8h#yKqQ zH7jnOv&4D4``}aM#_#@PBRl1WlJ73-%0#DAZ@DQ?rbTbtJ)e+Lv0HsV&+_Kk+M_#e zrD;5SMPna(vi^&I0fxD)z+Cn#3~W!7P4%35FhJb@sVvn43%M&CLPcHBBW#a5fE8<7 zxe8*3^pkGXQNj_U@;Bp`E>!~~8^)o%@5{fMXUx)V!t=3$7ryyYzEhI@>Y`ob{cs<~ z)Cx^BqQn<~hJ`h&K7y);#nJU3_*03Z3VwT!%F)VRE=BB-E1*O|woAXh4UT!%wRC19 zchGv0u@1C5pl?#cF9fhY$oPIMnZOj)o-4>(Og(+S7IK`H&&dKTJD5i=&w}Bxmz~v* zW72)QZwU!`Mta{j2y(yr+b}0&;CTc&GJ<~eMK5D{1oEE-H~c=BU8J2Fr!cmrG+tpE$X0!qR4{=aZ*p(iMul);D}4x^PD|9>4|tPeobAp7%&mIgEM zj}G=c@1j&MM6>)=%U*p>_3=$e1=R5uLwT@s@axmtt$lxrzs$z^u07vaG!x;f(6__` z70h7Y(aTNB&7MT$C6(4{{S(*Wfw)BhY=A8~xtm;}H77#e$<_6l5({=M-U!8M zX_9Z%`4{uCw+EH6Q8Rfa)cQG!40FsU6dz>q5|U$7N3?hn>ycw+#GGta^lSrkx7lZ- zj52tl4=8TQ&{w0d4Cx1s)Olg;?d|`X>z_Jqs_KH>2LJyeWU)So*=s(*Y)D;R>$JN` zjC;DdyYoad`F*H94faEZ_^)G!xMEAZyiz4buKvVN6J=7RVDux0?=-$Oz;>JqjaiGF zJdp?__DKSrk_?>K9b4)J;u_8rMv#22r_7K07CtgEa{MVt-fZ2jz?W_ip3K(%whFOo zS0H>xTd3i=pz;@BtL^bp>Z^QYdWQn16k_uok0Qo9|HfjJIe3?{wyw?yst6M?mj5kw zR7Vmlyc6om1?mo*mrUx??6%{x zXKL)%0KBQ#lR{i2BnvPnT2rem)trnfz@FdnOCHc;EYmUq5Psdt5Xt^UG#g3SI#ox*k~m(Nxt!Kg zsBKKI`%gCg&)8HcPu<7`Wr%XGx9NQspxqQjf4K8zermZl4u{$_;emK@X{9{;J@8(i#mlAbrXr;lW8FV_su~Q3DXa+U z-}Dbem_}}nnY*_cwXY;TnRRPBn@n}|mF-!xZnx}(ozz3mfBpCLmYBFeFTU#DQpH-_ zwvhP^tH3TtVdxX=%NA~(Y`fa?GnMu_%2OmE_81ZgS)s8{zzM7antyxH*|rRRx}t>% z)*%WxY;|UqG~{iLcYtr3k*i!p2Y0x9zym%bq~?AP{2nRnDSV z*yQZerP#rW^)lwjR5XFHAP-BtHU7eI(C=t5d{u*b_?~_enK@YAt{Hpyu!?@T8@lv~f^vzM4|K3CZBX ziSp2UD}tpgi`~3z#qF64S;;`6)};ic76WXvs9Or>{m75cG|^9B0}d-t05NnyZ_`2A zIg)z>=uuwi?V(k_BS2PIc|Q6WAc1aXvXbVXul5F+F}zW8y4B*zav2VDaB%Ql`*KXX*3chIcL-5});{;07OY*-&&4K3=%rIHIP301?Lr5+@&fbw~Z^O&}{)G4t7+$wvNQusS!Er;^ar(f#< zg;03)CDW4rr(l2rM0g!>p(xM40mwTqY!OcE4#7|?l*5s4RcB(t!+$FsNWs~i`r6u4 z+e!KHG5nS9y6|?=Q@(Uf*NJaN_bFktG`9eg-OXK+ub8A2Jwi$cM_*7F;4AjSt zyBSV0AaPD2QeEixMpcZp8>hm1V?+?(^u8P$qxlN&&5tbGN7iq0Ge<}MhmDU^K#jc^ zC-kWJWi!&MPucE-1>hX3K%pJXLQj6Vd@(59t~r+|W5Ep}2zMdg?2d|nD&SxJWN>d4 zt#w#nFp>WjitvCWnBLIru#g5?SrRML zMQmB70rbDP$3%(j?n^w05qfd*?_%*rwYYf{Wwnw?)Wv=!yH;3Y>4MGfnDie~sd z{gwk}&9jbIsRXhqqksOlbW_5P#~ZmPp=d8$2>F9yX@bB-YF+6 z>W|;zW2uEz3Jx!A?W;B1XC@wAVOu`G*ED`OuhajThi^MJbo=5`&MfzllfmB%bGMmc z4W*Om@3upiS9Tw%f?_~uJOa{^jkwNjEIAdcLxRXaY}BGNjmpybocmYhO(hS02;$d8 zlR$pO|MsjpC5Q~gK6ib?X|V4?PjZ+axSlnExyr|s#?woDgS-!U3C|CYP020~`DGI_ zr386DMcA=HT~oau^(5bV@;lFTwt#Wkga+FtVSzs-ZW3|`^u1l-*7E#2T7x6&GIEKH zSo7Ycc)a3tOpL*`xU0L@n%1b1QUF2ytaDlMFUh%b`;`PKw;zLCIGL~-&Ax(x+}5cV zY}kP#s#Wk%D^!qF8-UTS3Ye?WIk(oMzGc?f4s&Rmnf4C!;tV=D@*iX>_FU<$2!B2H zo$CZ)8URWX#*rjjgJ^4G!VQ&cPkMRd^~bWTJVaE(JDC;^2}ECQ;9r8{&q|Uok4>YV zP%A!_LXbXkok0zF7R}kPmPfRa`s@jX9NlaovDheI&o!`Vr2I36O=?bc8v3twucUm) zy>|2V77sz*ESfC41Ok$mLp71{c+t=N1PSaPD%2QiVI4U0i#dkzXW zGXptp&84LX1Wcsi9C39mhfb4P6ZgN{yobzOcO6^$JtBnifr>`H<4^BD8axw;a_7_S z-);NJ=r8|mxZrI1`}>m%ShhAEOqf6_ge)3G0BQcf!w}!4=B>_XDk0}t>J!?=ZLYvs zbP3_F`37oVXJ*w1ZQ@#mUY2<8TfYz3->7H}QrD0A=8x<)>H`N@C$ z$58&9Ap!ggJ_8pv-+R_sY6SXvM{o*I%^v_B<62$x6hWn8HCgU$La5q>O%wEnJ>~Oe zVLV1q;(m|*+oy@kjE;{s^pE5*t-9C-BuI{b!8VY(Al3^GKY{-Dd`Dok5Pvd#+@GlI zo(p2G!bXby21B8flW!_>$6CgY?-r{J@h=a)GeTuESS&pQbwP_eV5`|$C-zQ_-m9U6 z-28!Uh5g2rZRBO2)HV2UXIYMB39?fjlG1uH>r!>*(Lr0lvgzi>(or9hXj)<1Wrh6; zQsAy~rq|G=GXa+O44MiIjwCUS*!XEt5q`E2rONG>EA=m#QSzKp{eqmJP+(<{Vlj+7 zWK;Rqlih0Nxwi~%({GwHA5|%8gNeck)Q>oEDD~$Yyzs!3OBX&nKuIlx_A~jD6g#u- z$_DJ0zoZ3J&3|2e6d-l5!Wo5#ssoqR)*g8T`mE|lnx1@9@N!Qq?}WVECnO%~puSF6 z;Fe1(#bq>Uy>~v( zjAwWV(WcTei)3D@sLeiPN-Avh%d81@&-O(saur;#)y;zTIMIEw_b8Qmy@r{zvkT|j zv+sul@SkDtY2ldHpd>vWgCb|Ee_c_63fI4rW)GToDm!KQh$AaJ_M0})ymyxXa6}nh z(Cgt3-z8vIq7PIu+at*3O(Th$0Tkk|%^k%>z3plDC%MGkL|m=sv|*n%eSy#(L20E+ zcZ&KwL|HrM&sE3RdU%3E^)uQ@N3<(LUpbNgIDKW@x+k~k79E6Lf190%vr0wst#*uy z^)tUg_Q*39DaC;gUmm5o0^M@$P6P*TeDrstPDs(V#QJDDu#u(2&f11U@|IX6l{1@M z?rJs7_0EkOz$K<3SwFfWh3I98darNa5qeh0q!M;VC&ckQ&R2zFL(J(9rxyTX7khP6 zmiK=bIK09R6@AG+wviFqW>6{}rX}qCVy@(<7p{dbLET|SbyPxX@utT{q{Cgt|Kpg| zB+Ql#udiQ_cyU_VW~0;u-)`X`>%>l|!>w8-qI1Xj`g?DX9*+I!^j*Xt-@Xd$d8hVphy>=-w6H7`gF4g%~1{Dl{}Md6m4-};CrRNuKNY-{s!7=1Q!2MHfJv=yR8)5ExZja z?isCM>!4^n^PVb;=zfYd#$TBCDqi1y<+e{G9Nl&=rFt!e;E-0u>dp#%?77_YdNnYq zn>U#KjV}?S6ATc z=@%j4eaonh3N1MSgPmI&ul^^Ab3-~wgBNBt7}YT;ZUN07)8ivk4cT3*8lCM)6peG?xp2b)lv|YF@0GmXOkdpmUZZrMQdt9X& zD@|<*QsUE#3n_p=B78@3A9LgbyiAovp^(Yr=XmK~^vC~u8j7IC2@HahZ|}rXa>kC@ z3vgyH0OHnelq9W?o<_=bVDOlw;e1CJf$|y!z-tCGY3?2v(cJGhJ|*ayUF+)RllE+n z)Dnr|AY3bU;~SAc{aDPaM5wa`%rQU zMr26Vz*|W_fmz>dC@2ZcpRRd&2U`3PH3AJABYJ{gu=G4Flwt3kRW7 zURz%>>-~IvB_1shx`U)-g#J??$7`?HK`1N~L9ESf%OkIHo_wg+ZS7y(9m+p#2|ZU5 z*oTQAMD;nXfa%!D?vK$)o$pqey$R50Nt?p#avxHQ(j@am&RmWv!HM%&F3Uu?^jjdEFkt$UPT@aLzr+HqgX58aQ2us*9o>k?P-dRC}82SZ}u zsn0V^QNmVXg5#YQRDFsh4^jRuE-qozVf9I&r=LEl0R(Q06#G7nJ_3iH?8^Wa4Go2T z1i9>?;?C~O4>dvaN&Rlc6b6r<@ejnSM(@<37k2eq!VchVeY!D*2bz>%PP!T|K2nnB z(M1@--hy1(PpW`!?!$P(AdL}_psg>Ju-hHT?KeB#demxw^^$wd_4h~_yP7ZZ8Eo^k z#_g}iHvCh`hHi$>Tbq~nBxfwr@WZn1!|=|ee4K7f=cm8#%v@gwMju3k{T}T;ozry0 zCw5Vp7fNSzkUx+C4hXXCK)XHNjw${ujF%N$L2RK zcWgQS$*;Bm=&LVyEJy`ncEG)jH}Sr+F9Ej-3mbTQ`sr%u4C;C$R|#0}X;6S*9n8PZ zape- z#2BN0O!uaVVEKd9o0;*=KEs3k;9>5mCmC=@Y(!&Z;uTYLW*2M8ocJqSWt*>iQon0- zG(9>)&j_$26n4?}@?a3N=BxOWVSHd%B0m#gj8^^0GMYVx1Br)me!fabOF!^-R94ZJBSvU*EAe;bq| z_4LAz4xXFU(WJDqFikh0EGKfvB`5R>%M=|QRa+iV{ez1Yi>BDMD;zmD_L8~qp@^dnJwGjFR}4_ z)y8$)568?G`OTVaUtasU#uj=qjVh4g7*FAV)jL+cviprl;^#aJb4{H6>K_s}^i`+d zyI`IF5_tObs4SZ$^)|K9hh@=tjH4j)_4Z0c6qhRC$%JfIcdPEo2Oe*Fd5arO^}J_M zG;qaTwoSr+@#sO&zbM+-5unP>zvcyfOX-&tpv@Hxxkmh7Ig*5f9%XOxl%wN>noF*; zMxn)1uh01X5t^sLU*WK`?TPfjlXYL-C4a2QP+@v-v`>fUf61r{Spm0je15SC zhJqyRw#1J7(;2xE!x5vMU5xcr-f7L3ZX(uLta-KJf?pIbF)CjaYP18C%J=s6qV!nM zTh9T1NY+e~(BT-Tx*9}6-f??=dK!-!)C*;o#a_c}#aagI=03K)KD*adx*h~GoA_>3fg>1jz z0Q~Wb-!^1a^h7#O4NTuM8d!%p;>Xjaf}^CCfMqjC07#KSx{hbT(~9-CShwOoTJadG zngZ_hx0tC96G(jYW{KlGxbK0(xUTF`&teLP+IfYJF(T&Zgpect=mtA)ox4F}>k}bN ze#sd}MPP_==k>-(usmm{N{}|f`sbqtU%}eF(SDcCf>Pa*D8`u1Pfk@KvmXyQX$M3_ zsj&t&pQ`iA9~n5J&vwtO!5Sl<=(R+QsO+-QAhA?-32ooMS|H1O7L^UnO}4R4#&w*I zlefQ;mrTad;|_qArgZc-A3eMYs7BZKMEE6RAOd3WyEai-DOQUNVTgR{O4o-c2dzI+ z9^DK8ZQ;~mgzD$I2b<vSN>M6pMwt zlK4&SRgiVDj*1laPQJVcw(&_=8sQO<>VF*u{G2~Gx?_DuXGMsJ?J}oqK6t^nR>Tvt zW66WEd9D+&Bw^Sk&+YZqHGfv|8^2hgGKhnJ`QpxA{PQoGMV^QXYvPT%gaW&hFld{! z#E|b7;40^<<@qlLx-u~_B}9S~^JVOR3jZHu!ZlJNFyxmCqldR`cmA^-F#G$s(wDG< zZ`{%1e0FhhF$F$6spA&Qa(&7pxg=#8Tbr`$^@80ZqQcsBNl)`W!jb#KOPYP(=}jIn1od)wx?Hv<+NY+d zoxzuZ^Scx{yzNv*gzghLhL*FRNLjN6rlfhOHVP40z}H4=euzuqd{^`6_q*%+E-JBi zQtQiT$iDZdXXZBPw5%G6mrWU*F+n&go36i$o_%^fk~@PQ;7EETW;C2D>AT(gKldUJ zbQryX!};#wVDID!`|l+4+gp4Vs0XCUqLN(*wy+IZ=@6bz7?nNI?r#k|`Bt5)=U-^v zkx9?(SGDryQJ`D$O+nrT)(P9A;u-~}Re>7BIKeciTx?S0NKg9|7n;`plm`^L9Y&aF zV}zflLiTH!JdB&6OQh$Tt8r3#nzqF2%S#5bX9I+@*lKyyG{K@&wDNJwN6*i~`Dow& z3@csNj0I(Y?BB`;pGj{PX_2_dF_;!#ha=jqXmTZF*gS(SEYD@6FPP1FIhTz(QtO$*qKbc>R#r^v96?1$uM9IC-ikJVFzy*`n?HL=ysx_ zQt!~~BaFeef6L)F8exHZ8Ek!vMJ%SGTgEt0Li;m^dFMB%H)M=LO`7G%_IgwPrZ(jK zM?RC3-|lqyAOpx3$7g{cg>@hSp5l-fxL)!sj^;|2MGh)w^Rt%sV(m`a9Pna`xJ;Ip zqV^uQ-zc1ozu5Vj9~z`*Zf>3*J%Q-6y)DuhMWW9e_JLUd2VG<%sYE=RZ_{TTTlecc zElE=xS7+}gsp$rJWp#_eXnCYBR<+yJoB)(|nj69N*#?skj@WT0sr@vdoJ^1ZUYlq{ z>w~8P*=Q8kd@e&rFIdWG@}cp+Fp4}Z zwy~LUL(Kthq!QbltOh>nQu?Zj-;Ir)FDg=rIDxPVEVm2vO(vf)C0jFdbHY=+9@`vF z0x@k_O-_R2CfD6NMWTkYPV)Y3_pl^7?Rc)eu)%i~SIu2Wo4u;^(WeZ}4gIaS7TLREe zU)8C?f<7qeaXqB9`C*CG6hLVoLN%HN_HUn5?#!9(0F4R6EMvNist6Kl(Jw7-gq5ia z@A-dM&sOa34hzF7?s8uCmdB@fJ|&!}HO%|-Nr4%snq9&dKAiVwEYZ~J9};Ngeay&W z8<_?!Jy+gAP|LJxdU>*e@83ubE7DTO-gR@6)u&U!n|g*MzX!m}e$~SifH86*=a27<6u?9|Z#@V(*pX;L6=ox|%Xer!(SQl#BYVC1DN7pT0YV60 zwv-#`9e$P6c&X;tfr}o$#|NislO)G4_8~}&-D2HZ5$wp`DY5-S%{?I}0LMvuG3}RO zUJ9n{{O(g{D>Rgk<>4E}r#;1*!%BBu`H2o1Rt(AlQ(7s9u}u`4&TsG~j;4lV9zGW) zINAP`jaKxf)Xq;j+%FX!uQFfH<;40s%9C-Z(}lK5YG3|?Lk_DM+ zy)?mB{*}OpW%>%FCilgPM{Jk)9hd?kOVCJ~Dw}DCE>#Gt+tZ$XzZU!a%TqIn>80nf zXE;yw?{5rXnHv#t7UI)njzy`chgSvFb2KPu_z-pmaOY7wt?q;?vi-w$d?5104@lQU z1uGCFHFdnVCm)T2kkZiKE(H-${hha1uj)NSqRhS*iI5b9f;@)*$`CF4uR^~pQrmL% zsapKNA(t*VgUMesc>=>kzPHSjr9E_x+hIZKit}?tnoEGBbKK~QILKwAC)#^rJ3IdM zzYcj84)Qp}(8I2p4%&Rr}9;5 zIbW-Q8bp4OsMckbIu^ZkY^(B+@zcWsraYvgE8LLZsHoiU;Tc2oALR~0y5TUZFg}a8 zP!jnKjoe)6zUbG7V%v)ZdEdwQc)q)^#MN6d5mAS+`~*`{Eb+?X7G7{4d#Uu3{3Kcy zfy)Fc>h@$!U0ohF#e}Z$LmqqwVl(kZi&V)BR)VyjueqIc_W zF^26wgZL{U2p@BBoXHP87j>JL_Bxv|xj2e0HzB;S62eG)+~ncjzKbx~?$0+F8I8JQ z6R|W|rp#~XpZ+kb{Xop}BSy4rHuaevsp8&kJge*5kH&$YmfWv(l#QZ(ufQ|sB{Hlw zgLkKo6n9vt&1#|q-#>Mw;)|)&45KlijkWKl6R26@NK##`PfAoq$#sP3z+szP6!~fp zT_q_BNusv1CI3?;tIC~96XOo=w7j!ZccR?a$CO{!GkUP-PJ>!xZDWj~h2jR|9|-wRa~lNJIK8$MWb-JAO(qGYl`t zpoYH%M|zCyH6Z1$i{gZww+;?2KEBh*HvrUJ#1->V>l+_-7kts!n&&vt2U znD-j8RGHpj(mD+WvW$0A#`enl{KlWnT4WS^9FJ@(HQ9K9g@Z&h zz~(&ozb5=1e_#$ zJg^NoT{Y{rT(kRZ+LcrFEE0Z_E=X*29~7IAbKG-bTEG7{?mOl)jAaHQ!BMe8jF(b! z77Iv_`G+9}@4HX;O`A#K4v2~BRT;wx1Nhp7)fGQ{6i&Ta-iMv}q*-T3tgor4`A-*vQ{X=%;3JlR*7(&0>^se&rIpAF}8EM)6RZmg@yc zC01PD4_$8E`Rc<8_fVHzzd3gW13!zNr`SPmppcVaQffd9(8*T%!C^?xYrP!Fv+)0g zaCkuC4kNiLMcNn~Xgd%674&8fxi8v#gCxwv>{KBZy#d4X+{e!^2q%d*#l6}ywC7J9 z7NRT&e%&}Ox+D=OK#vQXYuA~}`iXvnaHS%6Dkl_Axz7D`(ULg4u`8s51Wv${bE$Rr=!x(M-4mq0c6T!-dGZ2o0l4ai8qq*sc;i8-?E00&(e zl5@>EeM*FvCQfmyvm))uQ4xo4dwUa}qZ6WkGGD@=;JGuD9lX>>E`HJ7Gu|vPxPl3b z{T8OVjPu|&yB*~HivQP~os1=W$f@W1S38jdc%prMuhmq_IKKM`+f_OUGP_rB8q!iw zjV6OI85tQKq`r764+@MY366!rBgkYL_oi(TvSIEJf}*Gyb;UX}SM;7c?8$W(vGAMw zQ&YB-Zah{`o@ z1KvTI1gnp-4lv#{c>)&r8%mKW(QR=hq5|qP(qYylu&;i3-n_QGBj*u@NP1IUmD#z; zRPe|<;*s+$dCuDCpY33gTBeD8ezv?*(daJ@Vqv1(eqt{mkFvuS;rc0huS+P6`e+rz zwvfeJ+CeTikY78W7pP4&K~y_QEV-MX<(kQhk=Xax$H~;WaOu6N;+b)^;e-^GjcY%; zoC1<~ai1z-t5hnMkUf;cb-uu<(Nvs2xl4=uE&ZA&6AECp8|i!L@MpqsSk)zvf^55O zLEi2Y2#~}I6h(LNj7p!BH~mM*TS(DGg`>aZ=k@^$e-#+OZqC@H=>%C7W1ieUfHZm8 zV*umpZyof?YrSk0f9?=_r=vwHzxSIeKUkXv!K(rte?(yx0*G;LSgJ#%KNsPP5=6&mK01o;IW+A-8z%pF*%lvkMzn z^`0;*b}UHr1ylI%W4Wm#(R_JgnxXv1Z4$qa#HsP$^~plnm9ZWwfi>;hX|%LaEyGjI zyIQPXIOAv3qM$?8-tSO;Xh^VO7BwL`9- zAmvWl8Kdy@Ktxw#B!no$Ix!lb$%g_+jhV|xE%^L(%M#ThU8*ui`$%^{1}+x?7y2Xj zp_*6%Z!O9weqp>+4}bB!b2i>m@v-7Fp=!S_8Mdy|?PwxkBfRX$58nX~72$a^9)%i) zGg$btf7u{w{kxP;H4~ft_}NsZh;NlXpGiP|l%U_5=hPQpNU5QUg=uBm(-(P&sZj-L z8#!N5X7+jN%B^6PL>%58GfG(R7o)XM#RQ#9Q=|B<-Ryw8aa zQzj(zDhIIIOG=5jsnn#152w4-7lnY%e?0{Z&$odryQrT2&JN!X31SDd!q)(oRg^I2PrmT~pfESOr?Z6>76nW0#imewW zQl4Iqr2ujYZz7+dP7G^_RfIPx<6W{f(UBdR*p*{-#3vEo^5QflsNs zEU0#}%;Qg1hM4Zj({n$+2U;=(1(6c)Lnz>OYj6eq7lf}<&gHd&aHESHlfS z!P*`43{#MX&<7WakQ+l049)aEZ+M-5bewt)%HmUuD<44EhlW^_d6=o!**}4C0(2l@ zeI#5zzr>8ZY~#`(PTP*;D>Xigp<9~zTGfyVG`>HTBO}S>r6P589#^^y&O1ZW3NCc-M>XgR(&F`y|d<=ArNB)Q~e@mzKIU+6%KDrU}sIm9quK*x- z990DbvpwWBUmhx?BP>>$T=!1@>K!<{xm7y8d6K@1-vOJpKv%V2J6`}SZE^5{^a(g7 z{ytlUAd!;1{h+;Iz=r(y$x4}ySvx5H-_QN8ogCujpbDjXAp&-B(H%jqu1gAZ4Tvf; zHX0j6@sZNhp5BrCte+HFn%i+yH_p~gdDzgSZ&Z(0Xj(`R4<87t_#ClUML2r()67z7j}_7B|tW{8BiEg8A7jS?*RF7 zI6_VSdVmA7aKaHRm-dOe9eTVJ0Th<@5{3!wl|EVzOzEDVHLfJ?{24;2v+r{>Uq)5g z(XHH~lt?ap&Cix1$c+tOqpVJE(ub&Ug3`o3s@{9J&ReZLQd*7UC98}Zl2-OUu<@M1 zGs#>ItPFxVvr@mCK?lO`~2pwA_44W>V-MChF4DW zjh9jSKNVlvs}6(?2JO3 zei8O%%SM0iC10_j@o;v0S?6q|&~R-cXjc2M+H~4jas1VCbKU+YvJw~)jGoaDe3G&? z2q})8|06vBULCIeNZ80!hk5_Vlqn33Aa!)LwYtCRS@(Q|{psnCnOk)Q(Pyye&Prc> z=S=gMS{aJ4P{+Nv*$^j*cL$%n7)@<2%KF<%Tyxf;GL+T4FJD*TO6+xS8^Kou7 zoWGChZ~hi(JS%-&`0AM5ad)59V?8pXT zv|i1{e$KkXp5wb&3q!bq_PP0sBXkrot>4{fUJfu8KcJg4C&=%kMNHZTgL-(>Y;a#k zSL=7rp5(>mA|o_k*K z?Y(HA?&(R-HycB-JZ@TD)&YattU`@!yh`DJ8xLX`v#c-K$Ot?^Aeh<^}J+T!Y;O3XorOmaHEW0`Z`1kDDigu#`)$1>dPN zCcpq)&|ktII*srw=~{)!}KFv^+X2n-10I zVMHa|=WrsH_%`=-$ue9Eh4)lM*`5{ee&?@x z5T=oQFIc8KyZKEFNy3JMI?Ib@jMBG@aOOW=@#GVEzTft-xWo(AWkqSyozsQP>&wqq z)AalffKC4k7*uh|X_u_u*JXB2f=5Jk$jyV5v@w=X4KEDG@S@h# zD)#`}@mT^417x44y4|One;u>v9@2WX199G|ZyWir_R1C}LFzAL!-kotDErrbamm>h zJL7igwi)piZ;^l34j4`netaxt0@)CcjB!unp&h%BR-FEFPgyj!n5y3yO++=Td`r=C zZnc*S<*+KIemWB<5|8u(75+R1960qH1%ggU>opbkyPh9TKkap_rI>%zCCNhC!?iP9 zo)z|YJ3z*`@`Li!a~+hrU)Tvu#PHl&*b(yT*s=81JsK2oXvJ!-SMzBf-u~zv3$tEq z6=5^v40>xFR=ps0Uk?ABj!rlkfh+#(+#G|4{=p)k<%x3MG&>Q;=5LV@mcW2R1m%f9 z=zmzWbW>E1F>eD59IPl=64tz}^mXO=9OB-7wceftUFBFnsY;aIJfHsMF5LkG4s?Y8 zc~6(b`2=xktaIlSUPzQ#Fm0vF5 z=n?B!e`4cf;Cuf2qbC}r791t|131BuC=x&s_0Mo4-vltxSC)a(b%w_u#8q@V%5uT7 z>D2HDD5K3x&Sl#-@WW@JxxQ#%cEoWvtq209-I>gBweAGy>>Q~25(dzYzVrUbklUW3 zczkBR^=ARk%^T>gJN}oZ;`rMU!S`eI0;Vo z&G6~k4(?AWKa#oZpX)JbeZ3>C?;G%FPcT;O{8=u78)Ae9E1J(FT{k zzI1$wUAr?#&z8u|Yem;E=|RHvOmaf5TDbTip1#tX{kqya`t4;IgvG&PLdE1nF zNIg!zcjfrWT7~poO0~AHK0Zh2YUG2I9`@W*J9IF%%mlTlJyXD9;~6$F=1YIJs2DJ> z6BRy?Nai_d2y zW)BGsr6g|=>&B;qD%dCf9BiCyxMXsPqv&L zD)&-UV5@8@rqZxPU72o<48ueho1Dqq{)IoE!(#QGKzRRW&4Yrqq=kHgPI05qujV%% zkjpjk^@k#n;2$D^WT&D8gXz{CCF?Xw3?Qcov{DJEr;6pUvIlAZ+yt@e&bsW*s=s~p z+?s*CU*LR^g8K<+#xr*;Zp*d#K_b*`@zWO~p{T~LyTmqWJs=p31E;Hdt9_QhI(JE# ze(1&%C3KfJ z4~e%CaTCW1NjOQl%3QO0)dJ6@6Yd_pZnDVhu#~u9xEJQhF8?jerpEQ70m$x6PoYQv9TeK zx_|)*AT@~R1G1f!<5)c?V+b+}u+$A9kW64OADhwM&{R)N2t!=<3ab#tQKC}lVI;|I z?(KI;^`r=x2?W9-E_<032;#-J6~PT-^E||>>EsoAX%NTtCjUF7NcdBgVpr{d*n8g2 z?6L%hK}{F0m`U<41eTIw#kdAN-vl&YvoU8(2gQ~DiUCJHK&67g*})L|lAVxy1c%c7 z-@??>pW@iYO@6*vER%7(;`EbI&)4?idP6ZKUm>4WK}W!8rWs{R)6izr#Jdz&MBuLd zjLx)qi(Pquw_6roI14E%r}KYFQ^?FkoJ%y6JRTrUQ6JlLZ>5R*)b8gGwv|SKg9t=VD*`B8nuCQAeW25e(#(u;!-?pxEIv`c< zu8uET13WM8PI+4X_DVCg**=uku3LI*r^tWFF)8=G4u5h!Y;thIs_6x>{*zbe1W+JJ zCC!xkC%Zmcl(>-F@>lCBoIR^fh{^nmh503-1(Z|i(q~nygVu^ZcV z8Yhj_*k&8+3mP|OV@&?t|0D-v-fxIcXISriLU zTfq^vp=iJ9;Rlgmahw>;`eVY28PG7&L6k@g=`pSbL{V%ZR5Vuu=oj;^-!8ven{z(M zNq@*;xvG^F-T~?gwj1I2#;YeT22lY+!m8O^2ilShLS3C;FE4V&*}L`Go}GvS)M=P2 zGSE9KtT5mXMj06a-*fsv(7UOT%Kt6%w}-8*#9$(gR8_}X2gmKLDB5s~hmw9X6m^10 zhz$&slc*f=>Io&h>g;Wk1Zi2Y=V@x`$fdt4Cp;XaC{|FEuPB`T?upvg7*F3}aU!Xbv|a*7~P&VDDt zaTYL(#5XrdH~0!)%9#>J!4V&&33s&kv1-X8(~~;{g0V)x?fm=1dQtm>k^>xjh3udj zO-1HVH`GTe+w`r}gFmj4JUF}Cz5Bq6mB0G?_ivgY;%4j*qM$Jnx1Uu&_jLPaik*C? zomcMf-Pl)Ao)5`kNST+vdcA%4`)!^YyE$v7fPlS`;C2v9ClM%cg~k#@aSupG{^3Zk z$-j>lGtR%bh7-3AQ4AqM%J*3e^afxXI!8(}QO%IUeSb?beOA%j_z~X$qOqago@wS> z*y}C#r#X}fHW1s4pPp?WQz{X^>mAOAGq$-cF!z8L~;d5{~l@$Wf;|kGv2s;QU zw1!z2i=#Zuvq86z@=Obvj?YdpJuWx9B4i2c_(Jl&B&X&48^vs7_U1+Se!U zKt|K2TcJ~xmO4K;#olN5P0at9I2ND%0$~mQT7=mmG*KM#IPDbhH`kx6ZJZLw6hG8Y ze+KHZgU|T#KM(- zaeVk7Wicp&YM)|>^5>!$UN$NR1>eUkwnU`m8u?4R%miI-^61YzK0jezaZ!gQxCrn) zi#?>dkpeyp+H~~Ik71nC+cj4An^fE`QDGH!yuOuXr8bi1%u}Xt9BqQ zzb;CD>U8JU=(ABaMwNzh*@7V)O*TgUw5FOG*0CC%V2@5yEckQZWzg^iUNs6Vz)-hP zs^~~T13Nb$75qbCv^^2q|8x7Bxzh6Ktjh>j z1zP&*_s7s5onvS-DMZxOc<`?8T+QaDs7ExFa&vr$Jy1NP@USzaBkPg!R8c!U^l;@> zB)3mY7n*c9bk#80E@W^4_45F5gw-GLBy?pH-E&~mrVb{vt@Ar#W>!2BEB9Zk1KpSY z69joXh4jrnh;Ow30-~n z0mn=2?qRG*)(oyzp^hnbpg8V|uF6#|dYv~{DrMrwg zB2kO2a6@NfIE3hZH>ts$%kT<%&kYR}`Vr{7jgCV2wcmt3A`}rn>>*rHr#UF_*$Hji z(L$OX(>;6w%c%G4@aXUN8b4ZRoUI|$juq$>9qcmwivEr5DSFhwc}p>y@tL$XY@}y3 z)O|vVZ5-x3ZQBo5gcqlmhcqn($o2jG65o(#f=fLP~g*@=?^bWU#Wiy&Cf z=cp+1IV^$g?Fmi%O&{nR8Emt&J)#?}19GFwS!wFW6QKJELwCh4!`cvm3Yp4aJ>A*f z`fq7z#Qi~NUV+A3{Sgr=d+q7uMb7N8IXS^A!-`!?1-2KjXlfVXz}`kI^qE!Da4Y3F zF_MuqUFRW42@w{nYdNcr%DAOL$spc!J8?x{^kjA{m=@A&E8cmP_@U`| z?=MVXB>!kE&GCzSNZ@l8w+~U4JhW23TUZiT=7!uKQ(HVSmoIQnSl7bo(Mc zYhCcqD-ND@sUft%F)a!$cJdYCaP|L~vea0g5++`>wM3%*bp<7HWlT*Asp^<(m*9u- zk-vUq14_Pq$DM%1ni2JYi=;NT0N`}|=4A0}HvkdnYx!0Ikc?_6m`3XNgLxeZ_f&j! zy=Q1WXUd#f;zfhh_d$FgoC06gzlc$CKNd=vuGAP>xg1(=6a&Hk^sdUw<3=k7GZLA$ z-n68|DTBgbGT^j3-j{EiPswq|Dk!*M!HW4qz>|ZRIrtHNa|#vv?HXDR zOjAq{m}P_OafFSguk|WU!L_jE`m?iKv>jf{WTdf@R~prV96pzRa#>|ga#@5~mf$na z0V5;ICU@|xJIHp;a@x^ffxdnY@RIc1{>tXIZbZbji4L|{ ztWe*~H1hnf%CT*8`LMDpJT(aY0M_&k{(WLss;hM!02PHnB(48tsQySPwcOH1Rb^IN z8JOU)8*khJ21uQ}KC4#SkxH4notan1vvT%RR$Kq_)i4#W1I+a*c~sY19yURX7LMAs z&QGlm5*pZb5xp=%>UO20CiNvNn`^zX7x4Il!pa~-$&{03oo?r^JARV2(^_7=zWzar z7Bwc(1yEk?lS0z<>cz_u@A*Ti$?zB<*5xly%s7mLOt7z04>t3vK@D;F&>8zHK@u3v zWp`wCE6#!t|}{Xm9_2@pVjpT7Pmgz7Yn z@mI~_DEcc8%!DUse5?I;1Tk1wF9JxbQ~(8o{t+cPiX20Qy}< z1|2Ri4Y6Hf8>n-i>~C*X8#ZGoJ3tGp!koCF(oJaBkePUubxHi9(&L;vNk57kWn9T;ZuX* zEf`(F*;QEa;B1n>6kBa&UUcIs8JWtbK$)kXIDYQpeEH&A6Wz^0c)5bRB}L2n)otmq)a`< zA`(Hxh>dpqh2#9nx^9IQ{2;N_a8&DVEPJosH^2_52Dk{k{=OR?D@MZXfQe*07eCub z;`(;CVhp4#?re5`s-^jUU22VYR)5>47e8JH6g#7BRbb9^@OeJ|uDj)T+4E-iZYMQ3 zxd;yf;X5VyXtj2KFT;;QIb0L4C0ozNq0*h<+O73FG>4?+hT16+@y~BBBlAofb*nRI zzmDRibQP@RLV}vpCR}!t@L2K5lgviQi9~6aG0H4PWTi<@^@}+bau_lOZ?kx=XV@fV z@PB*x&qVPj@Orson8UUC-I=yQ7OKXuhvq2w@c*r)1?!;$(?~v+pp5xsq z8R8Tlj+>&SaP`3lTQL;P0V9f-It>m$O=^8A@ z?RPFBU-3^!F^KueKJA78&J13oUwqcAyA2Tx3!%OG6!nF!CgnHX92v&}nuS>83L_J^ zjA^|fh%HRrLqf=lfd({)t(F+?cfEO)6&4x$ZgA8XbrMvbiQj7|{I`bN#Hhpd0E%Zn z%;IWJp1kK|)jW~(s{O5Qhi1DU=>Dv4ze_@HP*yzurDe~7pmdxz1XW6$@rwV`daEUn z-eUfTkO8%@8snwxKF0jM(@Q!Yr;M#-Hg!_0UJ8jI&F};SI2aMTmZ@#F5M>{NZnI`jn#p|3ko* z+jWZjd^8#w^c?p&%YaK8xs@%h0SSY|;8TD+M+uU*Q|WK9c-?EXk7}O`v$3~QqaWC~ zIyJPRwOUGb)B)jK-y8H8fIL!OJdVcF3p^zQz6ky zBYO~j)QGbBXw{6*?#$pnRJ;B3G#E#ApV_ENMbP-nB8|wrbw~Ph=@1E3 zLl2CdgT;}T`TF_pvIxS~`!KKgBj7Y%q-knewp4iysVF4X>X8);(<+DOYaYTeV_K|7 znFN(J5z3{ayhUx88b6UCZ-{k>FU&X<&B_er+52==luBS;U{OLde*0L~kUnmW#0x!n z;JOggwq<3V=Q6qr1kHO8riocuS^YB&T_>u5I>oQMqq+xh@F^>3dl@JZOy##a0#Q=@ zf2CzFAwp?oQJE1x+%h;LPtF$rFyKbUIp}W)V10KPSwx=|pg<3t`QH>HAP<&A=?s%!XxP zlw}E!aH@*mN%5zcueIOzUA{4e@Y(AT6Dy=}%!Dw;$)Ig$BGAQPz?md1rOw~_%`XQ{ zaEEQ5-#<`<;_z+6CdQk^yRs}H()7yTtp!*iv8zAEN#jc8Ccx4*RH8$akIBd%yQ}yX z)T4@_+7KSp^vy(Iy*(bT5a^LnWVC5N&jdBNS$-RT`s59=Dbqlc2AL_t0)orb2oUdU zVAL#U0WOrb?0IdYafA!BJkUT#Lwbc48yu|Z2Ho%_xG(8Rbg1>$KM>zLWY#!aH3i)?{;LS5{DePCQd5g-@dpbSa$XP} zI4HOU#aMb6jX{mrhHRsSf_&s*I?HI>U&>>pp)JcDShW0-t>0l)9>kl7i6@&9qf3+x(=Yr} z6syYZlv8B>>$H>qM!?XRqzAR8Q#~zHtU;mGmbL?xjg65K^kZ7+)B}=pQqu{D!yd)~ zPX|V1=^!@&BmqzVVa4xqw|RnY%e+_OaeW(>>v9bfKt`wW0jM$SwIS9HBIPLxtFS)| z8NSzn_oC_Z_3mb+(X}~~JS;!?DKA$djnl$y1bOvqP0A%l!@_C)M3yi+2EV&(JQuHC zKL+)In-Tv)OK9`JNJ@?c+cc`LKNj`6682J{I^m*XqolXsr=KoKfmcctnkN2^#I-r+NnAkC#5{{S38x`Aca~ zmChk6^i($Wm&YpaEB#p=!m}XV#WD@- zQ-GAj$p73*ChX~;ev$hAwdb+g`(V_6qH|b4kMIUX0Dvoim`r47;Cqzkz)1CInG%wB-Z;?2Yb>+KMc1mZT- zwd;&?krN8pVnDL2Lf-OLvd|LUGe@s?!Wi;3+okVd2>vJs_b7`N9EJ5uQ!zf77u84l z{`ouD0QUSELgk1=)b+;bpsGT_i9wxq9Lzvc4nlmHJ{ zs|Gkph-aGVpz423(rCci;`i{L3KRjnjypxRc7^47SsDkugZvL-kEX6bkodm;A^_t+ zdyEOXDaYu-+}{W2L<&Lv4J8~LByk5@6b&@$8qnfuUh>m>s? zNDcfs*Ue{)7aZ44gmJXJjHh$1TbyG((v+B{>hPqW$9sSJUs7}C@%*PCCC#azky3TH zGEI5cMiB%ibb-n}RkOsDw@4ngpy3$ttg9I5T03!fr+{F z#(r=B^LaUp0~X9H>25@=D{UlN5;IZpZ8e6s2YM2XlZVlHPSa&ZE6iP@tQe zo4Nz!q*&MfU$Li{xxU+VYOX{+l15@b=o`>eV+O)c?+Ncf9nIX&@V@<7Uf0Bb9=s*s z;Sc(GCx?_|@UHo|LuMhI2{4`_41vaVcBweQ!^$S4{F*~W>|gVLtrP;BaOuCzWNz+Onf6af)|4{7LN;jo?tyX4%(O^P_ai+0{*7>Wu!g^O* z@Vehw-(!u~-`VgpU-V_~^}w&y5r7$i1D+QO?v=AR2CA+vtFC|ecLPA-3vtMGqNrHi z*8(n42Pwih?JY#f{>gkqNxyarI@X~^gWxSKMVrPGp6&XQyW8g{n~B8Z6i$G|cC6=F z3=SfRRFC~#evc1pyFl(ai^BbU+S2}~Lnl$O!1OCQRwRM4MF)H9Q!w@|81JzoW}20TcKQvb!AWSw zdX-wcXYgMrJdK?P;5N$>%1oaa=|PuMNd0Rd$gklNozx#)kn`5{I{8?X(d-eT$CB)0 zzSF>o6k1V0j+0OPUCvAF^&_+`mSDB>X&`b;69I}^V3rmc_zFkmRSr*zVlU7HvPLeC zHt-D+Fa+FBm60Dya}Y3TJZt0GTfhXlGT%7V(%xnm-A`96jQYmy5)wGoil8ERV?r{# z%73S}zo2?IXWZ#KfHQri>7@{$B5xCJ|UJi#;BkGTfr9f<3^JRGnt2dHlwg=sx;*+owh1j zPaAd-=L>l*&P|^5Y&tkTY*)%Yi6HvdqF9laGz1R?3}rHW4X``#net!}iR(K$Ix5HC z=`(-jkor-ma<_d+$wG7vL62MnYFEm^dVM>0>(%OC*6|D% zpFltVBp609kRhwHCiovj>NilA-YojlS;V%5|B8w6*R$)?=*59|4ex%Km;baDgzgknKl@!s6j zRcFGVjug2Rj$3=VK#q^=W^9;c-JZpEScHU;s~>u67LO*Zaw{ZztIirz6V73gIVc&F ziI1|pIb^NSMr&4`BB!k|xvPEcRFnJNcVswsQIteGr2z+Xwgu!4_alwTqHr>7z=KvJ zWPO%K(RYJEEhA1EL7A^i<2NY1uUH`nt7;cd1daP?pb@2sh!GwSagHcGJ1jiSD|n(* zkCK%n9v|({L$)ehEC>S$%NBE_I0^wLCuYQKCm4b3=o_#7veX^P)dT!Tv8Vlne_RW; zjsGs`#XBxGJSqtD3zYQ8oy7#~S#M0dcfzF(->ug90JjeR>3}`(zq#e*KYP#o!F?`y zot>RkU*D@`yqHoY9h28e^)?py!%VT1t{rFhFs=Djp0oG((vSbE3BcxTYLRZer767P zcX)&OdRZ?gV{-Q2R;QQXq%pLmALkV_7NMnIBy12SP2>Rl7qx!GZmxpAyd8fW50SNz zi&(fm|2Lbh>Lr#HRiTTpI^MF=DWLt*-?29Nlm+-=Z{HKhQG84zf=MFNh`vLwl={-F zi7Hp>9{3%@>owLv#Uf_I+7lkCip!@}XrY~F56!UGeTs@f++;-KRNdR_n3FZ{BN||boO&w%cdd(@^DyVLI(8#vVN0!>bQ>BSOD;{ zYu2$}&j&tQV78R~E5ztXU-5e&%bonJ&FDkGKqGoZLkjGyUPglCr2oO0;3SRg71AfL zmILW`m)8Zaa%Qo0$v+OlMZqFxBI4KRQ|r>lvf`n>QYT6pSvr=)JFqM>vVD}b8-k31t~{(o zWoW6BY?;*W|(WCiW9@wZ4yWJ#*U$C$%PeL{?)n2=%7YYHY+i3LEU zT5$oOMBQcg!T^@&LW!+oSf1u6L5cnCH{N2B-;dUsQ*%U#c1Z0m8W5BA@c;@mmrKF@ zm=F9a-pYOiA>yw)=*;1hX=IVVI)g$NA%&=y6f>JgWZbkLh`Oc(U6I|d_!WR32YaDnOP!)rOgkNTxcOjQKDtYA~vs%+I1(=wr4xTe-ms@=}KU2{qpscxtm zv=3wx6~;7(#7?^18?xo@x_y>gqcRdS?t9NF=3Nd`KG+4$7;%(AH{Dm|DlL;}*sv&E z(cF~K4YFVOBtM9<*&&Xor+-2hh>G90pv%dcr5pQ|SELvb@AZ>HrilV(A`BA~Pr}%` zQJjKi8tbvlO3Erf8eYA(<21u zQ3l6}@~bOIH110h>*a22UzaNBd2aP{+}56{QL>u2(3TpIy7O`qQ#zSWmna}oQkTR> zf3OT6-gWgXmhs$v!}>U{94)w%730L5E`GQ1y+7RwDQ)%p4NJwI8o1%BwK5coGJ(qa z)pcD-jOfdod`p9J)hPK1UcXu7BjhtkDsSY)PK=6YS+h{Rh7wO9WeioezED>tTnTnH zHV~<-zuXM_pfo5tOq_EAml8{h%?(S9Dds_OL?hK(6zG7PbRC!HPNveS5K-QM5&#oA z0XT?Mx2w-H1TCiXViQF#+UvL4>psm*KVAVhqb40v4r^Pf=wtPDK7bSUGEv+1dyN<9 zpp&7mh~MSC?pprt_No3e{mmd8jkSUE&behlP7BIk@g2}zMJ?tO9QzUrG5=UC^-zj& znu$s-U><&_Yasq`#g9ZWFk#3UG=4t0iRCQ*)Dc-Gou~2I829*q&i@T|?CXVrE)4UL$6s}O9;n+wSNL1MkF>)%Qo*a2^3M33 z_2#Ea!TjmFf&jZk3B(q&zrZ1f{_w4R9@{y+&bus({*t}C8x`(Y3ZF)To{p(5zpi+q z3F<)B#fR~Z1I2||kV|DG`Lh)s8tcle2RrT$!at%isi1A{wM8w9U%w8VS*V~nFaib# zjP{XWU;SNzL+x6G`U_-}(bW#yC7pnd$2nkbkb9>110<)>e;qQS9@hiKbsr?vfoYSs z6J29H;$R4V-{Zly6qNmEmLdIsfls6zCsozY|AdfJ5M+3th))IeA3N|nRG`Pg)8Y*= z%a<7U1tPqwwokL=s3!ve4;u%E2Ps4_VTUi@1BshrO@n`j6H$qE1QY$8}-5FU2$e zf)pxhlH>bEVe=xwefQer4M$>}s{Q9h(`3xNcKfhnE?h3M=$|ezAF^+r%vDC{WMiZ* z|DD?jxRvN?TInF5gYD@IYyj!mSXsd*I|FJCjnA)tXD+=&*iJ2Mw}r}J1pvZI^&{o^ zpDT<-nI7@9J&R+qQ^J)#r;dV(yFLK%^y2B=&g zl401;*)><%f7Tc}fpawe(mC8y2lY;jpP|3l#8Oj;lA~ae>Mq0`t+QD4;$QZjsOPU z$q}YZYj3$q(T%_0@zmAK~ z$7EkIB6sf*Vyr%9GUL^8K+!#y%X&weh0OuW6d?Vf`8Nq`8*jaMHE7-rqzEe8PV4+E z*P(X{>aDKDQx|q!4$mYf&_`qBXxeLc^$9CO2j{3$!-rWx8QR$WCl|7DUYeB@%2xCD zW$BcYG>UE?!#RD_g#-V%uv;XMcD{up7^Ia1J9xlv-X`SZ|5WJCV!}ste=_+7S7knt zW;gi)!|EjS2m8-;oaG`{i>sO_%TV(ZC+^1(F{&*cCvhc3k|;reYD?aAi#VsPwKI# zCvRJbD17<#tkJ}{oe%U@9}xbXULY09pS8-%^eOp#2r%f9>t{?Rns?(V{E;llCj1N8!yS^eX z&OKs9B$8LSa<{&bZw&1=$dWmuBBne@<5doW|DKVC=mqqZ4{<>sNmEpit10YL)pyJa zXVa*M3H>|J3)wI!`@#-JOqi_(vX1lg=gtbLc^c0Dyky!L)RxN$7*X6DsWcfKl7^9j z5z#m>W3V*_cKt>)IMa}Iq>x^pc{g3BSb=g%*ZD30CU*S+P}tp#-RES>02w!+v`nea zM($xR`rT=F;8YgT)KPC*VMDN4ECu5Xe7DGD0>1LmVx@S|+yH066&7k7m%) zJbUCWna9}OKa`y~w(H-&R1rptN*4_73lTqt-iRVAJ#k#E1Wz7usanlE5>b|=eDn&W zHi9)r8IdHQGPVyK?9ULoO`tZW|heFJUM*9uRhc_1t0-JZGAq9rqala8tY>*xpS zs1$LrMrTN+lyu+z2O$dAvKNg>vyqEQ_6}jI>XAs{j5gbgBMKYC@|mHrknn&GJ8&u; zqgY_eDQi_<8J@2)dEfi>hhOwm>qf4gP6jyea5rlkK4p6UU+CXluJBdx%U2VwH4C0`1YfU)US#Cx@lsQGTNa^9NB!UT$lvuk0Lpm(!Sg*h zYmWQkPM|{(Z{a|{aoUx@##^zl*aw|!oe;**q4@i7%3o8(a&B*`{wx}E+aJ#=czvp( zUB|1pmo#-RoiSy__ywaY1++qt9T{#6FRj%4P(2a}`Y)~a5g^RPkFy{`G2w;;L&lM8 z8L5Uo%`wSTIcdFsgySrSM)ysnu(7?=QU&jK)59Vt@(`meb_ta1w{jNOP$BX^=Um}d zrt5~RU&-EOSZ*~bXzpOuf2@E>nT<(dbkKNHx0niLV(}_CZ_PpW#a^VtB8sgL3WB1> zutzC&1(G@njtLF=wCj7+Ye)5zpFh+uP{~W79kCQ8G^E;fDzun)6B|^a$c2kAG3el8 z&Q*ar^hzeXi7{qb@ISi$3XnTH44IxfG<@x0UG`)&1%xCDmzR%hR6phwK8LMs0nxQC zAfCjmf0hPfPkwQ1p5EGKl6Attur7mN z>B-DvH42heIv z@4ZQ{k82HsmI|imjg?-py7Wywh6xDSIR)w#w{?vfgxpqnqv$(0>qvm}M!N*miGL7V zynQz;o_zK?-aVZHNG;3&725zG3f6Eh(*pZDPwY=X5v4Db>O_iPIKPAzM13{tceEF; zX>;{RLL+BKSu*Ux;cg(VJXZad3F}PmTAJE`D!>W7uUE2KsVA#?SD4={S1XT)qMgzU zi#-|LO*;9WhTrY){^Hh_D2#AS7H{|9&>y7ateGWmSJI?4){UZ+Rg}aAv?#r#S zXF4F>6oo3LlcQ$PCTu?Vi5ka8Eqp0sPOTh=GNRY%v@t{*u^g7aAEtja%l zHrUv|5@I@~x9u_**Xi%S@R{zr`{F0`$#rQ-cu)hj`Afnmb%~xr4J>SeODE&U8&c<= z*FLx+i3Z}v&X?t#ywJ1b<+>axb%)=%L~r2^51yEfWZN;xaA-R>H}WcklOqYj39lmqGn8pM0Cp9bnt{)5XQclt4Xn<1nn2s z>Rya6BbeAqwIJ`Hf0X`dAgp_+B4)DkQRuid0h~(L@g+AFILo@}Fe7~VS3KAdBswAT zSqVxOZzh0z+EZIw>reDJhDvxu!M_N)=zX~`xC1aN!kdj?n3$So)^S0JJ!3X6K~5Xj zt{UlKG^NI*(17@ZFtGpqPLra43Spxm3>>tta#ymRxI=!*MxU=Cr%|mbc5qArqj{Nz z<#i-yV29kUSS1|`lC#v577V7Q0W+=0Wh}N9o+4D!4p|sT{L609qhOjIIS1lqaC`c4BkoN*FJB$C*L8kB+ za9bKdTO!L{7BlhOKYg#Or3`9mLjJdA$m9<@!hfyVe2)5#z(~t$^@jcFUY-ai+6Q>l zb$b9ZyGLm58j)KY6!~46m@EkYW8LJ5Gp$b+4?GKA3u9$cj@d!)fA^hJGf6fIj?W;m zoSV8lRF?JidDpBao1650j88L7_T-vKfiN(cAy2NlX?8YQB5uT{GO30a7_X}2gJ6cx2c$NWlG92p;4GPfqB5m|cNVB>eEoJfL`AcmAa6mu<#J_ji=R^OtFmyqU(B@lmi z1xd{4fRJH2SiK57r=!|}i2gbh`=y`A44G|LI1;j)D0(pQ5{Tu&g59YlqYSY zuuEWsbt`*Ahv7>kK_$~rgmr%ev;p0&uC8jJ5_th^Dk?E-EJ>Xf4qW~XS803orKY^B z9WNx1IWXID{ST14C%Q<={hK$(ag9VO;v8SKufpW{}wjpNpXG z#ZH?2n*AR?btf+?R;Pqx?{&xQ<;$(5Mfo^U=!9(otSmA!$u_U1IzG#vd>1F~J9Af( z6VT9~?^;nLe8Q%3A8>s;1iC9<$J)QMq3GTG(2ABDp$yjYWS~0yR@LIQpHPu8T4^P5 z*Gvx|(YxEH-Q=>$54tX`h>!;}FwANYSDeX{{6XVj2v#Y9kZ9R@S74aRtWlvWuX=Gr zwLE+=nFQk0eL^p2hac=HGd}R)EPIf z3mt;I@h5TF@F%ca^TPi7Vf~OmBG`oQ_GbU>v_k39fE8Fk1_L0<{n{?dT|XU-!%F(& z{QWwx)q=jl*DA-6@d|}pD?)I|lSV@%xu!Oc01DA>>hCdFvJ0|nd2aN-{(*fViz`$d zF2y279vVn7y^)^A(WqEPVQIHoue<4jvrW)YIu?mi7XUHCL3j3cvCeTU zg#J!?{2tk}y(N_7=lQv`+#svmRZy?otqj5fdEf6rY5t`E7xiFCZyWwqOM(AArBrMeIk zL5lE{5)u^DG_X=5-6jt{(@zg)J@>|m05{65w&mUJ;$ZZdP@MiD@FGxrIS*iPyoyEV z_AkREkzm4oOF~6UiV3QNX-pqZdZ@Yk^OXwPEgRvz1`YNa1-l+>FP!Z2j}{=V0wVGK@wrq$cM za6Jw$fvS;%derIPXIQPRRT&Cqqa-i@k-r_uqrY8~zhAG+2<*j0{(fF=Bde*I$n1IO zx%7Yevt0h@NWm1S!@S1a_Qtz%#Kwt6BG+29qUBnoIggvTy0TZ870wx z7<>r4+gBZlZ!nGJs+O3nm?ai6)p*2qk7H5Yw}X>rmBvH;L}<%>bImF(>CVh?rZbCW z8ifDj)Rt;al1Xu@E^PABgn;`{pATF0l~DHbFpjgS@z zXCO+d4F3Vd7g_(jrzgAU)^Cue1s#l^KpW=Gz{V~0^ zAXZDtaPZroD-v1U=H*y>Ky!$JCp_pc9V73nrv8@n$Nne6a2847u^-DPJnPz!0IE1D zNydhzh|jD$80~Du2lbC+fyF7WPG57Bot42s!2gapAJvK2ED(LEmCT1ymhYYOFe0g3 z1oNtGbamMF#F-Ee5}~{m08Ji*a5%98MR&=*53Rt-;PzcOj7;G38aJ5X0DwIKxJ&@ePBaTlx!ly>wQB3lrYm1xv zwt_t?rGY<+*7!*)$|*4Rg9P5s_&RdRmJe8q!~1G|eCma8qD)^7c%RbW2#_OF{8{}2 z5jbb%708N7I4o>D*Psbz8Ur-uiMC1om`FycxvhutqSBUUfu+PIo#^BSI z5yOv|ux_GYU9(IM7|NYQ@$~1nt)6hQ=94K%=JADLm65-u+~k*|ZXjTtZcpnYthY=J zz9-KP1QXcQxcp9P*!AFPf&u*cWIH*ubvuTc3P? zp+mOOa}EKCFA~WRmgP|zbb^O3EPVMsfF5vFWL)1`NHZzTZl~-t!r8sNt;!A`&%FbK zWK!x+EJIKNQ?)rmYsl^0M8V4Ff`V-BH|wfw=O18M#8?c&b48ERyMP}(G;Tb-^6Lg6 zxL~4p*fgzv9MWkL8=V!U#ABJnCCye}8R-=AbjM$WpqjH$$?GhDG zTCX%RN~P;ZRr}+W5h=n0)+Q4M z^ZxK!0R$DWUZv1b^w9EFY7IyXBdIbN)!Ji#TQ*p#zWpnFQ<$Ma1(P=L^p zH2L#v@DAh`@Z(Ir^-xMLT8VcPaEciUrM3`H?Lq!!`RtI&k=TLmeiEDlf?S!h?CX@EdT6iIg8U>shn0fOqJvr6Dj@r8nZf)kT{e%pdzH|BSVYC7o zCdNII&62Y8rB7K^LC)@#4)Q#m1^L~3XnA0}0jw#vi2MWZCf=`i3aP)0cD!G7ec8-O zMkePpY`4stm=e^WlppD!Q?9FU!fc#2o3kJ`QtIh@$N`JRc>mqWTZRToo+#&W z#N>5EQziWIA!nuA>ffp{C&Vky2xtZEdiR&LmEQZSe=8Z%e8*TZGK*EV7MWML_KD)y z{Oh2$V9+JfdQ5VP>RL#Ub?RwXNfnW_r)mNj8}L#v^soO821_*dd}7n&p`nA$xH?43 z;m~>nr;G727^IYxKI4wcaO%NvKW?R-6tALSc2bsyU>FB)eB`)M@ z@gh%VW`76@Go3ZbzaSqXMdgLaL1+8b!akAEOU_1V;#AzN!oi}O34U5eMU<^A$qKU) zly}kyh;nPmEvyU>tlS#GZ0*$|*k8%=Qt%`~>OPr%BnFf&snyP4QP`ce&<^OC2{E_!Q$#~8J%Y*#%kTDA4<(Md?e+rZ z?H@07Fo5w1zYn!hX(N!*(Zcm@}lrgvv){1m_EZFs8Yg za?*^eZ6yj{Nlkdttk!4|4ekAsTR{G22tzpW3?YZg_FPO=N~^EiMA~F`u8S=m)b?}P zJlEj;h`GmQ$y5W{cm8zgWw|%Au1T9(A+py2hIj;|U(}i36BICuYJl4X85w1=%SssQ z?Kw)O!QPS6ib_PVr--u*^w*SEfa(Id0>W~T`VZmf{yN6(&Of3a`trO5-(p3lnd8orguK5k9yCbeL_WOO@AsK)nN&jzykYd= zSR`M|WMqi%=6v7QAPj`)OZtSYfB0@+fOmbyXraoaa=S@pQ1>QLqT3r)q}6 z6f4ONqn$=Mbnf2snUrn^?kdLLaJa(2kZb0gM=dQ(ZEd2LY4;U64e={0f569C$HSh- zqvg!|bo(enLuwlFdaRPc&(TLb(5Nu`|CP4FI-AS88 zhCp!)<7~k@&dyRk+-dT$7E0LLlpYkJY^n@ZBUPvh{TrzMZ}v4dUe{& zb^l3t?QjQD3-$0oT_cYC-SoHFabspx9m|zH?1Y+swpL=mypTMFq7Bk(elu)&Kl?*1 zC30apg*U|j4W8WbS%7r9Nh(4{l-DP&AUe@#+Ck$1VEF@&jeTL5l$w|%c$xOb;saA7hEM-f!O zD{5WmNtJ=|U6tBxU2ap|)l z%O!E`I%e%GU@g_mfPHy2N`4nut-yH)Gk?*;amP|#RK9Wy^IcCUOqBcPi+B$AoY~H0^}>100X4*P z8D2V2AJ1lc&}Xt&QV62n&Y#eI8v1D(fOe1I7(bgF-QA)gVNU7!SJEW2!m+I>VN3~^ z+j8Od@9tuPI6Yn(h5!zk^y5QGFg5aa{T7f@f_dLph3qTo8v7hTCZlgp#NT84Huipw zB&-^B`3r{wJAXuky3cl_iabi3DVV5+F!Ia<&9{NEo@ZJyjx!aydI z5|+>?dj1H<8;zo7M}K?Gb3uAfTKGp<2Jc;9h*Vi{|@N`4(ZoFN0-F5fhdFP#|EN<1n zRglLNgFoMI(j0avdC){+wBWC`Er`vkv=5v{!Ep zjT=`G_JWNuFLI~?o~fMkeBthMcn}n%h>lqxJx^N}dyqX8NQDeN4Ne}}<*0;m&Z5cz z@N9IqL-`~N#N-2l_$*CyMn{>T(+|$$A)Kph{(h8}Elh`u0zO=t*(V5c!qpAOXqoWc zTvF_I{&G4GnIjbD6*&rRLrl*>!NE^?zYnDJdJ{yKuyA~MkPrKSnhM1=C%Lf5vGb-t z!EPz)B%mQGjLh3CPGtl$qfsLvjE;6A3gCD`)-KpW12}2}tPS_A#bQVr&=*$ke)qe- z^p>~0<&3wz?QJI&LgIh(Z~xc-?b$!~^FR0Z|5YRa!|m;*Ctv^i*DudL`|Lx)0-sd> zqhGU~(^Ocz{&w%Z_a6G#$3AvgVDm~cyH!2(TD7lLRxI;x3jPHsQW}@i{JN`de=SOW zB?LlPc)N@i-!Sj*fBX0ofL@cfy6rU|1}Ihi;ql4%Bf$Q2uHTRP(ZkbE&w*BL80J=w zu|CB-DRq)$Qnl~k%HWIM3#2`a9`=OIEjl*RHjj2*vC2LtG$NarMzx9u*1|%z-?LyM zr4SJIL7Kb{iB=bTPS|&#psSF~{QJ-71@}9C z9!XNeU$DmE304=vDLh=%JUmx5DU4l0za~2J-(*T>B8~yIx$Bk`( zUAxw?9~Kyk0)ShChuG79`qQ8O>hqrWyzlzNCq7XMd%pp@cKx%Xod6tl9e}sL{q0r& z;WF*}PCW5M^V2{5)B8(ff4*|nRR8#*_n{Ac=%Iq5UQuRuyU;)H2bY3>4MNrSDP_?L zTHaM?zEQ?kY#-2|PkP5dC1|D3DWMF6{Y#HyG5Cw0jggErN&8M0^Vshvz5a3B&qRmb zKyn~LyPO-{Q=a9y74g36eknar~0nZGVcp@4pPk0<#T26eM zQYFf48xle!?~&)pAW8JV6p4Qp?9)u7q$Cs4$-4w`;iy^S%GgmwO$abda(#kqHb$-b zgt1!?)~BkHA(c`t?H@liFggZKoCcDWx%QTkj$ z$4b2V{@;Di?_9Ba_il3}?kgt%J9&HC+umkL5K_(MRNrS@$f7|S78 z_D`3e>+nAte%`qGY5)6NQMRj2TtO zw@+i>6VaP25fI?(MCH*Q5`WACL9rqb&UrQT9;~2UhCmZa?LB;)wSG*5EklXXU|I-+ z;=hr(3bw30Ey;wkNZ2pzP(nZz)RtbdBu1#(EHp9VYDiu$B#s16q(Ik37!v?1A?#&> zrvxC51==w67k}{=UwXzfo>BXyS4C0poxL9u34o37rXcxy^2sNgSHJqz2Zo*7XPKmI z`cu3R_U+rZ@sW>wWW(qTRbG9gQblhI`ezrinbU4i1a?~5G)39pT2QXM9u%f{Vc&=` zNM)!Jq94?tXKiX?3jE6~@Q=ZNbIX0c6Iw}r?`SV((+8?xMuS^4eKFBimh=xG;Rd*D z!0QxD62Q(o&INw!2Ty{a*MtE{SD$*sODzx0yeBUz!AlX>)B%g00enHFQne}BT~HL1 zDqDrrXX1WD!fO$?PvQtD*B~8*<17t!;X!6W-Vu|bw1Q4mYctf&Q0<0#V2>0O8PVZ^ zHcIKHT#z(b>azq+2-Z-t(^l6qR)P0S!B9%<3n+lc_G9r^eds~gziMLK(+jQ9g|2!2zXJWE zl$|Ow$kpIq7{?PAzmf`?^1HBnv1`>7^_F$1E-%sk_w{RGcnA& zptzV)DNO#QR`&rVPl4d_=K=gaA-^T|#f(Fc(B9JCw_%A?)<*D_#VA1fq7tOAE+gf& z2x}Cd6o5*R3M%g8P9LL;1^Q{72?ZFZ_LAf8iJmB2BblYQnPkH|>^Pz`77|t$z?lTu ziGbezv15TtYbQFbSKam3U;mZIJ??Q&`PR3-RSK&A103i-aI`A&Y64&@r=wB*Tgl&7 zz3NrA`cK^cq_Mx21@5MsZhGjJTW(nv-UBU}tXAQ=lNvQMloktgqnvoW64w#}EojdP z{xztt@i)y9LW*pDb@Z?r{1ZZ(ZxNzNjGbr#FwXQYNA`B2#m|FOQZ2<^FmN*&FQY24 z(5VhV+M1Xa-3ZT<V@Ls8Dac4rIUgD1ub0NZ3c^pQB`SQs z(l$~-tOx1rzBJ0q6I5-Q#v?f%!I>r>dLyBKP8y@N4d{HBThMy zwbLg=(sO87OAfu)f{A25H!Xlk$H8+_1LEg4hMInP49y+OF$vhcd-sR}ST2A~9DB=I z@uUZUjRzih;Kt#tmzGd~-}=4xy!2<@^)ny%CzAmD>aYH4wfcYK8{c?vNCFbKzf7@= zc#D@7MQii>;SYcKprnRHg+IjJ){G#hbpjize^Z0b1*4u;MZIc2&Mjup;Mk`99__PB z`zYfpXu-b}>!hA`s5DIP6uh=X5hh70*{=%O;!gHCwf&_*!7#X*ALmqf$P=Or(R3&9 zY9Z)K4F3)!j+E=^Wj*DN2peJ1#V)8MD*bd0~Q9zH4wz)_h37;djARsW}-e!BUwAN#SxbOeKs`$@rEi>n|1_{ZtUM7Wy^1`zqdlQK!U#ijH2jKO@WYqc7Z6E ze2!#^%oG%%)NYZ3KWQl}`>gUObN(Xak0dan(01a%pYpLBkqfxWTxJ982#GKX3N5qV zbX>MjOg8xS*olm2{o2j=4SWFnbItoXx>-f4fi7L(b6CMF@ zE`)FmftExGk0I}|lkgID-NGd*WB!-yN8!Kn-vpZQR>HT4$%EDk%z`VT%oPxbADIc0 zSGd#?d#TV7csIz-f1FvUaPH?EM70UsWQIUg!Yfx-^yhA(BQhZl!!-;NdS-XwI1yko zfTNp`S2;HoN~BS z0Sfc_Vi3Oj?z<0s<};t!GJ-;$7BG3hYdygzb9luYXbA?@*%pj8V8%8EzFuw2PrHCL zP4mQsP9ayd*ex?tCvksdP@?i zc&c0A&h_;3Amy?-X=+eJk^)!`P`EAu=&4?u77)BK67+1w`qw4nJ2dI&YnU)`i^%8`FsX5l34m<{kl^p63A+C>{N1X=hAWSW7C=cy zxaD@uHP=|xze}{|AOHBro0q-pWruk6PkaBI4NjGr_=)%74}bXK8Sr%rCxBG6+@Q zINKieLzu0otFj}(kF)#@Z_1#-ENKdV%_J&RLR1u@A82E3NR(_4g+;ldB#De9E%p9s zy8tN*P44Gs=QlfUDDepx!h@2Q0-f8YtsDO3KrU5@94Qk%5k(Hja4Sv&IT0bR@aHEu zY_qpZd*4`SJ7577#Fp!kgix!25st|G463fBR=Y_-#!9tkT{#Z$DRh@=b4g)8Ug&I_Z$q z`!5A<$pP>yU-`;Ix8HudhsUuLhfBWM+T&cjqLyf&s9secnG+V$reIrwf`#OzS(m%! z?k8D?ihb}(@Rv+cARpLG#@_jDqrZ+TDSxFFPm zfg;-qjeY;Q2uIVuOEc8cV-g}!3;u4gg)I2fc`8!*OYpy)_gVI?uP+9vJZTBNcVf~4 zUR6zWzmrFZc<;ZqugKyLQvbfSO;bUB*ck1!{phrhr9_VJFOp4*RUTv47m*E*Ao;-L z`_DDRiw3f|-%P>-U?>+FA*tcjEG4{2MpV4w^=+Ci!u^r!hi+UfRSJY~2BXYsFZl!r z$65?jrFLCNg6vr1q*b3MjDnoA+h#wb5_Ts$b=WC1kMNhSYhaivbjv!dwMc<<3yB{9 zuyi+x^vQYei~~f;bbFrI0@#|Tfn%FWG#R~K*ko7Qh5%rWs!A& zOziE&lX*0^qR3u4^6D@FFqr6e;-nL1_&6ehpN93vZWM$94=W%H>k_W*8VP_n$^@^i zITT>&4|YdTfD#w)7uVKyz2Om;0KD^^@4UkHF|_ATI_V_y@|VB-pjZ84i?Z1J*A})1 z9(dsJr#|(m4G9+2AaBlI^;+Pqu-Z0yTdH*M9K)R%!M}M!3xOM2w#)NHO2J1sp2)r7 zrK*)C0=(iUGQvOtwT#LZ!u_^pyE_f!HT!v(Nl92vV+2kEoU4 z$Y=snYXfP&g(4FLFAdb*n{gv@^q^AhBNytzDB0WdRta*%!6;%joY1QU8*bs7Y7WFQ z8H2oX>B-l=_O;8CPd@pO28n8! zp(FwPv5$T1A>rf(1v-UNLZCbcrAlij69+8JgKJfDsY-9r<*z|($~J(uurv}ak@Z3| zRDo%Fi!=U_@Ye$$htedU<=~(7p2GvY1>hFvs*qC2B*mm7XQ*9wG2o6`l!tztpNwbR1AiD9B%$^dY}ie1BazUqMJBqn|(y>w|vj_X_vWq+tXk~iZ4vLRs7b7VgiDMNVoAYpTzl^+t${HiK5IDIvn;ekV z0TSE=6d*H)q$9YEg%H+St}9Jcq37>FcKj5iEU=VeCF#Gp1vluQv~{xI^4rkFky z`etjA7azZ8YikK>YuGvWz;PUy*9EYT-8QUY7gT=Xn;&@azJGZn5`gL$AQw|< zx&O+q{K~1rTVJhx=JCfLZ(jMzS0;{synn};=Df1;e{aA2_WfV_(wDZRV87~kULp9; z)z*jF_+7)lK^d>yR`91JxWcD{*dds4`aHc>CAHa{050`(AFsWS{D2nazu`UjK1?ac{b;W zJq9ySOeh|-_Vv>$bHS~z{6bPK0^c7kE84EqnrbJd!wk%ci zxvC&$^VSt4pt&E-D{~FeQ&2xCNHbP^B=1S;l5{FS`0h+LxG=$BBKa-%I#3ZsjQggW zzR2{<6Ja7yKr%)e5oRvn#Qq(oGUR-+cS(scvgUv2QuMkGil1IcwogGFYB0Z|Iy$Qr zAjRVRy=XLlfY>hp^qm`Y(K~KuIuT%qQlM2E?MVFg0F*z}<47RJ^VzN$YrA%h0KnSX zqF*l8Y#X3+=kln}HWb2NA71y=!hSq+xW96^|L;e=1@Q3056}9*Km5Z#T(=pE_Pk|H z{?t$X)V{T~wJjc0iAq>PiXyDxscJy#x!Okt9pf22rD==RG#o_g_*0qrwPaQ{@cWYsEf%>3jfco+c#$ z)9a{1h)=}q@r%t1B>#~E2GmwuAaX%^&3bJbP9(Ou-w@oF>+8E~hR+`x3R-jOKQKne10TKT82dsC&zci)d2uD2W&^#-U7yi;Q6ld$xo@JIS zl`L^N`Yg(T= zDi6%|33U~9C;%)LYtStwkAR{0wbK}jaB59j65kjeDfb3Wfm9XI4E@F%Z+!5+`|j(dihqXsUzTj}KD%W9_f&yzhODAQ%< zMLL0;pdC^mqOz>ir-Bm1Oj!hyN{=S5m0B^pC(lO~r5?FpBvn6O*`$T$HLra>o2CG* zjp3`LsTB&`n4p-j5aKAfsU4{G>xF%)>MaxykW81b;4z?1uh;_A23C3!^o~jHQBLnm z;o*LlIFLFVN1hA1wTjza4~5aRK9T1qr$cw$Su~BWNh0i@y(A`ZX5I+3P+mix1VEx! zT_}nq!vbji?&c{j1s$6dghqZx)JZTYpUR!%;1ZA#1z0Y(>|Y|8ZO`X> zEZ|<*s#nCbW8VF!AOL^x2Y+Bk|Gr0i{`~XLH_v+3vw9(j6{}so&|pS2<^pI0Gu}VfX58nt7^IemG;7TT@JsuSo3j+=l;bg<7R0fC=2`zs zBBg5=ODbKGzF6qgRRLmP5_RUU~TJt?pYDQEPa!9lO+1X^#?d2!H;>m50W;H zDjuZC{MyGY^h=ug$>;ka03%OMB--K^-Jq!O&b8@)BpmC^n>>ut1W;Rm!5px>t1*G_~Va1 z$b*eP5oz`BGFnlE?Yir(d+@-41FPswuiRsA4Dhw!4h5L2*ut1ok<$gcmXIjA?ZrTl zKHnDn$DpnE4HMGl(!u>(T;VTNQgZNT+d!qDAqxUYQW*_+I_NUpeG0KpC>4sby*wxq zR#uok3iI5^_L0{dX*}lyL96QZJEsjKjVStnEcT<72^@jgiEMxJeKujusbnq+4uq1r zAoAc`N$QcbhB7XJU|fVGNekZ-u8{&nPONs5Rzb9iMlrDYQ`wImlFys|#$$xR7CGu= zOGyxT5lBKD75AAYp41{HCZMv#a}|XoVCGn0o|Y8W<#8<2n*t$<`3E~Ez@+V-SeQ~o zXQCOn^VRbXrZvJQ0qg7Q9RbNYXIVWyzgR5p7|x5WN?iV#(@s16T}Q12V5NXt?!W*0 zzkgy0{y+89Q_TkA3VgQ}D_}U}}|osrqjX_^n#{rIv&A zK3Ca!mT0pukhlEx_u95&!N#{>IZU>*jb}iEKP4}PmVg`Ua}egZxY^)8RsX)VL8cp+ zwQ5W7FD0{*cn;0rU9f_Px}Zo-lNPNc_+cEkO07xNRt)e_Obr#Q*l}}oo;wTjo8fOV zsr@i$(7AvD@cqfGfHNo_p%ak=5uFkQf)d9>Y1{a@z*`8K8X*{vs!*U6c`qmz2H0Ol zM-pBK6|{=&fb_bb$b@;g^JKqq%roK3&~}f&^rwzKAX_t+P(o9%<84L+eD$oTfJ*CN zE1A3Wz40?ZT_7ZmS<)7v1)5){!e@H)Xgm+bBDb5=fk#~c6US*spkR3TtF;#UzyJ5| zUo4k)ZEU}8P_p!0 zMF(Ifs{fXtO&tx{0zX4HJB7Miww3mLQFtvO!+?_619fDhDp|^Rh1ZW7>CU5ra&$&^ ze?J-e(2+kscGpK;f6!2pWTc<;h700h$#ySDlF2JSfsmJ(^;E=eLciDdIz_*E*4nAr z;`*vw>u+l)UW>R?ZG6vNv;H};YO7dnC-T2k+d}gdFaqgM+D?WpmRbUI6c9MtLEAih zGC)ETlJmr2#1vK+oHvuNohAZj5V779 zR+5p9f|Fsj6_9aNrU$iF0OSBclgcQzuJhAxiPH$3iDQ~lqM7c`aqeTX7tXKKREBVx z0|wXD99sc4_}lx|a;w87dSM`&9NgjLfwkLN!~JE)?LO{Lk4g(*rxJjdzx?G7QQ;o~ z3qi{Wny>yp{_&4*_yoZ_6>6+QNGiVNKi^N~P|G|aNiMCOxGiL$`a%FMR3sfk;beE`P@08xV^}w+6a(Ztd{}3^ z4KSImhGz>{lyh?nEDInc0QP^_$MbKv?Q0kb{K?0j@z|r20JPlR_rCX8@}KHz^o(ab z!<=)@IZF^;-5mL#5U}?9>CGYz9e*BD z)IuVvY4h}j0G+_kp&B$!Gd{Kxpp{@}1dRi!mtG6r(&;B58YK+@uI*0|42sBTbN}=h zlKJT@5s{9&>t#FDyjb39YMR`JSWC8PGMruv>DA|t8EM&t1Vz($@Yk9oex(R035i)M zcO4N1!#6PK)eGk&lD1YR9wf?s^Eg{Z0U)V{=lKFiiG$1yKr@em$ks8C5?w(`JH~LN zFiJ?~0n+oti)?e@X|Gf|RQy8*0G9M;69HH*`-M*fr~0V7y)qK`;@|x3-#qj6|NX!C z$0MEqoN&SkEuV8m>B*n?iJ#cFzP^5#S5R~UKq{~4f@G`zPkiDN8>Qauk?8$XRlQQb zyfygKN~Q!gt(vgZk`2iLj4k*;w~^1aOP?hLJe$OY?}F^R#^gH*{#!2ivwkHzXA(JK z7WlH)C^D|z@>dM51&LZM5Sh~VvDXXIrlZb3N}@b9c7F{im?_o*0&1rHUKNs6&=S;~ z`-E!^dF4)_TFBUQdE(RA>(b7QJw6q*-*q zJu!)RMABLcAwX;EvALkv;e3ImC-hO~v;r_X0S-azFwC$7fO!bcJiTvAMC_z1SU4fUeu>z5l2yjPD-KBik-Nu7tRs za`GwLCIfC;2jJcBezzt6{iOE%i6@?DF2DTpE#aa7DL82}#=iP@uLJs86q<6YA_Jwl z{%y8MvIV2?JrcPMSf-AxlL1^IO+q!ee6U24Vkk{1$O0=`vft`^{9)YB1%GO?L#qFz zgrjh(pB||Nf8SOZe}9(|)3mBT3;r6%M6;|W`!#M}N2~`Om549MWIPU}W2OavPK+z) z^cyXimR`>u6G?;x^aD!T0X!)p=XyfCr0R{+`Inp<$)o{q5zuYNdOHLpQ7N7bd3Qca zob>1`0m#G z$_aoa+m-Ia?PbG#U4;g&xWbbE4)dMk$CxM&K+>?cJJ9$3D-b=*ss3kXqN`-Eys}|) zzrEUW(3rOp!=EQsa})rnP)O}J`3g>(T;PkHss%6x;~uuCmG9Dx|1I+*jO@cC$zztf z)!) zF?T7tM5;?a3xfPF(zOF+^i*2fO`M*AeiAaX@&lc}(Tm2S0F`}n?r;9qZ@=`2w*VTH z?0&NJ@92GWn zwbdZ~TiI^u{Xj%mT0TFH#hAo$08j9Y;sL-5OFkJ9&Z}{xHPs)WJOE*GL~_;5 zV18*9pK=AXaYYD%Ha$9_#A)i5%>0WYMM~_QzLQSoIaOZ{uRE_J-XCY@(u&>s@_L#X zJb8_yoJSy@uak$fK}Oyak}Xf(g@i>wjtdY1EM$~gV!t%$h-`sZ1MG&sb}-O#GKV)U z-K!5akO8k4-m@)f;A#R;vU`2kyWaJb;m-@T=P$bGqEY=%>M=`+0DaD9t{WR0OFQ?^ z<2XSOnyUX6h+IJuun|G1W~(*H(bfvSrP7cf!xrs-w&-BX=hA>oHO_fY47WycAMj*9 z5BHB60AxXzf8VWl1@wHBh=}YxVo;LCKy5Oy zbV+Iq@I4=k3!i6S6OkvGKVTSR9fkQoDo^c$jr|C(YE93b-1>OB@Pu(=h_O=co5#TK zM&t;&F&?W)U{DF|D~$>&Yg4hmpX^hdsQCu6*Cy{9gE>Tx4S)1(4rQNkzn2~#=OBoo z!Vi+(uw)LU#Ew?PQbH2W?`3aw+b~(sF1X z8Jpcd#!(!KnvEqY_TpNX2YpI_jO58Y0l}7FTPYZlyl2NN{1s7v_I2B-Z;s+Ti_YZ( z>CZ?L;g}QwnHuWjH4Tv1Jn?OtK4C;>hO=ooeDPu;{rMI^=S&*?G{8L-Wk5Rz|I$1D z%N_sjh$R4(ovS7Q#~**Z`Jo^Bp$!4v*5>IYxl{beM?P|RmB7_H7u|{&tWy2^5up~( z0j>I%7U`1p?g~jt3+e#HgdlgbLmB|a1g{?ne&B<@XZ*7sTDG_`vLOhtVim2zIPafK zIIx8tq@Z8YSD^5=mZVS}dw#A+-z5Yf@w0fpn(jX;@mji88m9^Z?%AiB49MRvNd@!q zK03n7I3Ck?5iNDvtL1^*DOp06oPkhKXgu=8C*4F;gB#HPOe0xxUmUtx!7!d>mAsPsmwQ003 zNbuuClYv6XelpT3I`)f^2GHC8pGE?g^wX_bz|F(9Tu|6IuNcmWBbNXicbq(laP`$! z+tI%ZwfFtVkNk+0|2?dnIDz~p1ssn6+<4=S58Z$N{XIXhjAagmtUH@uEBVmRf!<56 zqrJYCu`DU^F@kcS(uZG(%TQzIYW?8+_nWk#n)W;c%)SAeENU}DUTp@gu9?kF|_Id)Q|ykEKu+}0J6_f zQ!Qk~L=f6)X&Y&JU21ZKar#|S2TYO+>RT`CXCPJO8+j>6n%*pM+XnhLHR9&G!{a75 zjG|^yd<|fvRwUeq1fm{0QN*PZoSHQcG|mmtj>L?CKhj(JIcven0XjdI9jJy0$F}A3 zR4eAz*?`20ZH6TcX@iW_jK$%PQ+Cb1FP3}bTAc8h{1S-)DCP^%M z7Mv7NdesO?4F2@(Q$_C*x)^k#&z@I`@)IJ%|1G)&lCi_^_;3(VxIVL>>&M)Xpej6M zN{EP9^@H%cT%07%gC%rHlKZV;tH3m~XQ5k#@Ae1W#cNDOf3nS?u8=XtM`y;Q@{fv@ z>Anbfx zgG&1UrSIhgv?MUQ%HEzoe%_x>A^62tgVp(>EkXaKeXZ~X!WQ}^YGO#6q$N<84cGe3 zz=9_5*Ym_x&I@FAJr^XBwo@`RDxo-~B%1fE3#x=kASN=sw0&SWGoJHVrtj!XBFaeH zLRN3R^U1#f+DhQ7Gh)<{*<;sSbB%@npP;?xB`Z7YmFxA(7Fh*q@FwUM9FW^luk6^MJ;tZ_>w-SV7&d~0*dY;JA3 z{(saDNDv=a#0XNp>V;Z5hNa-ou5*YBSXb-!izo|s1Ej&f7MM#34@bkKB&;^)ueDAp z*9Y?N6=(1TWT0eiomAUOJA&*m3^@4ku@fSY+A#|zKFJ#I+*idm1KlUyp^qPX68!0U zgH+YhRp&vznG<+P@E!l!`TGoo34IP@7Gvl?Wp!{>0qv{&U5JUT$1;*r}t z>{z;T7J)ho=g5Kz%cGJgQkMIwTmayKeFp!DqfZKAFN^`3ge_`vRYrjibc@cu@`h_r^aQ0pNj7 zTG;Np@4myI|NQ5d94X)nhz%w~#w05}fK=Vo7ENx7Qac%W@}mhMwFcc< z#m52?HdgvjYMp2fag6vkj{b>Xi7CO0E!I_KJ`G}&MNH`XF;rcNj<-q0W=s_2Xi5yU zsy|l{9FYfVT7)IDAFYCyCJr>0!WQ>G$w*6Ug>Y?o>AUzS9o543WJSTS3sZxhv~?3D zYO^ZMP&->UK}TGyfVQ8I?t6kzpoG}?8Sn8oAOr=#(r<<+_L!h30&0Xbk`k^2Qs_Ra z7~nWObxvOI1qb*+>qQN1P7Y>Gc_16|(` z(#X3U;iW0D02x8RB#lVZtDvM9z$@P^nbch^0$@f#pk2ExA#myg`294TE8iGiS4se0 zdfTnH?tB8UfB*jE-@ox2zhUA3Gqu;h_{A^AL{UMEblbHCan8p%+RUyg+9LL3u)Clw*w9t zL>H@Ncl?6BAew+OA~nzPueSKGtm#*(R3<)P_qj~xrJs0#wkt?=heAyh^L!DNjvrc8(iv)%xZLSek_`5NXB z2L9!t?;o>E-!a07E=-amPDp9|y5Trk0f2D=U{nX}x`iW(`pJ~LYi)pE{*_;Q!Y}>e z+rPD=3BaQs^(g+(_my7%l9#;XAu9drNB(GkF_&nKNx+Ra-uSTR9Z=E`TNr?}0YovC z*3rM3PkGDD%azU{2sV-ffo46WHp9vpbEEArM^B^##vWHo`tykyZ3k4wAwdgx^mu3& zfMP`}{w7d@zgw6gH$vgPbfAHUClBg`0OhZ6Q5uso{KZy4t09n&v1}5K;glFl16EYy%))`GJ&> zCEscI6C!u33DC|72xYd-EgJwargMISe>Aus24?n=3Xw9Oh#{>lK2|%PKd_i| zfqEi?ZU5Sl`hP4H-)3^E?TAaHRq`(}PDyJ+v5e9my~C&{l*AVd^~R9n8+k1^@svcf15Ceh&s z(>&P7W1*0+*W>Wtu`ods0jJ;|i}|65OZKC017MHYMIRdbM4^|qL7dI-xZTtK?7@Fk zsjZou1FT&ns$I%-YKaMuDJVvl;2r7n&-s)%w{P_M!ooOW_S*xe#3MZu_7^IA4SyiL ze=+$;(LqDS((Ezr7K=5D_xHjyfIaSa46iFC0Iyu%eca9_0JH`0u6Mm_@9^iWLiLZk zcke!2g8wz;D|`ROH@;y<|B$+S*^A0o0;NiN1{tLUCAIeVs}Oq2ch=bZT5{4NGPDgn z04P_=aNnB8)q(o%UOMrt;Ss{4rX8WD^!diW)TN zyfneWxFd<7Ou9{-p)CapvzLOqhLF$}5nt$~V_qYaYRN#0l3@Hda5G3ltb-Ht9IjbE zA~Vy-T;LXri>9BaV@X&a&7v}cB&ls>q)7p%{11esPxl*HizbymmoqTkh3V~Xl@C9+ z8>I@w<;}@LOY%|T2ehgHDi;7$+8S~%*adX#G0|gKI?^9a-U4ij znVhRB&H6LfxULp-G}50M{6iLxz^itNfuCpYC_ctShY}wE^4|pRPhdbO=>*h>jy&*7 z?`=U3bOD=uL5FMs6nGsSI5oX4NJcYq7%J$=WMONoU1x{ukpv7JY%{9#d14^A_DoNX zh2Jy*1xPDLHxg=0GUq=AZPTa0F0>}xK*=<-kFi3Ws(ib8`_eH(OIYw{(+u%CkP^7$ zbYRRHUw=q76?QG02rzj#tUXu_=WM?l#~c@j1FlEUq%h21UwDe z5&Qs<1d!4VNud7#-WUw2i6?c&&(_)iNegg%Irhu0TP(ai0B-|Wbo{>I{vD+@z*EmX z|KIIc0>ExBF2Vm7Uwm=#p!@|TiHQ*%=rQ9KCFH)C{6VNBiR#)Y)Z?vlgWf-yjH&Je$^sUlo#2zo z(9&20!=tgH59ff3MTPaiCOSyQz<9v2y<*yL4%ci*0$c^!xu!m;lYtN~=QF2CiJv8i zSqFXor)vRRY-zQ9Udx#e_7sr92Tiib1b?=*sdHlFZK*Md`I7jzPv&S#v)kDOJ8uBP z+5lD*sFVP_XpT2PH31j`vxomrHU;_L7ryX?W_^8qW%&Pw8*X@*_x;J}j#|0+1c0ar zYSsVj9%wCCE3SfAtJ2v8lo0(|PzCL=*0DdSx-EDoM6nS7nK>_R}x@o8D62 zn+4`j^)stkkuhI`<0YlRyyu$8;1ydH+HseX5bb!eZUZnxB(m?PGFI4zus={h8o+%< z2)4O`-o%@5RS~@|^YK1RKbxpDO5Yu|+Q_s5o$dpNjuFygSe2cDPhy%>5;)?M3MAnX zrEIy)HEkLAB#DNh5ER{@L^t*^+~Ao>EDx&dL%dw#%$k(QZkGqZl7d9(Pv2HpvFg{8 z5sjE@lG4FRIZr$Up!z|Es|vE*B78nX$z*!njBDo7+7mCjenJ7dt*xz|BwGhe*&E>G z-+K4|bj3^m`O81J(=7m1{m)weKI0kB=xc#c>Ob3^ar^DJkL17QsD`xQ>S;wY6`wTF zHb6z&URn$_K^@pG1TQ5IEhNH9brqguicu}w0<~bTwIV`0W(@q3;7^Y|((x^=cg%ke zfmpR*1;!YWaKzqc2%O9xo=%jMQc2z1iG_Po1&I8RCrfzKVD-Tp6;k z|E0b+XyQB^-seH0b2x8&pJWacDma#iOtCSTwg`b7M6PWpS;}<;HOGJaX4*j^V$Irk(bYpG7NvA*RW1y%w z$&x49YDhIk(mpwLBg_h5NYIajyT3wHH`|*DwF_FI)IuU2tCT zf){M;+O=zwuFaD6zqGgwpTGR{r$4=EpSQaHG>FsNlIY31fKP2ZlU4!!PHlXv6zEHu z{QfnQd}F-N%~V@oPy3GCMwhi)lE?Y`__F1GdTzv7xyUj6l zyX>;dcF}FNzt6CFX5f+1C=qrp^qPwg&{FksGwG$Z)KoYz-AA85kT%5(6Q2^IVC|4!WE zrm7j}apa#x?P8O}4C>IICZ$Xue<&>az@hJ6fz!wT?!<`#UfE-?lT8D-Yk_xJvcnS} zx3AQ|FFkkCNhjeGp74ZYDgn6g!V8a|R1^(RNWUA=yoFa-%=QU=?ZPpi=|1v_wR>+S zEJdzGx<~?TM`;Ns-@je%2a$;xB}f!E&wS<(zd#dJxyoEJy=chmp>`5-6)}Z4xUFh^&V|FP__@};#U8FhEW{*Q_&#aj z@1@70Yb^jSse-;v0to<`dAF}8W#^6UfvX%GJ-LhFw<0E5ZU4mNp*V4eI zsow$Q!mFjg??3a>OD{!$oio4KiQ9=Mp15|-Ip_3au%D7Z637kJ@|Y5Y4iW`GHW>i) zIju#fOes(=1t_%+(|9>%E{szVIRY9?%)v%6XapiBCT}m{P^Se7 zH??vf=}{YEKRpTpc#B}NX5CZyotN)9?|kF$D<=TI_G`apq5tpEo`3%HpO1e0kL@tc zuRbiE2M!$A{OVV~y0pKK%KdspIbW@MWjXinENJp7Uupxac#_Nt_H(QARu{mz)qic< zT9b@%&c9!p-m(3sBx|es+mgUu=qoat)7CM+Z2y$>)BSdM!gg9uvHqlcu!CHjhn!0Z z%b&yjJZSUn_SLY^1~4)#-6ijT`qQ7jFvsBSl1nZzKDax$-_J6KAn&em-_A6PwMABK z`+Y{y5r!Io)JjrBr9eUfqUcZR$=h`?(dxMUw5gguW#m&@?2Ls?zYOhBFP+P;$y7%^ z26d^8n@NBgZM2U|)aLLcd2K&>j>m+I{C5A{WZyuxy@t=svqch2(ia8Ar=c`9 zS1d=uXjYF*aZg801U52d{8M6S1;S50o4%H|Kgi=r62Sw%f9%2wFFfv8OaLys?6URP z+n+{xEvJXV|Li{MG4qxYO|bk=cYRQ?B|{;4uFVif`Ev88F>r>Uih;Q8t%UtJNJz|x znwtPgC&x5BZYC5n^fQc!I6g-Ke4^O7=@Xi4CQ}oToUp~X-}}VFt$n@GtL>UIX&i}{ z+Rz9M3Bi1!s!3^245QNbwz{>7+d}jP>F0xnD^Tq7gdlxv28Z#o`eN0#ou zwE)=CW{d89g$v`K>xkX2mv9l_|}O^GAvc_$fQ9% z+nB$6%J}6LfKPT}?>~Jl3dfalDp)c_Vk_|3XR=7OjYjfnOOZ>Be&kU2#k)=P`t6VaO9{YG{oDTEM)8L#d*k7UAKtv_ zrkj>@G>caM4JUvlcr3Y5a4r{LkiJ@N#(ADq7h(1)>4L}3DHUYpd5eh-Fvo-rI zvG3n|qEbxu7{irckaR@T_1uA99gO4%)GVk}@u%ZrxjeAsq(}_8)j&;yznFL?AZrfw zvw)ohdB0B6$E8GpeGX89;nSb~baSlUcJJQ3xagvbdaC0w?T3i)O@F&Ul60>{K^!S* zlaT7Z*vA(3V^DLWfpmiSyHM68$133@4&p+!~&)Op6e_Z`9`EP!Lefi5@ z-bYV1uO~0`yP(v&mMY^K^RX2k(gOSpRX`|J=vgqPy5p%h3!DPxC&WMPWs~e#}?Bl&`$Pk9{ zOF)zY_Q7s3iGXAk?~{yw{$i2(QBb}EM?%zqhDn8=w&lWg zRp?*t3=*IgYOQ8u%6q~1RzO3qzp;?a-&Ys~ghbdU{Vz%ggaT5VtUJk=MtUBRDY)sH zV&j0ej;@1s6zh2U*O_X5ITEq36ZA+GA;mR zwV!|f`Nw4c-`|D=Wt{}^(ECrA((y1Lt?ZQq${?HBr_et`4me>GHPUDz_~hOOl(0ET z`-TxZhZrQqOc#3yJlRAZv;{;Wy?5%UDAcOyT5BPBoU}V7G(tcpDw6|!t=<8?W^K3X zZ!h?(NQCY`jD>X_W$h9AX~59{|0c;fGednaDQnz163Ty{8=A-iC|UoW{DHsxi@lq; zv2p^Sj{lu-!U^WwbI#F zN<9^U0&(%rtNbX(ZDgSyG>V9kkwuBJ#hlEX3&=rH2IKhE-k zNa7Np_wf#$W3B^aBYn}UCPhS;oH9gbL8(H{yctBKK;RM>PsD>zeM#+{z{d#YI%FHz zxyiAq?DM2`hqL{=yz0w7+rLIk0wxV-@fwNOL(aEYtxejlggU1!YZ~2JORwF|nuv6J zLHo_s#7StkAWay&(qK4w7SQ=Z!cEk4KKO@2JKPV}P=Loh{;Zu(0G{=%XW72rh7fo% z-GAQOyz#~x_nD{|h=r=T`9ze+Xqts^uW+C?X$g)KvX=|DHqih4-d& z|D%ClPkR1a(b_jS{HZMuYjcVc1kc5iv?R+jnF$d~)O(;zT3+=_vaif903?*cq)94L zqp?thP5}s%{wB~oOnrbR25jH)wM1p>Qw@UTxMHsQC}~1CnPQWZ3v(4nZa3Y!-p~Zl5OJww zn^4pNa6M)dRcqN8B-Op3F{Lq}!kjg6)irfafG=!=UZ}NhTTv7Unzmg|f=u9HK*o85 zQD(rmV5h?xdtmNFv`=i+5`ae?f8zWEVEFt;4S$}gJ^x+b^<7)*>+74Wn)SpqkhK5( z$$9Kp>=F7pVu?b)-Z>r!|-aPE0E?Uf+1q>v$YR2#tFN0l@qJ0H^T z@CXn`0TKv5zNcoulyNouI-p5CYotY~&B;uLn)=vG^40|uw}v>=8K)BfSsz<)yIg5K z+~yj6gm2~OJe%-<$vz~HLh0)iqNR{*g+Wn6dBTRsfm?qdllEW^*NXUW+;+=K2*5up zed>3A_jm78j(>4B{lNzx+_?4DTh-=~zpzRYy$vT5ol$Qx0y4q~Pwl%kHZKj}wZN|h zZw>zE69To1DOUTDx$gscKI#{Pza+a%JnoZmMNOkYSUiJ-Znog-%wQyoApyAHf(v%%Z47DZ!6dWzob)EO ziP#zu4lhGZq(GXr3VT*FXs@rR6T0}y`~tbKCTCEn1cpL@VrLyD?wbPgvI{XGBGbQ3CggH>WdmgzqnxWM{Ps6FBtEgl9-QKn(tp zgs=;$c-(Qvb&r7fZ_YU5jP;XFIw=NIL-iBrf(hPRovS7SPm(kghePi^xJx2R0Mr&P zJ(s8yFs;NmK{cNLWIWO3iPL}c1)W#kvOfgBu27}vV?r1QldBJQEU}sXf*-#m=!>aZ zt+xf?bPbc*uC-)h2igQp!M`Da=}NDO=pg&To^E%gC6F9jGy?i%hJ3?~N{xPk;MaId zMD?B-w0;@>xv%mopSPn4fSpGk7NkufuvPmtU;N@15A#Z~QND3j&xt1iB`91As-RlDIvx_&MG^KE#oAVC_KN#5lrBO$Y4m21g)hYz2LHPXnQfr4M z4OK#-nUv8ot$)*J@Ux^nF9mud5ZP#jNgt!Ve$6@1KDKAio?}Y;zXq#ba>*qKX)fk| zn;h6HMo8j}dV`-r2B*8(f4UupLOp)kZ+spyI6ulsLnbl`Qj1AC9~z2;@$p5s;7@=T z^9n{nGXZ!$_ByxV(VnZhr?w&iWZi{TWY$}d6{*OmB@Gy&KNb@S#drxcyg|zIz&&mS zr2JP>MAMy1w2DAeb_OiQaxDR{Y-3gbfA8MCNaz0vOpzQ&emi{l@Y1sXrJflN4s=7(;{iO&6au|;JHPa4mtA(* zey0B~9w>gh>86|Z+n`|;z(`P3M__nB6_O?G03wn2MK2F*;S+5;YwA~}nS37H#q)*m z-87cFaAM-zi=>{ZPdzQG@gKx~dK9Z{rE*B|Qz6LnAe@1q#h0z`yO)MOSLWCZi1!_h}r`!!GG!-LS__06P)pN1}UK|+vmG??>3Kn+~bZZ z``_B_yz|bp+k1FChsM#rc%Da8J<;qBNwRYcP4=r#!g;VIC=C$xPlR-I5V;u>8n+=B zJRQ(v^46Uv)Qu;_kTCnUC5}l#RB1zkJfVaV`SZ8Lif8aY#KJRC+#DYl_E9)#kSqO= zuVF+(Aa={D+iDWdd!j6gV-g@K@j)Uq4C=c7pXZ14r5FV-lJx-T9eYUpDI5YY{ILQywjkGt!WR#(dGzp_hg%VV?W(A&y73Y@614NMAcb z{;)+x{m{&I53Kd^41rEidR@m1QzZD?e+CWOh3`!gLn7BLwH83&f3WgYw~-abu^_k! zcF}E4g8exxuJH@}{=${Bc=slZ>OAc%K8~+oH3E}AidV3C=u$I?o=$`YtC@6|bZ1+|jUgF>66Wqs3=c{5EEjO~Xo z2{g74BU+)9$bQaaMYjL8&-rN*GK?}qBk2(uw@!k4pfLO$QS!vbkZc3t3MD!X5y0^d zn-m2=mjpbp3JNeg0T`+gbJ}UAwZPHYMg6(YeQtwWdiDu1Eh_nKXGBDC2rRvZ&h@Qq zrAvcujeN*Re${b8t!=??ToWL$OI;%5NBxNPXA?I;bX9Cipg{N>V!M{Fpln}2ZQfs6 z{A&A>PYmSG<%_KmS{H_p&PI_NM+obf-al^}Xn~)$4D7=fTyVi7D)^H&VTKCvaaJ~= zqv_FuL=Y6tNwK9QsSGB^wUb9gc|e!O-d9h1-KO#Q@leB<001R)`5JZ5w zMEP_9-M?#k0N*nw0eEHs{x_`uufgES4!i5FyAIuZ@4Z;4PFr;Io9gB{jR zN~xD`gSVf_mrRHxX^9I zETpQHpZE4q5uT2Q1OOket5k2Xt+-f(_aw(|R9Q1(?aC%tq?S_{@hhPKK;(i`9SeL&1aqLE;FPoxHdjFa zBo2TT0J>EBl#4IEc#9JN8Qd)ru)g@kFCOq0u@6?X4?I(op~Y4bBorVn9_?S|`FFzN zWt1LXaH8M_vH{R>J&-1_ zfsX*4S>iI|#E@${fVb3pK$y`VSM}vY=0duyT)hx6oLs)L(+U&RV0)7~T8f{+?YN_9 zB}sgp3~plC-E+^5TtTxPH&18iA-^giKnyxaFb_FMH6E~OJ?4}oRv`G}Pi=e&z+cL~uvWML&MQSYIrciOuy!C1x}y0DCLY-hon;s@=Zug)bZyNPZBqNT~K> zwgVB3RR+(d=Sfu-Og6Ryx3y-MrtBpylE^S9{bdS&J`r>KzbaeIgDNLHN!!8o1Nc7X zjQ$XOqhp{cWD#^bpd`?vLH)_+5D-&H7=X&h^7rPWezB6nR5&Kr%s{`5=z~$%ZaZx= zU#;xexSe?7iHoz&I;+3!w%a;>z6gXj5rjjM_>EBXlo8U_P(}`T=sj%l8q4fWXL*o?$gd|C0Xw{@22z~RIhh3 zZdhyc`9d2F15_XcG4T`@I1XlK$bqHxbI!y=wX#J1Dvap@8lID~g~ig8 zc@BiQnGh7IP)cdCL2Fr1xK*0Fuem8oiiPyMwSF#d8-&GKPkeZwPXoE(ai-`Xa}L zs|`k*V-M$dCtw$WNUuAVA^px@6yh8UZXpBwq-0`D56%2hyGui!1W z`2u&Kvb(b4UP=I-uD#n508csP6qNv|1*uzaz4ZX5eXyAaV16~Ni0inqw?6OtC_+3^ z@0{}qD)OF5z$zIPY1`iL8c~gr1%LZ@`02~UvW!>kB|Bbi8#IEHpq!UgMpC8D;Be5+ z5k;J*j*L){5oKtekZeI>TyqsUlM4Wa$*>W5GHx6x3s#ZWL$m|N$A$#p5jFqMZx>#8 z;TkpY0d|ahWz$i(I;iYHqHq~TGIO$CHb(3PeZqOhAl6{>+ks~EL(Z4Z2RWi_7#Z3V zDimuz045A4Q zd6Ul8+rXtIk}(!z+Ure6PJu(?W6j=YUzM~1>|wIKR-2lP`Va&nCkR?5>PF5fOgj3; z1U&(x$Y5ucB_gCf;=+0%2yl7MLu~|Zn}%!?<{y>vS#N`eLYULfZOVXxyQlG;2c(xQ z(gP4sXO8sCZ3#F2I2oY%Gb^tj(RD(^1x(^TAi!%8kR`w895=kC5NmM$~1{W^A_+s-&zwO?= z8_qrVTx3ZZ_~4)SxI?@Ejs$VEhZ^_r*ST{iX)(oP3`zf=6%8P%F}kYUS@MF3+W&F% z6HKDelA~3fG=|tUqlc^w5YWkld@HPDl-&P*ecivu97{09cwy zx3`BMet6^Fd+#k;PaZ{{(E?J0@q{4Bmkb)TS=E~K&Tqc?=7U_`umBw&(VJg5tPCzk0U%G>s;+|?AA}6tKIf{mu-zT$kxmmRCh;2fbG{wvnHnecI$Udy$oYRE z0a)ir6j$xzsE*|HXB5*Yrmujkx&}eB0jL>}Ao7%>0ASoPa{XsFO%xKkF8+6XEc^wN zsJoSYAKK7v$T&Pms!Xcx=|8?#I8i>~-c|*kzqgqnzT!8)3dsmduJkpvS^$c_Bl226 znW!!a0=a*S&r25U4ttDaM)CJORN4IVS4seO?b>BYfSs_TfWZ3M?JHmT%3&JprPsB{ zRqo_1s^V=0f*cLlTHr2Kt`%_~X!$H(m1+|Xsx>fy@En_W`fbatOm10%`btiI+LQr> z6Ir_;+jgnaQUgCJp(sSp;ZDPPtwGu~O*BD*O{LEuRewG$(1{H3Fd7CXnd^?*Nm2rgGaB!;Yz>d$IaKZ`yBoKh%x2#(~1B!m|R!CNX2eN-Q9imz^r;nwUxV&Q1 zEQQrd+@wg*lte^Anuq5pT0lu^bX&SECot9ojwHB0D{K&{?>uk?MiMpR=j%y5Yy+L$ ziHZ(nBm%epi50{`ds!dnIPyR(w6RinmVY~HNdZt9@1Sh!YqKkS7+NR2fsWorZM)9wu zq)!$Ik*WmxMVJyab{S<_bCcjZaJo{ zKG=2$JvQE_HV+D)4Gmwb*&0Ej6bKEQFO5~^+n4YIrsZigvNFqMCJQ|0wzrf3s3Jh; zpMSp1bhp?^RJ4QKw{PFp9e3P;bg`2HMS~;L5xI?{alRE$3HYTg;>bY@X|VQuf++AY z99q?m62!d?{1~8rHStI#bv*&F5{#?u3P~Zclz>PCLMrUzTL*Mp%vV5CH9nq02vU22 zHqzp>K>!$1MZyHfqlpL9; zEO1+W`lE%|&e|qZ=3~v>@1$cW7VjXG`)IH=sgPE6kiZ1O9kR!qOSseHxUnh%U>BV| z+Pj|onl0c(1;smMA50wf2bgnT7TbA0Y$^n~s>*V!;Vy zBL&wxyD>LKT}Pl{VbG=s$T%Tn>T*$Up6ivTq(JE6XZgA4AsHC1wIkmDACmLs{{8!x zM`KamyLay{fwi0j;miyv@0ABsD2zi1Rla~Ogh>r%wq#mRfSPxM-i81P2@?^rhBkn2 zJLGE(A(iE8h?dY}M#|J&QjbtluIs%xA*%)b?C}a3F|{$m$RAK>Z+blyEJ_iJ427zo0m}EI#{cl^ zcGdROR3uB|SRng%uKtxm_+4_LMB$QdtlX1+OP5X*Q0SSGaWthufgot01EIeST*M>_ z%nbh-1(r=NcMF6(PRA$(0$F%Z)7DGHPp{;cid-Iq<4 zlu4)o%;EwlS`)PZC$uq|+W^A#0U3->bQFll4D@K&loHIT`aw%pO6%DXg!^m6!Z@~z zE4{zCwx{5sIMz1=)6R%$;{r<2hz6K&G6xtl2sn8ttTXc_0nVnA05m$ETiT>2KJkg0 z6z(Tqh*KX9GD)LbZnjwfapU z@Tnq4o#?Tikj12|rjynG#D(4Yce|>ezTSNmsaS_ZyE_^|`JQ|3L3=+M-(`Q~-o1N! zj-K!eo(D{NoLJQ^wF-b&OObfjC66;CM??Xn1cCZ!2&0=~a;OPpm1L@gYffypP2LfH zypKzk_e`l~T-9KP9{w!!a>d}i6QhFA{z7r2ANblj=UXHj6^uVCClOT$fvPE+No7e0 zKL&(Ex;6=L?ra3$%rnn~LZHf$g5hH6zxK6T`Y4y32ZrQ=!<7$uF>%NcgC!g5b3!>A z8yjYGV{>c^82Q4ph&gIidwx+6m_DuGVHI2BDX?RbL@Wu3aZCs>ml$4RFUjkw!aovO zGe>XwY_Pu8HqhD!r4|FNoT$+YU1&ql2a`d({=(6uV?*WLWxoL>Xx9~|S@kGYZwV~z zK96qqG?v!Iq`+^B|3P}JfQ`rr&+C;x1#)oM+f6YO%|qPp56z33;!1@d->$y)pf>m&=xdF z+Bb*l(}k?`xsuH<-%hEGlmru2#RZ-NId#HfY+v7FQu%k#W%~TdwnEm6110EZ6{_RE zgboAa4-_ z;M(YEAP8cT>;|i0_F5p@7`BxrqegHsq_)H?QARF@%@Hue<%%7m7m>CQe9-W=X`dUH z6%dDvA#5jGSOHOT#VfW0jAfkv6^6s|Y5xd=e_ucTo0}mXPc7weQ1;pB89`hJX#BvyE&m$s}+8$;8gE&5Em=6R`u8d^d zBtZ8ya$kw$r?mmJ*BV`gQ#`jQr=7MZ4b$L=rbN+7*c9xqS7_tVm~qs{ROzHh2}LLs z6GhYj%KaE)idHeE1ncXC^Xj9QPu5k9*%36LcKb_F~~d-W5a_(VGbxD=TBpfSKL0g%JD{V zF@}G2j(i)s`RXz9tpRJ9Ye(aj!{7SJhC+>RToxITc`@Db3*0Df-q!=mxZhL<81mYB6QJS!!hheit+~^GMvC@Yw@heZ% z+M+EfXap|a7FMc6s4VcIN=y(2SGw)lv*)P5{|`O%(9$m+4?g%{f3y;S^Uptjks}{q zw92`2i!v0KabX>4#c@!apv4R$Pa|#EP!r zb)F4CLY@qeUr#~2M?PLGBuj%M+LkQ21kGA(0`)@Zx(oM?CL{HfL)YiTgIv~5Aw}+zwvo- zdST=u*nKzc<0UaL>P3wNuIsI}E)=t$%(#bbKa`mM4X{2G+6?0LA}z$ysAOEdrYwL4 zb-oy<+We3SwV>wc9fOrBAYK%io?FOOlYp6X0mAy&JSvHiwGTRKuitvp$MxTl&Zzzi{lLk3 zny3L%`2o`MBms4Q^X5p7v4;I+P?%OP8ja^x`0Rscemk}Uawqe7T<&|K`h6Kr!Me!z%co8qn zCfRI~&F=rN6I*%C$#WtjBO)_TRsZGH@(3mVU3KczxnyQUd=c?|jumnV8U#stAu8PK zSO;nQ9-@R)#FD;y`*i;%>6*};4#-0MSUEdmHo>&&SY!IrSOJBfAG{)d?i4kV7PA@5 zoR~PH5b}@~fI-uJfTCf>qdLbyJ(U(%QLfVr3oQ^lefso)737^gdv+Mg{oe<+-GBf6 ziE}iz1#$*ICn^4k1%MpELHcY^WOM`x!q-v&IY%Iq1^_)`)z`^*tY?EHaFXI&r4cq6 z^Z=^Y+Dsq@Q`YOk01qsOPH+LNdj~9VH})|F5d$;>Nx*wafK% z^$o37KxcS?q7AeFFEZZVI4{Qj{5?X{(~=FprGB6^LxblBC z7N#i+y(Mw06sG|tsyTJ))XqT}Vc-8B&gI;>a|h}G+;!Jo^8uL&&jf}V4cb1L6#I=7 z%XTdQU5#fBxZwt^)`Yb0nY6vib5CG0(y}EY0z`vCOam~qA}(44mn}}rr81^O@-@-* z8oIbjTEFAp@&7KYH<;qkNbrBjc~fa-*@mL&=>DTARO>a{#Wo>MD2i z=uyxNzmz9>>Zzyp$B8WG_-TS8H&99juLe1_ldwm&n=TE3+#3vwODpTiBUT@ccZF)B z>R20I#9aWbvYw051weCoeRsi7a1c(n{Y`XjUOG^D(h7gUn6_2zklc4v+yTaFE0( z6n25%%=MRwSbduBDRKZL4gfovY&ifH{RDQD?7&REXnB%u!k~0v78{?Yty=$+dGaRj{1!Rgbd@v+AqgU}@u){3rGwL6XZY~IV< z_M}ZXphZZ>c=#SQXp!;7!TKW@>w!fcl$&RQP0`4Ky7|?w*OCJ=IMdE^`L|6)H0V;h zX*48(%}bO%Q=;Jrk4Duwh_hpB7$OHEA1o;}VkQc|!W$icA(RZog(*2y_Z;Q~O6P7z z?n)vAK!k+{tDUo2crug{v=c?vK|597v$GLNLVPcI14c+VFnDx$rbnBKplV^w7rfWKkf%%!%vpN zuyi85@WKn1qzEf%aWkk7r{t-O6aZ>i3#~x*=i&?h0+3Jp=>0p*q7ksB?z^;LL#wja zP>W!+u!re0#6i;NHgau{&G%cWT>Rfd5XH=Jg2Z@ZHW~rYHb3X$uR6Ari82S{h)A0a4}< z1m`pc&RN#Pl0Y$0Yi2y?4 z1lXugS=(CXM9NwW2Hci7I>5->+)Ra>mhouj7osiz6#-Z-3~CBM+LjK$WlHQaRpyuX z?bizD*!Om1)uW<7C2e~{eje#W1H+T|$UuXOzOtXRcnpTBEeza`S^Y@99;xzP&heq| z14x5CL=ZOF5_{i_CNhx*@IoDm3|ZG9%^j^$SL(f2i=c=DNOb^$bKnMW08Jqp+Or;W z4$Py8?*BdSP$rh_fV@ojSj~&p##AQq(vToShafCv;>D?Ml+4bW*DXL%o5k zU0>HuGIYh=3H8}U&pAf%gE{6aN4#1J+>uPPU>#x6lUyP!a zg*J2mPMUk~y*KCrMC4d4bTZRr zf9c=t`G*4D*?b@i3Qjq119Xf#LOV8f&?Xua(45=e5db7bfGj3R55juOFb3;Qi?XgZ zJWU=QU^QL=h+jHSKatI@HTKMr1Fd!p6o0FbGI$8kb1VX29!h?mD13E!?pxz~2|H^p zXdhAlH<)|faKmK^`RBIhp8Nj3HpqDXT`ONKT-Y7St?*n^zI~+WI_VQdyn=}WEQ8?< zcqhuEx7bOlb|F<0CRf)nVOHcq&LEEYtZ{;d)B=Scoh-=!nn9MF0VU#hY2s>Z@zc@_ zA%~UTBXul9+62PqJlt_ki0Dvok~n9->gH!!|0E83-sgVjop&A>^xnh$iB7?bFTQx- z;x2dJeK&mTTi>ElXsG-joa69!P*e<^IKAVBbX=nA>r$4(s1goKYg@AG1IZl~{-$ZHdXVyow*6l2j z(t4tLt@Z$Ng`H9I9wNdWo^e3Q{&$6hcIg9-i*J%r2pvM~ofX>!9 zyvefxZn+E%@O9T+w^w3_!pT1S>@$rNJrm-eVK7Ej@`mTB^=pRw6Z0d|b0d=whhmvo zLL!1)`F>%09S%R-Dfd}@^5{5ab0!~WM&>13(?J_PPYQCie0-~NJ@;(UNS=*U$xpgJ zc%?sRQo>UHpGA*R?WF;k%J8kwz8G5K^fe2_+&z$}gc}FV5uo4q(f6&l-g;p9e}8^w z&z_y}d-cjIufPFFxJKLQ)2F>o_&%xD7ma^c3jk5d$Vcd$e2Y9w7&=Qxl-)YN{aU~| zN!YVPtAm{VRfvjV}S>A{YF^?;_L$HkgaeI6Fz^v|aZZ>{=@bl<90S-(PpbspzjvhTa7Ff;z za=CU!?F@=wkMaz0Kz!nexK9zdI4B4$_*H5hl7KHg;-O zp|W-hcs@Wu*;FP+ATw2a)`(?ofEiR;vpQ<19v=kcP#(qKn`e%AIddtk18|MChbIE! zMud{m(2HI!B74drsljV0ev}pn($`odD5wTH%AstGpJ?vPh;eS20?UeDg6bc3__|E& zpg0Ar{SIjzk7Ptk48J7n%JX&sZIq5ji!Y@?E)8yp<&Qc81%*v2rK-W8COQt9ld5X0 z_tlUCcY`rLwe&(-Bo^p}9p|@4Ng43esnZ7vyZdVYge9@x&Ye5g9H`}3Zuj4Rf7D({ z?-}P;=xY{$qILnQ=HDBR=g1gjX|7R|XDogTYe*bv5}pm1&9dn>6|JdtwARr;*m zFpmH%y>#s%0*$hI5ui(g_uIF;8l3<2(V%f33&fi z@yMa`Rxl_MlVLd;=VY44>?A6Q_CsfD-&5rzegI1ozC`p#QiKf0&V9FNArMm-VC@Ii z^y{HjWNm-y#Ei*=taijS{UnhWh1bcGCl5sZk2vVaqDAP7@SyDuINq)}g*`$v-RB?K!j3UGiM_dZ1cV|0cRnkJ3~l=XTEd^}5}t;IRH zA%o9#MhXlpRIlK)RfCai)m)BIx)1twk6c!5X+O7cU)c?ln-vUw--d^cF?+NY7)kb*M%a;210ES7A%t;iv|FQIm`f3i zERrMjX^G*dA!>=?m!2yj3qr|3V1yM>RAxo)l7l3Zrzij_LDjyKiA?Zz>=MasiY9^6 z9SbBmWU1|t@6RQl$$sG5;BLI}#_fYNxXby8RQi^-m>zT|fZtA^KE1>49q`sbDIGxT zM7Sh0=WOUmnLrbO8Rk4=6O86;gb_$YUp(khP=Lhkm*|DZR7ErplW|lKb=R~kt|i)E z@n=A}7?^Ni7-M08!UOb*wIDfZ!nuIL?XRpu)!B|dryq|g4*#zLJ7f`nLx&Ez?d@%} zyg+&vr~{BY6<)n+=TGF{Pq+ev$rs)PAFREN*lbx;R7b%x6#e_F&_;uNUk z^9qfW^=SrvTv;E*0VI5SM*C@a#MGImvqtJj4LrQ!os$U0sXd?ig1<>LE z;Dc$oz$}2|K+~H{ZYw|(Ns~c}68Vi3nHPJu;9^3dMv0=k_|Va)kjfSd7(k@r*HH+Q z1tdlkL{1+N!0OSVkQiA)1 z`$-L|B6*g(?XgD|& zz(#>*5cIFC;dc~0>gyXQLLn5BXa>GCcWHzyA8`i@m))(9&AVaRpo_;2^e82jDoBvL&Ib7%%O40GW>z zYB!B$n&trpv1OU|EQE+1bNH`>gjpE2kR}1m5zaZ@2au+nWKK0{@Op%SvC&@n%Yi9g z6QZo?GdeU_ngYmeM@QoNAXs|C(Xs8>ba^V)2@S46vLt zuom0Fz%;r`hc07OaV$a#uvGV(u@7RV78+#!BP3=nmr^J&v?@YVnp!9W@F0sO@CA@_ z6;P>S=LRHZfbz#Vg(jq2P-TpT`?(`Wj^K$CCl0g#a2fqKu|a_(G(6}|0Kc6)d2+{k z{*o5NY&Oe6>D-XEAi!+ws|Q!kaOt?H)NCY>$~=+AG zP*#gx{Q!xs{>?Yv-1k-KLJ(zMc;MDA;rcH%AnGxAQwOuw5ps|z%p21QO%zQ+#d(P% zruR&fd#Nl`p{JPl`E&H5YjJE+Wii7U$o+l+q@L|d<^^dvN1#MxNkIA{vKWbYMU6Zi z&A~QF8FEQJ8W{W^XmAg5^Uk?+uc%|Vi9*2L-QC%3x82sTdkGmNK)TN$Ibb1$1EJp^ z#ba=^3MAsIs-$lQ&Em)gO9Qxw@HbN@lQTbS7;7LqXXM|wx)U@SRZN#s>KUyYk*GMN z#)xb2zqFr2(YcUx0Vc==`@Ok^xE2oB!+t~^HYTTekl+ybKIil2&tFa!Vg|j2Tub1{ zFNxZ<1wMiLODgGEVr8-D&O>g6g9tKD(+SGh0cRRmkOvOLK60LNHyRv9x@w`Ynh8;X z*|RU5hZXfJ{0CD0FTvnd8CBzVti^5+VJcX(b8Uv$G{YLyvCB2q`{XtP zUp*Y#L9qb{+o@Bh4mAJox96XKUUkfZ5BAbaFK)yEIC=7<2xkVNAgQ{aN5jK@PG^Nf z$Ri3l!>gfO8XILT`ITrZD$a3@$N*I0=_@-R*)(v8@eio1i`zFtGaUvN(DJ~)BI0N3 z+~g;eQbm#93x$8{NC6zprT{{amV@S(4!|OKU*7m{ZEZy!ZLf+q0KL53W{Sjb+ye~R zV}#BFuCYLKZeu)3XmdGqfRSw zS1>_2PcnL}6%cZv<1sDs%WW=(7AWW*{r{0jhl3tGJT_EGn9%s?Y?=IR(e772rXc_ z`^NkHDNlTX?t4`5Cvv^Vd4OTx`|i8%zzwV~03;$@qLB#y^_@HieSXVe0ZyGd)kr)5 z^!{lEah7~O6W7F02q-$z&TI=^X<^TF0#qMENL4_=n9Ux0)oU%O!^y+<4C%#es4fb* zpmQ-VjB&0d{Qw1qzu>+>7TGDW;LEvO8bIVO?5LTG^hUgd2f%OV&YfF$FaGeM!!2^K zy|vA`uZv-N-p~8{iB^TtORKwu9f)@~sFx3%|qSE2|JQ{+H zItd1qL}x=xG);g#nqlh#pvK&%()e1=o5TjG9lykoySS${du%!;0xhgYMkLL#)RA!6 zbLT}jtV*0O%*l1U=RNN^@cjRaFTU7F=OT^IO*jC4TRH$*giZfHSHL&y8g~t7nh-G} zJ^}tOMt)W?@@tCr4)R#Syejm2F*VK#Ln}}%h8#*wk6nv4llgaydeK5^q*ZD zg1F^uxxiuv;QaaXt;J}1j{4B1*O$`@(Brg+A*|w1t|ibfcH9v|iN3Rmeg~Pu&~!`_ zk?*Z8Mrfzdf<|fpboD^FQhRU)mJYyy$^Q}u0JTVo-3z4M0tf5>Y zU6}#Lq)`|X>E#lG9F9d7)Y1u2VPxsz3ng&BW~ljoUgLp?531iy4LSR)pK(aP%f2(d zB|92mM2t7zd~@U5-VWNhe*gR5?*%JSm_Ct13C}Cz?OQ8#_& z!~hO|ONx6Xjv#Re2DxTQ!(^{_0oL>4*R^o?U}R(*2Zn z-DVL60{6>g{RCN9M@u2S2x9BU-yU`VG8#M0CQgMxZ{okt^W6$ug%%t-&bXd`8~LN`7(rp z-hk1ZZS76C53=J#VrCgY}i6eNQR^ zB)$D{?|-zmL9rQ}?g)Z}2utHPWiDl7Q;>JmhV+_ffIAdzRWH|X-soSvcoD~9EH~sr zd1N|p-ml=fvIPt!LsK<*g+Ue-@;7}VrQ|v*Z~>S|Snadbfe}xhq*1tjt@{ERi-8P zjdodB_p_vz87GEKB~-+va>F_Ad%PAjXN*D6BK}R=*vLZGiUHku$DNIDcROgK+7|#; zMUF4L@WMtq0ZyDav3>ODQIX*)Q(X@vvVxTyg^K$$)p$;g+$~33L!fHUN&1e!1uzkM za5X37;-s7PvzVNl#d$Jmi9%GaJ#9(lFKYDtS}?xfeipTHj?YbY&gAHSx`Q(-92{zN z?N4y&0PJ&UR^yysEcRWqzmK6Zx7Ns0Q^y^OG6$h6#8V^bj5Cs544CGq#t|Jk8N?|F z$LNiI9Zvv|&lDblQW(6XsIf-9Pgz)sC4QynWRUfMwRnbe;kPz$0iaZkSsC{5$;;c^ z4r*H(a|MFbA@=8=|GwMQX7MCQMlOASdOy72^j*;{hZszCO0MK7ulC)FblMPE(?E)P^Af)aX-Y%b91)oz2@?JO3K?>Hm(=fMFfnX3qgvc`T*_@YY*zsiPpY z;rQ+1#fuGJP^eO0(;NE^ww?Kvze?C5;sogU`xV0-WBHxb?6~&%dFar~`ijK!J#Hk?#pk10qyiRm#cY$_4?zrQQ z16BVE+w;#quSUe0G5!AcpNE4mfYRpWfor1dl63?!8iXzvRIm`cUu z9IImOBg~*n9>eC~>I;!73+B)g`LTZEN>_wS?Rplkz(hip;iLH2v`uHI5Z8EPAf5Yh zXhPeo!a;U+cJRgp@Rr4;1F*e3*F6>L zcG&_|gmes*6tTd%*3~KPgj&afWSnR#WSMmXo*y6TbH+m2^{Dx+6BXfee&qbX>SwT? zYa7iD2+&4%jx+IAM*JW`%^$t2VGU{0;J!xyqQSbtpk4U0x?BR4#J$e(S-tt@o5N`I zkj9L(4~B>5TLL|^;MVrm;I&U>WvY(ULv-#%c3iI>7~~?FJX@i}(g%$7DL^{>x{^X0 zjH7n*x3DQj0mMkZziusWULy>Yc^^wA@q`IlWYiO7H{*OmZHkQYW2EsY)d`4VLye(8w=z)GG8q2H*z-~;Xj9p)bB>&CFABCV)*StiGLtiz;-pRf)0#~L|B0CZHz ze$67v@Qze0QN)}tUo(C(2323~8sG2-yO=S23Eg7DXlxfF#E%Q?ApbzY8zAp6p+i0- z7pZdPyHaO?$pot$_6~Klpvbu(dG@gi)rr;SRv}%0CUWE;VD+Had+x|yI zd=(VDa@3uK1(0d?p6c`kXn&Rjh02TRHvmZy=ruaD0|3{Qqcc+g`}_Mv3-jA=zwIlW z-QND5TkJ0!@3W0)Z~Gq4CW*vyhPr?mFgK|HBxVX1F$IVUz9UG}{_d>ffCR&USx}V- z-;tz7j$}wfXbL#+Rb#6`@Cup&CArt0Jr7Fulg@*-`<0?oy^-#joiJkN;*5*wwfgDt;ENW2yj} zHni37b^J5mV8!nUYysX5#Xvcf`v6%|AXeW6KD+=2Rffr`OQB9+A82o9Oynd7Kt%v9 zUc4xJqA@F4dt!~x>H7-*{r$y|9W|~|QBq0nzq<)WK))>= zCSNacxyDFq(cil0d@G`M%zc+y_hOn~yF$VI)Swfty)jt3PSxA<)lrT7ZfmLyfRW-i zbyRcQav-)G4`;_|I=i>FwbL+4W~|Ll0cZ|D0)IV=v$)-X7~-|! zGG?7XO19U*@J*~nXdK*R=qu$rsO?ELkJTV;X@uI}%EABC_-nfCmeOG90%5Z!>fcM~ zlZEHgP^1a;#efnn)m#s8fTE+HIdf(sP`VfAdh4yXAUH))z7+Cs@ZR1YoIQJXBaDDv zFX5J3ZfVNaV2IaasE@Yj%CKQ}Jv#+q=Om(khVv=m?pVcu*NTQ+sSPXa-kO|RNX@ty zZ@BypW93m?k;WBT7hA^F;{*qwEdqQ)`}ESKOWA?#Z|2_qezU!`)fK@>NZ&i zuymaGGFGp*UTRzqxaWVhh}KxzQVtAQ*EyD?@+v}jw7xTqs7L+>l$}&1Bmk4~kz}0= zE7o94P_zHuR?iF0jxOH0XK~&{0H^~X61|MS*vyv>fZu0p8R2_0U_K`hY9FDF{Lb_N z<@3~%a#OBnUNj!UVos@wd66Svy7FM(QZqpR!SP@)>mn(HtV1snER$XLcu+_NVF|g9 zV)ToP`f|p&q}@>GbEu2@wf#5=D5H<=?^VhKO*Jc=Qj0;`r!42-_SkDj_e|v> zAL&OW1Rz|~@)(;T18}a$agwN0;R>Abloq*Mim0$=pq9lE+!{oJ)`4D3{mwejgUG2J z+Xj>lk<94{-;3-4#9A{PIn4%s3Qr9`hL-=QzdPZXT=tNH+6MUMn{S#3K&X86C+Ld+ zef!_`^1Z9By2^`+w6D->ns8gYA@NEp=43^D)M=4(IzpkO zf8Npl%*c;pbxa4)nilO@^4~vk;sh?|!bT8)=bwLmTmXA1pAnj1cGkuluqG5js?|b`y;GZCB`S-V-^MBD zTgHUgnFu=9YJ2?)YzbXKgYbvbJpd<`3z9Vn=;CeMN8pQml)`M9ZT(syHClo_6}F3h zr8}>Ey0^FINZ+Ao{HeF)odDnG?|c4tcXqmHx|axUQGh;T9t`II(N2TOYrN?J^>0#o zUDRN0*W`2fO{aiZsxMuVk9UPy1Sqv4Xs~w%tg>1IYuos=&k59DyV}9jF_9&e%5_^P z9(gs4eSn+4$1lC~5^yTf62THGm3;2px!KZ*-v|n@H2!eM9e0TQ5=qJeD%-m-&bA6= z&TB1xEHPFHxw%+6QL=%b%GoiILV4xLzkJ=SnunBGJtgl__MaBGjrB(z8a(U>madJ} z?w+XW_5?usGZ)vlT&vc3vj*Vx*I)NPEP9p^T-a;Lc*75U_tn8EVteld zjQR3W|5<;K-na&+Sc3f_V}y!~)uJgDD2fvQ&J`gO!g*l&xjqrNNRj3=Xb66(W0kEt zPzwEWUVO_B!8#$7#Re5vA4|~_at>JPB*snxd#*68P|ANs+u06sd-c^;n-=0fgQz*dQci*iVDKY%F?a|Pmo2Y-I2_@qi3|=yhYm99N?6HKf)`nYscAM=I z9PUe*>2=OwWr5=On#y)qO;$(J!l^A93(f~yLQ;|b|9zm$Yq%^2z_6c1T>vTqaN)uQ zjsTEm__pYzeZ6(@El)TCtpl*U$@|TI-%$;1#2rgA5{5fcRL5@AkU!5FS69Y`Pjj_n z^xim9n;4V&7{(A?8Wom09@^)0Y&Lx-f>z@o9|J1=*z|ZAf+iCUkl^=hN5u*#L+-tj z_AQNNTFS+h9)lW;+i$;pXA_HRAOH%E`ar@}!kZsE1o-^(&o4G*94LQ+lng8> zH_hb1P$C)g|6r*FUgcAExpq(!DT{y+y+ZybhPbBhSf z?8m}q`c}jBox%ahaTYB8wcX%CwL?FcXlWGY{iWf_s>8-ck+U)G?Yy<%~d-g1nhl-BS z;Y;K-%JsBw`dWfbZSQ*5yS8_CclR$|ya*NC`bE9?$Oyou!0prEVivmv;;J=8pY)3U zxiSD%T+<5bJ|@9VQC(Th31%x7AOrCXk+5jTb_(prlp*lS%yIyF6riyp1-e*=NW&NI z_*c^t?<_(cK*Xt!Ll1silt&zbK35#&xgm1{$VT!iQM!+LpN*&hmp>;NV>g%JET?8&(W3r$>eyGEowB)V4R4Laus+kyZkFCr@-g@fut@<7wpz+TDDr>FpP;${jWDYK-`whR}r{|!3K`S8?$p%yB zz=Z!-yNBn^6oB7+CxC^GQR&~XXn2F)77#W#fKfxM92tJSV9k){UpfuASiTat8OI6S z)&dlB^~WrIQlEZ6sHJFV(Jb2U(>`_D3}6*?SQ9XfG+}b;z#5iO%OIwd3sd^rX;Ev* z_n10laof)X(jqM3V8|KyaDAFYY(JqmL<6;0_VfJ4OJsu=-^ zxEGO!i=mG`VsRr5L;K#+fI;7;N4?hwif?bKm-kJ%{1b}BXx`)C!1x=EJ#0)Vj71Ka z0agHUr&S_h3Sy$HO^`XRYn>`cZbaV#v%;cbi3v%YcbtOwg>daCt*-{mkJ4fsjwQE+ z0a7t5>RI z&MlXhR~j#AyPeCy?VXXvsrRb%^Im(Mt_&uhRrm}U3((Se+FHIh4-OKy2-dYMKgwy4 zehB<;i;aJcu#gn> z&}+*nhgnEVCVE%TPwPCe(l0dfC|Q(p&ra%e=$uozSn!TJ?(o_e8-dO{8DE|FXMiIK zIw0;#oSzq8d~suhK-XP&-S*P?m}Fd{=hmnlGT9>4T*dx6RQcAqg+R?1!)qb!DKP%9 z-s=KUZM86xpHLnheFw9@Fc!%Z+Yr^dtEohEaYtF`6xiR}-!^_3GnbSrLT-xyU$FLA zPX2Ah1H@n|c6WD&JhpcLx;*}Jhpo|)1qY$&BKiTRiJ^X=^n!!i=kxC4g5-eLOn_l6 zxd9CI2adXn{XvI_wR2p+fE^=(9BDzwG2Efcl=k8@FI>sBMTMI=6~GckKl;9R*m~#~ ztcAKpAV?z@>Pe;0T7U^E0NP^C3*m%CCAw!NjzJ*2y62vIHWDrS?d;jJ2_&SR|8j`8 z3T8$@U{49`Ho2WSa|XZq)vrn@QW6smxQ^(A+T1;sGq6hJpK~rN^Ww?a=W7Y(g@ckM zgE~1YpoF*v!(T+OlqphPwRO`p`srE$T`L|+M2u7&mIbBISV(SI@3sqWxpvJt6f*As zy!`UZhYY-|oorsbgY?XTR|Fg&e18!G9d4%-2(c9(tCRtmG%LMwh@ZFP<3SAK+Uq7I9qKj6&f@jR9NsvJ8I1!5D5Wf}Kj5QI+Ck&Ky5Y$TS zhTp8o$&&mql)#$O^-G;BblinHPI}PWZ@+yb^1pt2=%I&pmM`~%w4FS8a<+5;$}8p5 zezBn&$SM#eV;3u+aj_6}EfH@jXpe@?Bpirwg?p`0=i{6dd4tr}O9BU|2xeA78Lps8 z<%9$GP47^XvSn~T1*m#ShrS?wM~d&XyUPl^tw7|(<*xgFp}Mqu`9%NKiUTlTKJ?|Y zpQ-MOkI2U8deOdo7eH5df`)PgWbij>{{2C~Lg8}i5h)W{_bgX@B=EOeQ zh|8nt>t_Q}fo^af&=o=Mqwx_;l=V$<64t|WMH7k|LE2hZDJ2|Tp#N}DeDg;5|9?=oQ>RYt^y?HK7pkpo zq!!{UpUA-JZoGUP-VIQDEq%PT_BO*w0fzE6Y(jH-oew=QvZ8uF>Zp=&K#@J1KA$pf zF#o+a@So9tNZYHgzUsY@nexEf|C@C@=~0Lt{^@0ahl=@TtoRQGh39F(ux!-~e@gy7 zdptEqjLF~0e$4Zpl=TysKQv;Ukn5{#<10F=q;5gE&m~cWa^JOMVu|Bt$uV2|Bb9(8 z%zv7=kg9YD>Av^ebI;=c>kHaHx9#ZBqx0*pzrI)|TBD0K#wuhYdf_YObIceHSayt3 zd3tH}T#cZp(AcaVaZi_F-i*QLzgw>;=okwoREYgXrxFg&TeYb7{!4bf>f0 zUgyr83rs}BsKvjvyR!qKFsl!_kmf{(fM#+aM9v_o?2jlG zM&h7J&6`E&bu+Xf$^$XM4j-*oa~))WLegHO@Fs+3YDg;3>SXQ*i> zSwzytx;7^?X^AbE>$R8*O!!X0^MyKWbv@33!3>KO9oBULhV^7O|7=u`H0J=6ApqV1 zSb8FJT9uEn2ZO%ic~qzE`;Og0CLeeN}Rb25t7NibiDz~u~ZDs6^c^J zrGaaWf3+hX)`ZRATu``=@+_t<)F}Rw8+66VEEWW!sFY9unACU~*OLoG+MquyR;uo& zrvjyV05JA%4|&hs-Q9@pU_SQ?>Q;NIZH2glNIDq$D&0^s5EzrRCpl=hJKNZ6(O=vn z3PmtEEpiLW!SP8Y;{#)LXuC7P7-cYU_aK%Hc%>5|c$3_DB1S!l6#DB1Yq7IB35;5R zs6a5cM)k72?L^@H`ynr?9YZqw^s`x<+oySb={Ql5pQc;jf9u$>W6gEfUAGbb{~z@2 zjyvwyUEbrjwdX*&L?TqiaOk^f3?>4%NhsPr=K7=&N}POge#a~~Som{OEw50SZ#_FZ zt5EQ~&^q40{~_2VGwpW$-RC<4N(X(On9k~Zs(yyQUZL1wqi|n1ibSTLR^LmAQ@4a_p z{qjHPo9{I8`U<(cei7`?n6R1CiWvvJ26bDzHOx8wwUNKjX`p^CbxpV{j{}&$p%J|q z?QsXO1p+cwThi6M34tu|AjIbXFS?{0_2P5X0eH^ZlS_gEgUh$Kw_(vP-aP{#j8K2f zYfMgl`M!^IX{4Q}f7J`bc%HWOc*8)aC`6KKEN?5NN`n#oB}RpOMD?)2g2f_>$dx)p zRL7uPX|T+h$a=rBfyP|no&~?xoEv?jNhb&)Iw~2QeS z0me2g+U0Vki^>k*_>f;Oz>Fg#!>{p*)ScXVeFfk}xNGbfQ)2V1=8t6f1B5`^*Ru$8 z-2SII8EAL}xwYuIA_Ty1&p!L?j6+6rW^M26%si7`uqOax1w8b0h#?P8^ZgpLoNk05 zDnb;Bz_QVIj)R#-#*-poDnvw#Tx;kW92lM=C&aELLetO@31evj_xHSgO)>b={nH`? zOZ+H2uTWr^JC}@MN9HZlXkYB%mQWz#Otu{O6h2Nhh0+>?s3v6j-;*a#z61YX!R^jF z@0?3;XR;0|-5Y`33&=hc0@pMJq(azQgHnos>BdDUpiR4$V1jJ2Y1owWE&?4_1wF!2 zp94C0HGVYjiQIe5vtTLM(z=87z&(hb>1%zQ03lNm>R0I6GEuK;v<>JVYvzYu7jwl>0}u_& zT7Ry}hzg5aETWSQ#1eTb;#RR!7geQJ!4+kE%o_p2tzk%8=iBH zZY{6QGz|^q2SCU{ZAFuyt0}Cialjlxp~0rPh^EHi2ghR-jsR&;)2N$2tAd5QeMH=8 zq$!9bdT7%7#uodi4&Z?Y9(cz>U*XNy8l5_Is!_)>eLko<3D#JviR?|9=u&NoDMlu) zQF3Y!!rN3!&v8PbJgWBy3%T@V<6GoFm8>IVEeWc9&P@a?AXn*;7OxxRQf-k+2VfEc6=UEBo_Ip%D9gNu5o z)v?`oogf=qpwz60+yqj+NCEV&=XX&}7a+RzG5aaUDs@<30+^5~_)Fw@p$HXV9aY7t z8>svc3$HIZ^Jx6}!M0Q2IBMh0Z@IPT3GM*2fxfo)bI(2J4fz&55jv=Sg>Da@BF*tH z_7{yK(z+uLfFokb-LxGL0&=U6TwC9JKdSJ$N@5_W^B_m$hD=^5zXnvhpk!dhC|mpc zP)GC=?nl$0ijdSg1f@~FmQSZ}Xr?AZC2+~16*(tGQX-_0J0G{4)h{j{1wOY>8#n&A zCr_T-i2U#W-!{*)z>INhTpRLl;-8~b#U=zSG)<-~zO}4@s`ZGuNz?^OumfNV=aL@v z2{i#ZgLXyo5gI>3#!0}8fu!voP_xG6?0*Tj@1T!>g0rI3EcWexPWwdiZKvAc z44htDWCx}($uTNk(GBWja*@5sg+W*&UW zcoq?WN434Zu+PgcznpN;hil#0+1W}5b{t&+8(5(ie?azw)DCBH)KHHe1JG_b&F@o# zNnzh2W;r)N`KV!BZChF7V3<%kmKar9kjT%!G`iQK)rtv}oRj55d`C2nAfH@lAN2?G3zY$IFM1Ih3M~@yQ1_6nI52QOXmE}On;{XF^{vd$?Detrf z&(Pq!R3s=nojhwCnxnQu2+)z@M)4Mz1$~2IA^ZRZ=Z+S-QB$Q~Vvwam$&v`Z2%U+a zp*ZPsq&-fCbWwtGwQ&;PdyqO5Vkv;WQ1Bi4--F%myYD`r&z3uZe0(IWWFQ*(DpV^s z_SX?Lw@T%loTtvxmpBfPBlv;rLYa-yS5idOZ1_5n3Hex9h&@cWE+ins$o{vZJ+F~+ zIDgN%6R|P^;J45Uh{ERIzxLW|N4j7b4S&nG4|DZl_0B=W%m;mKzz|I{N!PXr#_I-< zC>dN8H$Ij;AW$B}SO_Q|F@fTVE2l--U@{%%jH_PABspFdCj#x$ms$z6?@L5Xm2jO{ z%`Z7TTI-ck5ST>nbnKOOM`&>J*47r@amO8p+&kNWZJuvo%=tI+vvQQtah%N-vJ}Sp zV(dOb?o3+Dk{Xmg-B1a91~zd>j{#UT+%kGcr3WY`^>;sBUj0FOTU=n+d6(>nk>-y00OcLJjNK1j8n0?Iz9S&`fa zIBpoEr^~e59jrV6kWBnBgAXW#nRXC=sGPkYBKAKZ4s4L9subImo_D~b*gY>*k} zT|}9hALM9UZVvI*RAEjPAqd*{rrN^3+L2lpGbn;lZP@HefH+`_B#J|$ekMmf!CZqh zWsBDHX$}|U-iz*Qv4t1T6+jCdQ~KXA`7H9kNb9w)MY8IV{>yToOOgZNTLHhRee205 zpM<@=y=?^w!(+#eaT&P?`uFXBAQ=xAROq}a8;su@`&=;=pm1H~eUR9NQEpdycD0ar z=lL&sgVTiYT}(yT(6?#yv)uo*-OwSbHonqWBT-14Z1A1L_@vF!OzK0sCdTfUEUXd= zz`r9z?8qGhA`X?v088KF4$T7(JTPJ+ko! zSOkXj0I7ro$_$lyow+&yEwYS@i1>{!=f=+o?ns6gq_jank;%A!qea{%^=_%tQ0hc+ zr9~@!AbEF07XYP*Tza-rBqyN&XcUG`Ndp9DZ`8?%_xj_zmWI8M+;yuDnpwW7DbtmH~N-AYz#6Y%V>sk?a>2QR*+caE0#sn z*hIuU^jS$FH$w3oPAMX!$5UFQL@s?w)}tA4EA6151DaTrSWH;KkeJw#Lx&Ezcfb4H z@AUs~U^{*K^e#oBMg$-oYg(FzaA;u0c%XGG@^8o>W5|rPS}(xY9-Rb6G6LuKlc90(jeJJgDY z`txmq(Ur!dmE%^`EFDRaU|InZ=?yIUVm+*f^J_W>=%N`>=w^OXg6>1U)`+~28+)+> z!pZdp284^Gl@Pf&5RV*{Hp4dWeBXWdHL#`?@|~@2yXKl}wr;-p=7x^JZp}%xC-~f5 z?gyGPoN*2@2mN!Qs8V`i4Smq;;1o^{1|&?gg-~v&(IG{&bD;ZyfN@XC`;o{-RVBZI`6Bk zK{rH!(GE%FEj6-&M?ItYn%T^mG8&b+X_~)p1SXCFR!130Xxksbi5ddLi5bz1!Ztpu z`o844Nte7Al1a06c3@PHHu=9K0``&l* z{~O#MeDJ|R?J}3k@8>SkHY+;*Bml1MqVYuyaUKXCNu(H+nKueGtYhIj_@qFv+IC6bjv>UB2YO-o~ofn|_-^Fv!=a2k5|&&sDu zwE#e5V`=-I35S&xv7puF7Z4IbcxQ5iUx6B^g>=F>p(gah7e9_LR76E)mACRcgZE5N zRoNdjIM~0AA3xsQaKjDnFkNBXzx4h?IfO@%%sVb1>?gP%{Gs z5_9Gpmr%-yDFU3d4`kOzbXI^~Cr(shY2+gFWL=lmcRBwrp07pPquLY{p@08(t^?p3 z{0->~Ut7LDEV>VmJ?3-#+dDfumk5m2)>j-lbZG0yRae2=Z@(RJNBY4IHhhCMZh$sW zI*)DU&^sY9)FvAi=&L#VByP*tMl{J|f*_04a7bCx{I$Z^!U+^6!{^ZkD9*M4pLs;jQr zhz^3+Ai49-JDbNJe|(mU{yALF(@zxs2gpb3*5_fYR6_jhJ~w<$DzM|UdzYn3Mx zVt_OZe+d#bOgI7)1^Hi~W55{C>NqY{4n)LQVMew=xRM8uiMD%zmMO*a;8rZUU&WyQ z^V=AcPaLPsmx1TbD$j~J1i3ZoYb6fA!`2a=c;bm!0%`S&;*B@nIHQVw1N`oKK7f>6 zI>+XOQsuqX%13+SgEjZI9uh+EQJ#1?7Z zNS`}BMlD4942AoP)^+8M-AB(@5|x*Fy~6%L(F+c*_J#{`#-5C0(SY1@&pjKX|2=>H z{NnZ3Uk4YFRi*}CGu*kCv@dls==UBD+uuGaED)^+-a7!%Anyw2md128=H2z4R?}FblK+--(}S^U z#yppnzwQ_N;0!=tuK+}iXkcz0na1q%M>WD9A~$VOnb?b;rM#8)q?sqpDU!6XE`tZ)qj@ian68+Ok94oM&IcGhPsh7gGUe=yWY8Rc)nGoi;>q?GpY=qkLYhhZd<%ZVYLh z1oRx<|Ni%HH2?q9Q%@~e8Y$A|vJ836OK=J(ve&+ze)?(GS2O~%(LdLamzYF=~50QhP~7~NIVH! zQBJORL^lwUZTD6CwKC6I0bnj55mWnGR{QU@qx|DP{-fL5+uKQ;l;wYYRkSCi(LB%P zWbKs#^?_&^xRPr1@ZkBwr)dL}s%lEL1M=t4ZUM2dBK%b${Sw$+iSV%S_?U}$EQ#OD zxna)m7c5%haV#Pt#c9i-0MWniyYIe@%Kv&=Zd#WiU60@xRB!xP^ge{&UIqhRDtx1L z03Z0k2WB+AV>kio8l#aw8Dk7f)~n#K3IbfBk>Zr(x$dI%AnSFue?)TY4B*6b7>(|F zTM+x~KvrN`cM!4~1L-phAi{E-9@d^I%6gAs&}j7)IS1hHY8-&S(~ffP+&TB+i!Tn} z3xYqs5b(r_6FVrSznYex+ih=eyZL-pY~Tb+*kbXUo*ZP93pVm&I7~Lz*9%A!Ij|Xr zWWX$m@MAjLG(}O&XPrp{7>kaiIe;Z;hFor5v(P1rhs33xgYzV=cI}yGSlqA#1#kk( zBE46ExW$k6WyE=8Q^xMnrAy7T&pr!0B42m(pF;tNbK#zT`sw{mIsl#zpg%WoGIxX+ z{Tr!(B>I<%|Fqoyw5oqh35cLLFqdMQDjt35m@B2iQT)sf6t>#Ez>9wH4Rq7T% z4T#1!XBzcy$d+W*RUi&Q&X$`&eYFGNw{Lvo8(V^BjK_~3-$eo~M-}#N6bFLHsgp;i zmM6DpeZyy3em&7@L?FjigTLThsy95t&b$TVoO!N*LLzlz|GwGUP-Y*@MI zB@0)G;M|z2mkhY2)+Ztt(n8DOVG9Ka#>2Or9X@<`V;ulX<9}JI##bDIl{bzeNDhWX z5q^-{?YG~4=;+a-jRuqFa{o2XuMz#D;ZP*qDy7C5Dw_TO)#61S#4 z0W9NKaDZGZBCzY4KA_~13@kt|qYvdix1jttHe|)}z-UKGI?uz^>r4TBXZd$plwF~&qy6%Y5QNemYf%$tQdoVr3TQF z1rEqoqO+m}Eoq^8DIWmjqxPJBf$1VmYC8lwhfxiCB;iSV<-rj=5+4&v*F{tu)_Eoc z@J-GDSOOcUbpYS`*0&rj1Pnwe{#}KJ(ko0L`ENUvK8z-gz`K-3|a_L?LuMUo))yEjucE?aI;aprUJ+!d^0zYTR1Z? zZV?I~pa>XH00#TEpPxB%X4Coqh8LyfTJ^3#8X&r-FTeaUyz#~xi%s5_4}9PQb1D79 z3r~gdqDKFv#eXo)RW&_FoCuWWP{>p&g^Z(Qc>z#>2Dv@$P^gB>QBDx>-S9BZn=2N>Lqt%!`MLmChml>A0m zA54SIS@8m-%Pv5QP$$9uIe0U5U|bUMNQ-Lb*Z?O%*uEgasM=ylJzlBkQ0q?MqCPnk zKtckfRKV`;E}lMp`l?MCIY0is``z!(wdg*V)|vWJQcjUW@XRyMT;8MuaMxXT?HW7= zRxvAR(7qJ?^Km4RQI!B$!nqh|e7g1vZO=(MB3K~=v#4+?N@u{+I5_ufDq=+2EEfkd zmHmKN_ZcSlL7KydDx4GbhbswyzO+^c;L@c_UKudS=lDr*>#etLOBL|`z5O2E-=1$% z+E){v*Mx9{-)<#&`}9F+m3x+ZV0a}p2N1cTfEl$u!Z$ADkp{cC`nOD;@IT$ zJ#nRC7$`UgGcs31g-jHH)Mj99M*w$P8Tn7My}i9Lxch|{UbwuRdnh3~R;0epG=Rnt z@Z^(EZWIBy`R1E2&flk;aJdo#Off2^Nbfr6GT$*v-12>Z391+z>i>Z z0BGqudC7 z```yZ$e$r*2#rv4Ae}5|MUR&!gAvBE(AkvlNI(;6?4C_<0@(OnaZNBH=E;OWmBxQH z#3ENp6#Z59Q_(udHN{dQplBQ%cBD>&CvW(*7Uuu`nq)BmKKCzv>L;J7b^w+K_JlyH zF2L8m_BD|Z=!P3^I7Fawlq6%jRKTo7r^LaGLk>lZ{uV434h0X~BI_{<>e!QqST2}M z4+LlO{13l_vXY;rxEBqGa`{QLqHF4DTFi{bd&5M?<=DA{XZAiDs6bAOT_JD5l11Nm zVQ_lRoH?^m^}oj+dkiHb&-?#F^?u|`C&ndD5$s~kgfZ2C@$dN8(m8?P=x=I!-}~M- z*PgRg@G6b33@Qo1@lv9HQ=O8oal{yCfk0!TelAb6HzoSdM9fp4GxGIx6&${VgQ2(s z%b}=@D0(#lXd~h{G2#g&@V|VTE9L*cG(VzeEpri#ng;mhH^1rLe*5j=$Grrv>r?@^ zj~_oSa=u5N@O;*a1XWb7M{3xhib^zU33ph7N`{O;b~9{3c+Cht$L&wy4tU~Vi(-2!C(Loogs20_%& zt`Y&E-2CJY1Sv>H4N0?{+{L1yh!6x7%(Wx;*{;IuQHt6QCrAt1X8<-T0lb!3Kn#5G zpVT=gtc{Gw!sSqaTv75D_b>0Q-vRJ-0RQw){}dv+CKLkt&z6zWoN)Jx55%kWb#(yQ zJXB1u;|3Ol(DRR_(2vA~R|}JA@aa%zqeAyk#Pd%}eT~tMlP96_o+1xRqx>nkZ!Az~ z!wU39e4kDljV3kM7D!wDgmcIDzW2Qitkhh*+V;vTuUvlp_18J_kB6h$Cp!RtX?^&WuYBd0kPGvX()RxTenX8jTcEI$2ksOCQ4{GAnCxnc`MM&$7~aoG z4ol2lW26X)bq>&ecfQEIxtvN^)+va-w}J^OuwOifkGXpm{%gScxK0#6>PQgI358-V zvmlV3Gj&>n*v>~j@{x_U|AqX2#t5qfJ`%leu4qszAmqqEfWseu{PB&~0etww9|kSz zm7ssA@UKKEu{Qdj5S4PLE;!+FN)?;zI;PFp+76+LP}(8`s*T1$2@HiF#iI3&rr(P= zgVg2>EV|g`x$Z2-!j^enX<9cI`D2eLSOh??{M1i=^5LULC#C?FKj2-5Z)l(Y!$16k zuLIcS2Jb!ZdCv|p;EjX=_~?H=hc3@Q$|DxjwfZk=TIY%YPpO~~YM%vZNs@~fQXQ_U zCx5LFTm5&VJbpzj02)%lMKC6-v|kwo5TZ9UbnZb*1ql1J?`=~4*KdzK_Shmizb387 z&p8NGJ+MSUP|^diSOLHDo$qYg{=4_S_q|(&F-Ohu(};qk@k*or(s8Oq#U<;22Fde& zXw3X(0lAkmv1}+}0<1Not9%73Rz$Rdolokl0giLasm%iDaX!CD428g$R{85=p9|5RpcD zPYH|-CCBj3aQU2)if2Q?OpM6G{iXW|vZ>iM1sbNGxTv|WP~8PZp!=8r>f_?^)CeotaeMFMc!DO|9|e;=gl2T z+M3KdfUkf3>vR5GZo28FBZm$h5|#JjXffoTX9+Y_S}AU}?HSQN@5Qy?GNBl)Z$WGX z5CE0dBh&cjOI>6mTSLW})%tb?ya0~nqH0wBq-RGe)vHi|3@cz_=s9dHv9@Uj<3k_% z&_<)$x88bd@!WII&9s(2E&wIf1C+1?=Jw%=l9CY7(@#IW(K>)5M~=*cJg|fWaOhtm z3bLrA4TKO&^7}5o>bGN8KsSZ{bDB!C3-_j#uV5|^D(?`8k= z55`){{jXCUuts#q|NZErk8ZpWaQVEpwY)#;_P`9QBx;T^8DmoPPg8DD+8}B3B1?^; zVPLL_0*yya<^DSAh~#;n1nl*{SyiA=82uBD85M8wPKP6*`d0CskEFq8Mk zAWWs)r$jP3A}J#|d>Xy?$21u}1t%9QkuPhRNovvqP_O_yZjeIBMHNy*J z;hkXwVFd)2eihy)HNbkc-mj zf-X4$AiyWkAKRr<}&dYGiT7o!A`)gb@j&+9Ot{koeg7_ZKAppMaEWFHH zWi9mo{@?$4``>@}@BXLPmjAg%+V@9(=mR|Rz{{H$I13&-sKmXX?<-PgV z_SSr9Ec_$8hi;8pkGJthkZTnD`WJkb~T&o3N@ z9+&Mn~_Z;Z{!5J0)3*_I!e)QhW`?1T; zLET^aFMAJ^u{r#~MYXOZS5by^MI~YimFy^_dbn}3^5>cgpW)a!3FQeWQTQ!h{K?Jxb4=z2DwPbSbNu*-7oc%6ZP7|kS=FcNWE0B`3!QS592@+tVG~b<0*G@S* znvqSmea}CzL(4zkWlYtF|LyMhSx9S0a5Sf^eXeReB3g3_gOH2hwiz{D1h*5|ZM)F@|#~n_GE{>G^ zsi)$Nj>~C$B^aIO@LkHuKji3pRE_5BaUJS^LeZeqX^^cI2tqaJQq@c8+QYpTm>Nw9 z-U8!~lJS>NCLVzcQ9MPkRO2tC3y{wPiI}Jm_U2GI7S7+2=-&b@agq?w3OGdJ`O}>W zKmx!57(qJ+lTe{o=bNn&k>M$&TuSoKVYch&YjO&JL?wT&?c>`4zxvg$Zt;6@|NZyx zG<~mJnAF;2ntN72=Y@V2>EhprayO_MYAsJ67EE88P?9bv8Jd|Q{VX*F;ojCO_TfrQ zrJ{+264YNpzZnpbOjWZgibM^mut=LytCka>I1pl?nC1kuh*aOgh?qJS3`L60M`PTx ziuri!og>6NebFsuTJ%;a0u-H!;M`~q40(Sj+%2dhKu*alI04DCDUAFd9I^;R3uXS0rKA2{haSiJi4ak19Q0c+R&-0uP6jlS7na&yu29~_UCg9S^)w7q!Y{60c>q=yUTl*vDsgMkAuC3=AD5thDOepTx7u5`%q%I zO1-pbl64e;Q?gVjomcK0dJXJSISnX%Pvc-jGw4$9aV%Q+&=BQL{uN9J72AQJ*N~eeD*kua-vc^Bx-9n#_R$~#B z$z`H{$#7HE)P}vETR-{vDJO3-c@~fZgi}$lzz6fx()xJLUo1Z>!c@|3tz`gnAn}KB#ef45* z&qoUlg)IGxFIZdir4umk{E@xAJ?tupQ&Efx$xbn{r;KtX-l@e|4-mm+#m1^&5m%mB1~sUn1}J(~lsPXLOX1qHMS)TTmO}wbc+Z{DE+|BCD%2bzFP5hT)_afY zg3m~ZXoOu_d{dCrEu~DJILEp zv$Y6X^lwQm+Z-e^Z$>j1Fj@o)wJ3tKfFc}PY!4z8})TG z<9*bk|0$fwO!OOKgS6_YA*8SE z^y%3%&pguvqtA<$!VMqa?{jsyjILU(0~DifVpKxZHc+Kfs#w;CTNE{Cyu^`emYstG zSHbC*jmYDVP&#}a3cw?59@$yn4Ml-85+#2lr{+yUZTsHx-J-%Fnns}cR9dJgp#XF- z=M=$w-w@&R-*b$P*c(KFjj`lD((4~O8W<{(RB~p7r0im_aNZ)z3+*j^gp@NyYez(4)dKfQ*s0{ZR4AO7&}GNNxnv66K0Eh?~lcr7#F ze7j#{I(pxdh@TcF@ndTOLlP8SfC9vkKCkMG^$ftAV1;hw2mq{gUWwZbLNaw|k9^Wo)NPiXu8)nEM;&@_Ob zB=39h!K*x^@6!OG10XC0fhZ_EG*1gWpU-+U09q7)7x)MUUMwVIldb@)5+6J*&=cC+ zD*9w+Fl}rPALiM-!VNN1yGkY?5TYF(2}{lx3JY1(o+gMTI3?Af7HCPx5nnhB{&v*~ zFnHjHC_TXZ#3+dA2M%XDr7B!=1gOEI&Vc0*)H(><@Rv9a3hZvBCs0an@O^o1HmDp1 z5>3hQOZR$k+GOLOwENL@qsI7K#erH>%&*lzJkWN|GvRO#p~32rLIB1@gW9+MvB;?? z`md%X7cusTVKA)ElTioM^18K0ko*2Mry`9!m0*a&|2azXzx?T+`sDeV)1RVo{@La0 zSEUawT)5!A@|CZ+haP%pKxWR|d+*hkFJFG?(4j-L8z6zMp^J(k%F{P)h!T2uHW{I; z>N~3h7(=$@Ir|cb9gNXE^A3$mD0g!dzT<-zxeG&rNgM zTwIHwqE;oE0lcM_KFfx&Z)#36AKmOxy z`LwD3_mLw`kZJ%(6q29-nxPbozStE&4Rxgx z;P~~1N`CGXjORSsYomT3TDY0ibs|};RsU-anna!=IR)coi_Zv3jk)2FNKvZlBxIZn zYIuP=J-qNvLv`4`)y7{#H?)*tt?{=y1%&ijGUuhb3oH6Bd9H;ydaO38*0<8N3<-)* z3riH8%ajU#%h54;N%}*God3?nf^h)ys*JirDA?+&GuQN)R}Hv-QVPIB^7QrTPyd#0 z2Yg!F|8M{HZ{hXVUq3>O9)98zpEz>q(&c8+bZLMl7S|b*DDyeay*2*bO_r9wxUAokC_P+`2Wf2QTm@u=YlL52&;3Wpfqua;lE}PU# z55`}ZL~WF$r)FT40c8(n5O)T!fwhVp9;_(WQREm{1VNcxrdoYok`uM+0SX+VXz>?& z%R7-r9+f(wATPSMOj{Q!VlT+afM}tc#-yhiR=$sK*9))GI)EuJrS2gy_RQgk`1grp zA{t+zrT~Teh>|hq1Wq{?NJ&*vGyWMU-|A|Rj*Z&>hGs`xCjb$(Elx~bmL&w&L-JRb zQ!OKAt9B@=@}OgJiF_$IME_McVW18kYyAd)8yraRzdyCy?|f!92dBLzI{@Aw^Vf3w zqV=6W`IA4nc6nol^un1lXRh7Z-f?@E+irk{=HNn=Fg+!2TN?krrhBNKBq;E#H~rUz zkktr-MMn?|2Pam{6in)4$c!Bnu-HCOHGoVOy~z=O>Gfx%Qn4gmV8FtnGoi_}vPd5| z^=sA%XbtZe-6@st$j)&v*qVbPp#i)KUXt$BoB<69kctJ3LeQ#(SINC2rzdu#s5e(2 zC?FK3;b&2Q)aEPP6R}+|XZ$79LJ^^&=b;u#azk#Z?OACODf*|y$hl6O^&96PqvOi1 z2g}A<#_$`YQdR3VXzQ{okPeoM_FIjb?d9{?wGd{ZB#WHBP~0h3rg>X>EOR?QJLSRW z5dc50*z31{>$iOx;3*Z0_|li$`Sa%kEP{s)9fBYE_{Vnk7yH=sGsr{wQa;=-CEnO~ zjz%*WkFG^<-iz>b)xyXOtcbG+g4x__oT`{5eNCx#iSPyy9w+}#3d~ZJ0yAhr`D9Ck zIShlV-n$$MpuqhJM?ZB4wa+vTcB3dvcLLh?I-5%?`winH%EJ!l%M1&31SAq2`Z>9F zkgiRO!g;3)8OxI)ec=CWvV; zA(#<}#4uOaAu-Dd`(jDT04niD%%=?|rz9ULOw=gb0Mi4Z1XkyEq) zxZqB5E<6_=?Ju{?X;65ACNS|I6$aIK;;}u(A}{^h)X4&u=KK|+oXBWMPJ)19a0dz` zhDBnSCmVkyYLA`+7bxPX3&|leP^~iPKjy9wyD7jn^lRUwc5Amr(jP5ADdI$y=LRUj z8CJos3hUr9Q1yy@TSfOog)6ABM~nME7yW-`>U}R30}>=y|C9B-&wu{&$1Y#KJisF# z{_uxRcmuzD&qx0a6$#<0e4#KR6wF~i_B<-!xdPfEv9?tR9nA&+kzb|%r3Q%Q*oO%> z83iDzc#xB4z+4ewboMgUm@n!nFwR&3^c)1t%iO-Paob2wrdWP6MmrzsC6UaV?>iz^s(0e(DS^;R(?H#KUlU_L@TE z(#Rhi<6w~vC?S#LLvr821yjqDmJCfn!fJ`aMZ;p>&izM@uT!cd_2)tN@isKq{`b3= zFJ5-mYqA6I+8b|(umA9W{vY37e*6vXdtd(Ym)*-RzZ@I@yzaW|ckjFJzHM*(m#;-| z0;GxLg@CrU=B-Syi=>00ea~|n!z?`XhTqw&B{yU;>rnuSyiItDq4R4Z3c*Dq=agAvdc9%&(`t!|nVP|EM`gn}#~-;NrJq5K{-UdjHH ziki#P4hC_N<@rZN;ZI}6%og%ZDg0A4tpkBi;cxQ}gDB$f#s>&#q6G3cYB1`Ie@~~M zGya;Bn#)-$X)M!e4@gb-d84jGC9R0cxfLxa1$FUBx6PUmg1iWj@H}j4>|pw_Mz}4x zuQ|%!82VQM3w}MrTwtH&;Q!D4SO4{ozOl5Y&U$HEmIvV3XP>h^_9(#nqz}AA@E`rr zA07GSU;gC_Vd4JR$3C|6$Rm%u>8XLYwzhmKz&B(2**c@DasJVJg}~XYiyWc{>U9H_ zf6u!c2AxOOv}ps*NuIgkIl`cdV}L6l^BDw*7{;Y2vgj5A|Eu*u9geZYmN19jh`uQS5&S^;Ua4A?y z3`EXx_)%ZMKaB7AdaUs@wFkN=>f>sF24tQrv%y!B3O5sHt^YrNYg*gA{69(kkQ;xC zW1kd=qHg><)P$c8JsuecYNvp7eL1H=dWO(Te725IMkox|kN0=uk&hvHOrZdvmvkb4 zE37%Nya#@ewsM~Dy;4U2iblKt4Nv)dxbBpZuPc(!kL2X|#p>G3$^PL7KKPNzSDJMI z8e)JY=wbQ#-?R@u``OPP`o&-T#Usn@P5{>173vrZDdE9Mgo^Y9b{2W})VL9N&^}JR z`%)?oOs}z6G)N#diBx{L6F}~n>I86O-QFj^_@M3tZ~>y=1hgVk9lIYzYgDdx?g&Ua z>zvq!Ivo7`1u>&I#nHj}AY4;cp>f5WtNPq6Cs1=t5M{K<)M992#%n3pFGT!m3Wyv3 z6#YqUsnFeF8vlYc7FsQW+VfQIpORw6+GmSGq>U9)xlnLm+^ECy7BBy9wb@tA&v*{L`MLbn?)PIQ4VB5tRQdq`_nRfB4<+Jvn(Dn60vi{K~)m z)fblk`AhBdXP$Y+ef{fSA4DzgzyJPg4C zz^4OXChMeI-0@Bt996DioebEwDgxvDWdZc7W<+@#n)y zQus=&z^tSA$?~-5q}N&qrS~*=SE#PXj4ZLZu+w%i>DWozTiQ3DK>bqjT<*PE!635F zAOmc)ypgKCm^g^HP)g!Rks)`Z*or``gO$b~*fA92-y|s^p>{(w{@OUp75ybBfB$=Q zJ~x9vl4#V2yIo@F6}Uf!AuUQ^NUWsBdPWtD_<{$O8CqAfaEt4z_8YCEfKfLiZO<00 zc9h9q4yB#tGutUSEj7)Ij<03t0JN@yobb=5f8uBV#l_2emtF2P*#X#J4oK_OqX6pm zna_OYx}XrajQZ!F_{1l6eG6c(5ZLhkWqKpM5uXj{07h*x?M+@razP^?fy52H@5%|o zBO<66o0uKC2Jw&8^A9F8R}$Qf@GFZpiWtU>NB}kJQX_)Jf*fWP)wRWJj;Nstl(dps zpyC9mn(zWMo>ax2wDl202Wgr`h#uQTU+4r-+w^h{LeA)OAs{J7PXxP$7gzNYqSeIU z%T7ciDceQHx9QRV$egMiR{$;kL!osu8AnXyiH{S^$Q7H$-x$j%rFa(2LQNI8HB{WQ z3i;xKW1^skLRcG*g&ax%b+W_?HedT)3>k~g1(16hQ8oZUH^vif9WA&`P-;_{keZbH z&!G)}4zBEflT!fqp1do!i)RJ=znoI3_=PWg0bYLj}z}wHob@+}NK@dqHB&h(pSTBc@Pr!^$G zDt`%$aYEZCH~uupLS&RfMHgF|T7lqQ=N!jjK+Ge7mUpZwXM{@kiYAbUZ( z+q>rLcRux77ni&Jj`n?D4EV=?{KvsLz#sadAG+QznjQt{IRSzb(D1v|L;1c&AWG=~ zGnO#GJ`o;{%(&z|Opu9zc$A(gz~TJFdI0QN62&?RHrCJp4K`1V0LC!5#Ew_O0vL4+2zzjdpS)0dX?#Hn zv!X`jOBcAPPK4ewUL<94CrHnt)cEt~Cs9q;82>m@l-pm`r31T)f8Pm`gAGfee`DT4 zReCUQgC`OE6xJDFjD!yOc?pF(M!0_zp~u0O#88@K4;!2>ZPit^##I7F=u)6~L}8BP zivK=6Yv!)>HQ51J%$n@WLlM?7KKt3v9=&++V%s-)%{A8?`q;-lHeW3ETXjI{0SJc6 z=lanX2}0jBHy{CA2G^AJzp9U!I}p!A#*)G%-0 zAvcdK1W5Hpco-?#_W8}bnTd-5-3YcNi9`BCe=Wq8pA+&^#MK20V9$eT!5e&D@XsBuFu#k z7Q4%FwY`>{Yux&Wg78oHJ16|}>3{Vvf9{P#^O-AsO?LpwxBuJv@b`cJ_XmNXkA3W8 zH~2N7X9gUK0eh9eB#NHH9JjW%9EXJ+RdJl29p{A`sA36*K^`R~XWG6Iq{S=w!HT?v zf3iu08Dm!(oR^>g8Vp&?6sR48FW7VuQ)q-W2LzQa0E+JLA}=)Z&ZtFrPUrPcpm4sg zf)U~j6OH`lrc~+{Mo2q5@`|~HE`qN zn=}5XwD|$A<}NY*TFYTHA2@R?hE$YZ@3BH!AeYmYsmrB>DY<{3T4{@zJ#%(;QMc~# zphB!7WA(j#+$)6sRqY1dW(1B$KEAxOk)!_o+~Cv{z9u^W8WpfW4eYxCRMLRI_=~@Q zbLY;rp3Sw_UVHeXAN}Z-H~f7!z@j()A|@LBmZ+pRM)HG)=OA$Oi!Cty6r1yaCsasS{nZ&t~5wa;C~m@-}9iRe=ZWPkt_at_~-uB&wXxP4>Nml7vFrd z==C4}<9~R=LIEybyy*V(fBw&h1BCT6Kl3wJ6BGbxOQ19s=5vn%Y!7WDBMJZ{j*n{r zP`JXv=okwkQ*}VjNb#YjQfQ>du*k{8$cL{E_oB+#MlHGxI zZsa)P=|s#KW-*mSyDBMc->FxoH3CwLq4c{{?$}E4(1lnk7^HmsS-pVhyNfJJV9hVd z;7i7TNCS?L12z1>QzllfpCJak{>55mOlOf7gKEY<;jCaRp&VIZPC`r@lU9wDM*rq~ zHrfrN#YPbUqVX3Xt`uYl-@|Fqe+D%jjK8eY3JjguUK*!Et3?0k(2|2jAvpESe?IZr^3le~J_&66qxsV+yJ3 zm#gcC+>FT?M-0UYwAEPpL9h5c{LMk-()skTmI&x|P0@6wI|Fw;+U2Etim z9TYm^oZ*m|8%KwzksU^2w816Cbt2raor}^P01Qna-)j<`gDYVCvkH%?@lRMrTT9!l z7X70M^^0$S2+w!-Lra}cO37l)=b15&rCrmcOe{M7(iR;nK64@ZH&UT$>tA8<-|tx5 z{%dyt=CiHhgZ=jJ{PySm&A{ryEVpSMwf*9nL!K0}J!tNnS$0Qbje2Vl%n`N+Eoi!k)f?LBuOmJh-b zjR55{j0`g~dZpDkU_|Xeiyf8f-c|VmioS2rhDuxjl2!mW)?{K!P61j@KtHKeCqQyW zTvYfOQ8`+-2}14Do{N8wZ1(c7_TQd0n}g+{)piioXJu$5Zc1eI1uvBQsB_g zTGu2Vf)dy{DY!}t2inn13>H(}JXGx1cUEIL{AlD=yOJVqyKd)1>$~m*r zf?P^7tVKDe<9C1QmwxH|fg%9sUOHEM%uj0$fZu-a_kQo#Lk~Ul)}cd(F8<4({n@LR z4#3%^12FR_!1ngGF9eu{%3_WJEQ57l3u z?V{h1j1y!LB5Bkn`}I<`zy>b{e`w^i22mQ@O%MmtjQ122fDs6R67g70fE0ZqS6UR9 z_Z+0^L87z7x=x(H=dYqM64}rgxi>DTfid2H5Y9QGMGi(gh+t76AG#=Ia( zz7^0P-q0XmNVmMLvN*gjM}JHA*?5CiJj@>~#Up*R>H zVj&e_NAgZ?w?QUcqjQD_lDGrk{-~`gD#xV zv-F9=KSG@WNYFJUr;&b>D<6_-_kc!#CFCPW9}lNQq=bR~7Kg!C6$YyH3q)u@M<8;v zxEy9j2|@UO*?SXkTdt~Hbk169?|t?@ZTgT#uUEgv6ND&e1f;P;NFpJD03jjm!2=-) z3HT&*0R)3!L*peKkOo5|$TcSD!w(*KFB(8WKduOhUTLl-64IO|=bYWFwQ9aOs^+X3 zbJmz$t=jwSb56~Cm9@{_YgN^%nl;Bi{xQZsQoMdF!5mWU!`}mW{+v2Sgz+b@4OQ#! zc>a*!f756a)PUF6r*!4>ty-zND2c}3JhCevNyM>`Rvo^miy50X0w*!*j|27JNuxzV z9=oz6L;{NZRmk{%>58jfdG{!vbowx7U9S#rf7{yuG2Gw{72I~)w$W%byRfh@bJ}UA zLDpkF8CSwU6RhJ+O`zdmru2d|by$G}&X6_$HFe1DTkd0-p)a3?X7P|~%_>&G27~RZ zc(bOU4Y@cG+5;xSHJ+YSRBbMRfwDf1wrF|SP=-Eb3?r1e$fQL(F%265mm#o$pDV!- z*S}N8qH5W(2X6NWus>JNSNiyB9@xDAoC$w1jend8eU-?fbKgck0IGaZfni+l&^RB* zwWul`Tp-E1##F3cgql5h?mM@PzpyBO;g}n|rSWG*St_2ptSKWbm+vY%K0Dt(H~uT?sK1eD~Y894g};A7cL zfOer%m;ft55bs|wPYYmAUVu&W6A6I!^@aasIG4bOxEw5& z6OdUU2dB|;oe%EwVfO)WFb5>1j}5;kRA7KJ%lMO_6Ka31mf!Y!UAYj=^+xDqXCg~K zn)(+Wkjz8CI>;(ft_N+66ZuFQs|eRO7aleNMRh_^S3B!}5}EZ+t$v~bS(>2O<^4xc z|Mvp3{-5{#XXkWY5bey)43f9&-}@d&RJhr-Psn2Y(1$*>4TOQ7@PsEEd;IaogUnx{ z3J4%Tq4->T=JrDb$$pR@P*A*9BBd_#C>IklF-1_ZRMmZ~R*R~aVO%aYu> zJhh~lXky2k%Mka+L`|dI1Sg~dpm6392p=0sN;H(~n6y2cQ|&&t5k++^y)r4b_dkr# z{lDS4XP&*Xcke#dc54j)_KlWWZF0QVw_oY?^ZW0=e-kwNIp>^nHtJhF8jYYy&;++5 zZBXKZRRF*#5lKi`g~TKZz(J()sSbqZ310fT;ulo&7+pW#to^6K0M$Im z7cwy4*SxPj&=#ua=bSaxosJL?&+x_1kE~8>kMbym1va`st`=D*rn3u ztSapcV?J^^)GBSx=Hxtw>OrxcCDDxiTkDQ>&M~aR^ws-#i@BEYe z_@`a_1!16%eB>h#a~Yg|`sv503zCmV<9vB}Ialq14b5?UlQUHkn3W8-Kuvapxp0{m z+|vJd8N%w`HnQ89sN`0Kxj=aqK<=Njaapl~cp@9+1C}QrT7D zK>qsZkoSM~SPfN{>zP$hfS!wUdlGJpkkT-IGf%vFn;aQ%>m!%?{Q+$9dAS%3nn^D02a znPrS90hK!3jc9A3asxQ`@dDiSJ7L5oxDC3!fJLiBD z406=(TezqG6(Kf6<|<&l0PM{X;z78SjqA`66$f{f6;fq3I?PYmvHR!u+R&LCcBaA{QD8fU1Xz4lNI)o!OXf);RHXIhJmAdH;ea z{R@qEbSeCk3;^~$vbXil-}bh*-F@w~*Z!6K@oCiGckSB6KK8MXZMyvO%lDpp?z!6} z0C))Oyv_v}4u=KlkPZeJn@m_;ps$QG*P1z&hlmzuVLvR5q%RdV)lYpPG$gmI z4CIvf0O^Gk*&mjPq>Lb%^u??x^A2LVfYP(krnu~~!idF)Eba497Y7UkU}NhN`eA1B zAg9U@VWCDKpy4qR6sgWr;Hf~sK0eNJ*I>k_Q-DDTd$CFsiCyr}9D^t*GsP2fhA~@wdKP3==;b4+yRg!bKy* z)BHRVT=*IVlF5m|ZiiN1meLG!<^8uR`NM0PvGzKvVjIR^%l;k}IMg4#?AI>)ceaWn z2mqFsm#00(^)3Jap*8HDyd4i>uSSHo*pGyiPl#s8%3+WfAj;$9E3oZn|8MzPd;g0SZgyrWjsk(K1>{(o5G{YJU)Z z9-L|iEw)t>jVPQBwKR=W}U^tLNmjuEKUL=4bk$|B*mi$|W^wc3vU#JG= zRnKisB>x~DNBxi)s}d`F4XD_qj$al}zeJKE=b{$rjB>{XLaCj=I#)G95~UORInxHe+k zytLyw>9EcUtPKTMTk!#2P6|jyR1+Yef^mlIJc$27#LtRE*&eX>E_45~HEm zESVTcEcBf6R;ffrJ88>Tl|a3RkgD}22wLZCwClCsPi(?22XtKzSBvb3V?<@?$GkW9 zR3WfBQO~G#b|mIie%nuWkKP)f7as7HCxoy_>=RFJ0*t$NpDVo z7G?Y!*NZBDjb#rIEWnk{G45qVqO(9wPK=I#S+Y#J|A-0Wbz>-LPd2qN{u~*9CYr>C zr|#eO{^ufW?@eoAnAZdV$ZPN+`SK60zd!QGBka~&Z{2kA$tQ1n(vzMv%XCFRtqYKo z)c}?EHc$Y7+vu;{kTsHb*9|O8mA>-xs^l-- z?s0ler+X-H3+ovt4Br1SfusEo`JTHE833S$R~O(DpZLUv`|rPh;erb;IKpNDOE&XycC#ik}t{^E^43652h-uR+^hW<3$v`kG;|3U%THdHrFh&>F}a)PFM%W;7nn ztCGKT@1Ib5CYo+FKsK?fn3liqUMr)#W&l85i(7pxz`lL^*e$o*vgOoMPd)LtZ3BZ9LGn zWUbH0*kAxt)V2UnNWXurhoM#P-n>ShU{+WR<6(bq>-tsK)JoiAE>R_PpY`A=UO%P4 z7a@R^Wc-EqeQ{vg$Cd; z;#$OE1)&`QuQ7H1Yv%p0H3JZ20YDev!3Q7Qbn(R(Z>v!hD};eYDu5dd@BR$zb$a##PZ~nfEHfO|8?d4uQ>n+ zu>hbBc=OFSZ#(U@(@rj9`bE85kqEdlS}s@s2?B(z=w`71e2@*u?CJ^##2Lgu`)&Pz zaiJ$XrlIQm0F@Vj!Upz($P*+on=Mcs4^N{Fq4`gLBb!77VJ75wdyjj{qnw6+HQ9is zMv_PZ0`@h))hYNdIt)eJN9X3~x<{T@8$i(F`E@q=@fmVYjzHs_9iJCIwarjShDs15 zC|rF$-VTBx%HN(FUN4-uUflgYArg$H3vj#I=fc;rC4>c?2q@Nyi4yi1n8c<)9b23| zR^UHrGq#8rML35oXs*v>^Q#in*A2t$y8q#Niv1rb1^~vpe1E*_uDb@icke#(oO8}u zC~L0EPypltEG;eNdRPmv5!=rO%JU{_HI?zN+BgOh0;!yVs{5buK0FJNj(SGq5pbIY zuwZv$gp8ro2-0GsL?RIb0$8*CZ^)>N2++bJydjYi5{O{~?EUFQP7n%?HV~lp;Q#`a z0U+Dhidx<^WaRAD!!G)rcrO)$4<_%mtsG$Sy#Pck2%)ZrFN_n|m#x57&1;JBq7sZ< za>c%R>+||!6%mQ!2@%1bVTOe?{>+#2(~uBvDN9yeW#1ZRfn-N$(IHn*uEiYH_%rAF zDecB4Q2gu0f30)Ese8cHAFQ=X-;9{>!@Jko7_T`1P!$1Rb^RTLhiPTXGq8P`hCWq zV674;e+SD#g#0|uNn2(Ih3&k}$Ks@i{A-niC1g=mSw^9NGptZQ!%F9WE|9wvI@G!!WzDX z&}Z+x3quA}q?Q%nVzf`Q>Dwh?M*Pj~iM;h|>@kK~cFy}U~A8>sw^MxzBGreHH1U*5hCM)*-Qm{uO? z{lD=*7~(Zi0(2OFFEEcp@ROeOBzEbgmwxMtE3Wt_^?tCPgW+&E*tl`yaBgmHMvg)q zP%8xDCNO#-ny5}_xFVo57NTq*R4nMQSh-IXFC^5Wf+4IGUu=lzSwb;fub~<<5Hbfg z_9g>TweXP=7$}}RGlquFEvi2_^*+34*zZS+qyHS4R{_tfLKyltV~lEl$*m?o-1D7% zOboZLL5ljBeO^34%8$D4>usR?2a&R_5EzL^hL42(W2kYGCp_c?r!jPYir0^gzX?hV zmBJdh7OrJZ9Y5yUI_I;U$Yx726X6u^cQe`vyYrjKehcLHVGZe1q1U%2m@q;D+4A!8 zR;c&y!uvNuD4bCpB6B4%o!h`z4v_pkE^xR%mhXGvfp})K1GrxNp6`!e{_>YWAm~w# zdCX&mMkruB7Qg~PLLe{#l#Hi?39u3ZD@j()!nGuRdLFf*)RR<@EudK=rPN3tly*v; zmrgWj&{`HF;RkTdz=nu1Ij-yJVi_Wt0Ow}_+r?@fv`G+T&DeaI(BTSQ@=Hd%`Vbl3#4BKefJ} zN8%>s?r8?*WXqjM4{SgyU zusQ?ZULiO`zjySKHYgr#Tq=K@bN7r9HV9}Y;wY6u>%2?#!cuP7sv4-DRJY2WKoKP))E}n~K*L3|pWAfeaV;q<}pXf9|T(u-Q46;io=J$xKrfsi9bZY>6LD`8UJt zIbr-m;SdtMddMSD(*MF2Y9N_U*V=*DNC$W#))lki5vMFNcNzcMK?2%eiT$v){Z%<{ z=07{i{sgb7e?hPu;Qen39PS4BzJEJVc6i7R?0AjbPILY7-h1z5-}%mWjz8gq6aM+W z`|ca73kykrd}VogGBYy+y3QHAqRwzeKX|x7N*6%K{kTrVg9`n;rg^+Qc~KoVcQ90S zm$$NGA5H;K6+XCiB-MJ>NanLUk#WoPN45KHdNlO@srOp+ptJj*y}+T^snA`SEpNPm zmQ?R%v#VhfSO^=mQr!e8Ab^0BAZTV`Q}VttN&Pb*G~Ms=`hC|88~>D$tgqXj!)Oc7PuwT| z!xM$oV!jL&HsB7P7Qa_suX#xPD-YPVh=CJj;Jm@A;>15?{~PUo<#t5iaR0#$Q0#v_ zgaF`@{iA%j+4r71?zm&qX{Vj`7=|+|r5zuQM} z$w#sQPDX$_FS~}F^1hT5v=JhpviB+D;qvrV?usX5kXs4wYex06m~z9AHUVqK=v`mT zSqXq293Rz>4JvmiyAjnW!qRIjVtid_Df|Y$UK%Qu2Ngc*N zI@)Sc|L(N|)tFx+=O29V!TstpklX=nf(M&6Z5qzZ%oHtybwVKV{fb1tiCR=A z;|Vr&I2)Xki$R%)Lw4E0-Y1^r!uFE5DIS22N7U_d&*H>=iB1Z@aNa=3WD$?ERWRk5 zx)RZRiGkK4$V2H0^?S-ZS`%84+t{FSc4B%d&f^+;h=V&;)4oTbS{VX%X> z&={T1L?^&1lgtsS!1to;%bs%X#rZ*QRD<Py*Tmuj>9I68|&+ zn34ZHF>ttV$n6wpWpUUt0P1Q20`j&C{_JNzJ3RH&Q-2)yRF)UNNCI45UYbv!H5#+ zaRXDYlzFOOF0Au6Ly6@(21@2PT5mWaWMN!8qU^7d^tJ$KuGY3K5r@0(KpTG;2Ra8p z52y&ZKyHt9Jq#<75V_tN`dk&}Zg?}*XY0Ck9?~jiR=$vE zkZvji!4+e+yz{7h0^j?IGyy6E;mn_Kd1aOsLt|bPz%3)sNBBH>+dFKVBVd&mHtw$q zTZ9%*5h`D^&v!Mjcw28j@x4*U3AX|kf&#Wwo8C4af8aCT^9>k&bWC9YUg6i-O;u+5t{|72x^OF761bPHTwS`-omj{xQtFHCwe4+6jnHHx8fMVNl{9f^^=m#H)B|Vb9EgMmTqN8tY?Pf{ z|6E>{%exQ6Wod7pHCoa#GaBRS3m?r01e+5%n(J`N_)}~F;o%z@^PDIRqxRErj6b@O zP+M8y7^CX$BVZC1`1+x$AJNw1cZmb}<~Ig_WcN^fAB726NVdrgf44OL^z$w({-LR3 zMu$LLAU^;mQ;o8)v$L9s_MC&~U&#D6Kw)d@pEYX}OzL0XLr2+POyVDm*9`dj@V(as z0Dy7T)mH%``ke1Q_uY5jmJJ&=oUDWZRU1AoR#rxn6)Pdo%X2CaP~7xM)$?5F1V9V| z%n8q(@Y>;-nRD33%sX=d>pxo*l_27JBEsnkI%pA;FLyQ9aCuEGxnD=QU1us_RVWPDTPdvObCS5lzW0@v7-bgqc$%-7cNyJUH*nH||jAe4Ydl}P&7 z8=K}whTmw+)V1NKynd=fp&=)W&s~`J)nTgXZhR@mpD4x4X|=HiM(}z5R7H!=kx00f zmbbv_JOoiUD3XpJ82_O)4ZCnht6ncICz43c&_6>GVkq(NBbUGYihDW`Cp@G=07Y`1 z99PTj>#q0g+O-R)+Q-T52Xf*qDG&e#pD(YhoEpeTwo;m_Jq5I5;`M|unf-oaRCIH@FZ0Mi-&ML9&fB`{j2wP z;QgDqS)xh(*Wy#O-v7~o!+lq7m$7v;)(r!2^wCGNxBTW?pe*nP-}~UkOR(^0<7RQy zGPtP_=%Vb4x>9HuG#Czws{n{xsXEnl_s%&(%S3VJuEFKR!91#dvjz(UMPz8}XJ zxnn`JJ$ezbh?9zbzU06fe>n}KlVXoBCc%b7!rvLm*IN|L(+w`VV{Vt&=2g4yKW~niMh(?~F;)YmO zk2lXF^vnp73a9!cFoTw_!;;(x@$dxWFG8;QB+uvNci7MXH!aLRD#tdxeSNRdY>;Wb6Qm~_t5Ga41 zEG#S(&cC+SzLGBR?n+NlF!XHjrbv$o3RH!=nbfB0-m8Q@^fzJD#3Si`OQN_G5EQoT z6!(kuz~mHVB8)mu)np{rp%fLhL3rT~nlC5}WIR~hy@(NlLIT~p(1KT$PKW>y&~qZ> zg}Z>BBS92 z{@o?4LwNj+>!9MyMy$V!ct1~%#YfTi+W5OBOTl;&Wu5^qKVbYh82>rx+$}WmyJ$6+ zDX+}Xf-@(UAe;$(0su%tng3tB{IV--{?+xtp|c|KFo88r3zBU;((=NF#&TxJFeb(VdZ!8TJbJ#3uLo{!ZjH2#3dSJ~yU_ zQ83Z31^5!o$y0ire{V>{X7o5w>mMZ|7H|CZ9GH`9`X&c4K$fubNa~+qM})}wXPEkL zE(!y=9T{;yUc+bPG11lo00RIlSKA#Qz5@~hKNI-KW(flRtG4kP1W41LFE1@k>`;Ic z1Y~Hj0XBeZkJPC>gtPQav+4^f1j3-b%p3r>O#gRuisCfE1;$2I!Jnew9P z_}GTs85%UC3!C%BX7(D(t|JL6m=+IWwgSxMKcsIfR=#HNsZZi zcz>?ZAlIG!xZq@<$*m8-n@VQIK5^S^-ayuMe{i888rPk=GsF~>SU^zqfRsMJq3-4D zc~FDiXfD}V)1RU*K)50l1#k27p?!yR5CqZz;PIyx4iQlE#7FN(ggTNIf4gKZq%1Kl zHSAIAi@hfzwmi_sI<@JF=lhI9(USD|J^OSgfqH-?9445sAHG*yB};qrU~12o`NJ^6 zeXKfBnhwf~I@j7Y_e?#alKhz&jYpe!!1|{QN25O@?yql+7W}cyHm3eVWq8JVDVgRVY9y1VU1v4h7g6;YKERnFlanrAlR9FPa%r z>N8se2+u<&s1O2*;S1l?q$dg}_lv45#@fGd85`Fk6xR_rnF;W*0jb^=5B=Q}HfWqz zj0cs-3K)ciD-nz;n{qKEUDoh7y?!SPz^N{LvBwH^HgqsC45_FIpWc52+d_K!(|G$H zYQHhRaOtIoeQSsp7}^z7QLEMn5bce>?kJAfQR=}L4oaS8W`v#eM}22bz=~R*BqEnb z{|_$D5NiE7XQ9NI=nKU?-oJq;?X3SM1qinD=!iqU^{QXLdX&vb8)I!f05DEuLu0%7 zrd!@A|MbOBC_ozdAFDzEWhQ@N_?MQJa(D$mfO`ftj-A1z7!)CZyo$FA9K){-bljx# z#>7;zA1NNBHS?Q$xFo^YvREVSyH=@+a73kqV9o6RWTWr;oGXi; z0*e7gLU_Vcb=ZQLdd>}fo_5b(;jw7rU*{xohw&!}fd&I28U~m9L6wMQU<{y9hBIL~ zAIi0ROC1bgWTT!gjK954BI}=`{x>1V(=|RLjsLn({9E@S0MC8AQf_}2SR{U2-l+Sf zC%=n}BE0c!esFo2Fya)QVWKmcR|fLKtLmj!|j`W)BS z3xdv~+6C)&8N?fyDIctVofS@8VK4+#J+iD$2ILrR*V>ipYy_$ZV7%__70b=1h6AYY z-Dt2xG~)46uQ7iYO_?~~B;ye^=a|~9m?COHqZSixi@mc(Y_ZlJW#Lw*v6Oa$UiauY zr0zTMzNZ2Nk^1*Q$R!BI9CJVz-nt38MZ|Rz6kCP-dt?R_BUz2W(X7F!|c6E_&F zBv_Ja`~`M`K;Q?Ae?tnTDflN^&NbJ03c>+uOa1SY+hvpS+PCUHC>a3PxcR0J{=Izp zorqAtPg{sA*P#Hpjp1k5TwWvm6Ve1=D<&c66Hg`uD_}S6p<;lXH<}4?Vjm~S%9Jdp`#5fX5EnU0pIhUXNS@a5i*@G=-w#(0oHpe1R8o~z4S zHU1Dg><|OBStr6Znbp#$$80vxOCBNCf%==z%cE5JT;4vC=oJpoDprQ!9Hngn*& zQ2V=xR>r?(7;-_Ct6^f^haPDV=`j8x(UbF2|J6E~k;cDyfTZJil7|iBA3^;OBB}pt zF1~pCxA*Se$6Q+v0OCQwHS*=Ffwygv6X)?5{emU{jOC>o1mLnj{h;86Ff%j5LLf*# zoJygvNH8>(2JW0_!XrJ<><-A9UI;u4StSg_ZNuhN9pjGf-JE`Al4JalAiyrm3evHm z>*NCh&i(M{5P=s|pg1{q(Z&g0POt-D3lSj_U?2iOD6*-|78@cEp;C#|Fh*L4!=!d? z!*B1ylILwu^(dD@M0owwT;x>#jeSlzD^Bh{a;B1qqefV1_85jUn?LiAar!S@)QKC$^)!~n1(!X zh$>Zi|HgZ5VIU32;k=A<^@(sWGaR91G>PPE;X~SlVfX0}1ix<#?d+gpUGEctzmpU) zcV=4F$HR?$TwUc{zYkIP9stH$EOb2l=k7cEwG9>63vn0wiK$<==1k*HL?ftZ3fWF_ z^Z{9y$KB=93&vg;*zbeg_K!5TZ5TfAcM0mT!AcXg~5nc&zl=kJRpuvh`wld1(US038YtCB;uv z29)Z65=5{;5qz(El{1~9fCpb&3P*djB*GW>dnszT0D9ndtQ z5sGuRLa{6S5Te|@i@C!K4|T!|cFOa4(E&==>+Q!#Jc6;ap5sCkO#hAlzNn)yFYhRO{bMO6bIJ5WNR@U25WgBVB@@RqOA zKqri$tn(O1y(AEP0 zG_(?+Z@d0|H@!-}^(!G(15^U#P24Q%tp71QClUZmR#uiLsxm+@hn6HvnBhPI00{!L zJg@c$Id9VSj8}y!BnQg^Ab^n|04WKoeRDH;P8f;Eg%++Y4n!e$E~xWs*-|u0^(*FY zp$>5o3v(3x>;ePIfN+mNj-vl#jbQ>M?o$F>vk{892XhhPLO;#fZz2Xnz;z%{i+k2bwVT+XCu~_WaGcZ-Fu}Q z;~xsEp}0R|&2T4t_ZZ{QzsZv}y%P(HvGY2Y6tFY-Uh zpZBPXJ(-Lrd~tDUlJP-RW|x&oep!Q_&v=m#SfGc|h$|x@f1Tlufl6MtA_weh0HX_E zr~{z+Z={ALc%@WfNfd%~0nhYgf^U%{ReYj=))MG3bU9*m|82^g$jlFhK=fyDML`l1D*f580vqW>iw*L20%CE32kqD{hOgA@T!Oh_c%GZ{)@b+TeU|ZAGxwLaCvE> zM&$rfI-CP&89W&9;c!MhBst^-P>C}U!ayyU02(cN>xFnF7wFFXw$V04!6(xDskFa0 z13zSJ2nIi-A3!eb%Io*a4?F%IOqgq1L*JE9Pr|kq$MAXTKEWh!yxHaHQym1Yc@jnS zyL3a4M;xm|k&s)F=JgZs&G>FeF;h^KdgYV|kqT}^Rn#D&S}NPdf5tZcE*RQEk3`F( zV}YWE@wdPkmXOBl)4bfaMI3Vb#TQ@v?cor@$8lQ^09p!l16`< zy6cmoYvA&7p$`B3$+me!o-qR>35(Jn)7E?!zH+Vup z30gBJ9nP(XK;Y0u%0oOY?H>vW#CV;lhSgQZ7fNK0$aIg+q)#x+2fg7J=y_>z?_!!E z`3!zED7C{bo^TS6?RVjNmUJ84m_i}kYr*&2*iCsXsccff*JdSLVB2+)f&dhu=TI5t zS=HEwhk*ZdS^vTSE4J}B1%Cup=@i@O=VodAKYlRU(sdI8a7gusLIK`h0dfF(gBzHS zC!hy3cnDMip|o#tX$iRVY<|Oh(db0}jm{`9lX4^o7#4pU%RkA_bC47uEs&$^X@XpHs#op;f4rk}uRd*h zr(T-B4VdQW|81nW<7odvn5^k*aE}8dzZZ1%+vor=6dN){+~0k_ZM8q6<*G^jhiv3O zDqD0=j&%b7e&mNwPI~|CAH40`7r*q9i{+1h9C*k{0tSsFpmsDjx(ikcVydjMj;CuB zz`3i=Ph|rd?STa+N(Tb{&UpyDgn&)Mt8gX*(FV*8!*H{uTu@oiUKkwb#60IsEtoFP zW?JJrjp5-P>|?b+31t8~-=_#Pz;e8gPjmfKJlG=SKxzg0**Dkx;zT7-Yz0P4%%8Hd z0pq_x8vljH#dSWLqKl+IL6Lc^7WXPbz{E4ZQ2~IUBYNRQ7m55JDf?Tu0HDnSf9v~i z`Q)WLw%;Isd_`a~pCBj3_vA4^zHuN8`48ZiK>sf;Elu9G8lWJ4TyhsKdPnew0Y%Z-~xmLN7Jl1H=H> zOn}Is@NTFBY-8Y&w9nt5^m)uYTMMaTOj z!!lue9MxM;yft_Hjr6fLq^x%a<_8Asb# z>#WuiQ52B}2I~qhdHd=^IV-gaOz)$9Chh%}F~5b=s00lhp96JFM9`TpfpN$O*Of&M5UT|7@;V-5UWL#WS+4kK0 zA93DIPLE+-KM`Sg1?o)Zo5IF(Qd0W*nT19yn?dNuVYRz8{>kE}LSV#_K0#SyZ{kY=%&V#!EPyIQyGN9%KiN?ECj6Gx|sQVUa*hR~b zMYO?OBSkUM)wS#ZQ1HMpMB=$jXY3<-2sC>DB!QjKWf1mUK%Mm(?1E@k`UFZL5>hj2 zw-%(J+XzjsRG|50{`j6)>^pocCE+!6u- zazgwc$OBXv^v6qM`p{@J-YY>skqlTQ0hAp8A;_Q6$z`QM){22lfdJykF-ia}wZH@r zzzd7!p%PBn`-*iaC)JVW)y{)7^yUCAT?+=uJOw!*Mr15|4p=2=EjS$e>XgKxuftx;QBvYje%GqaAdthW|7MUwA}@l8;Y)55dC0f5A{mGUM*!3fHe% z8h;*jKXI9#)A+ME04h@EHO>#JLjgz*jfqgwU#3FCAzQe_4&r+Mbq@f#8WIG2mIcZJ z)!3|Ve1#U}1lb}1z%ffpOJZ?xkpuxQgbb4pjPh`1rZmosEXOfGfL*6+g8-{!4rdHj z%tKZ#?!Z=9-gXqF*k}icxj(9o*aHTHcadwi5KoAKwNL__0titJdqm2;Z(;0(rM|~og3r)Q7G9&R?xnD0c2SK^eNV$>tp=$dY5dh1 z6=nS8Qn>)x{>;-A(a~@y`FXc9{>|&dsw9hY&E$CfX(av0ZL8eoBMy7<)vx^ZZ*o}M zJKEO6ST`Ynzxe#;JKNXuU-;r1|NAFCc9Q(@^?^m=82PdyC(~XuDHbFM07~cqhz`up z&*RDf2nP&o`Wxj57X1UV8LmnMCk;xUnQDQoOyn0vI6!N23ucA{ay5Cg0vVK#W&s)s zWxygSfI*D_Q$-22z<6?87#W>VXV2ylNVMK^S>~ z4dyiW2J)pg5oLZN@f*e)f9t!|xyJ(MLmNqdl-Lg|E5l;MeqJMu|3NABd(bcdU3(SJ zd%?MHkh}d{#Bonl4IeNvoRuKpgvG_hS!f;vf0L6uCpv=(+xWp`1fx7NRPBVpKzo<~ z}c0$Rx9xBCU?OmODT%ivRa z{E9=f@IX*W&jrq@)&tr*j|2g9>_N18hO~!0kn7^s7~>z%`A_it83CoZYDiRHF5Xo7 zvzz>EP%5#Z@o8!Nf0rGiBV&j5fSAov3*X8BqW z3jkXb2vA-8ip0P{*~+*qaEk{481>o@DleIU>ISN0Kv9(ns(-7kY^ThfRgJ>?U~-=H zfLDuht-0p)>OGwYMfgz7xVX#XO#lL}zVc1B0CkY-z`Jh)45EMsb$wjdtb+u`RWs@n61XP9>%)K0oXFLuqyk#=)6lm`Js>A z`s?z?-w(WXPEMNRq@nyzdvZ$efBxZ}56>QP#I}cKXXlDl1~dXJ#lA*PfS&vS0w5b@C zYwe=CRVnwrpI$JM%l=#jDlwifd~(JtQVb|`E|E$>zVg9j?Slx3<;219Vx08Ji1xj% za}3EwM<-(@zCRMkS#5oZ{L_?{+njO7{BfK>E{y5|OJ~4ais{NHWJ=i&O8uhGn~)@K z?eqfe=zTFaUfN|GW6S?eCI@_&~&IY?2e? zSZmT0o1FN=ojZ3OySTVGr_g0WI6yH0(gPn1hPWkftcJYEv?tqmc%b}7l(_~q^C0S+ z1VS)`F{af59bPpHF%!&D$`$ZTqdY5LR(zxf9Zg#Y5(X*bpHxy9il^fc17a0KL=}KN z@(#9^BYf@=)I19@ALu%AUyezlG4F!8G3cn4jEV3XQ62}_s=2|)?uI;U{ORXhiTi9z zG5#J{)T-`}&?LbzeNmAdbjCjzf6(roi8$1?()i!P4udgdhtGgm&ZFh_^uQ#8vJl{B z9^zVmw{lkUZ13K^N6FW{n>KCQt2+ZSw@RDA1Pq1)#tTn<$P|U?^el~b#C0Mb*Z)&7 zuS(mcq!k+S;3a{eS_(6zUZ@|7qPGBD8CP`rMqWpotnxbRzv=t8?)>KJ_U8aQJZ3>5 zzk}N~7c7D+P4#G8*R;b{!ZBX(TJBp+yRUThypG)aM7AMpEOA9>`Fqei0fC?lt84eom)|wT|&8c}%`q`VbQk$AYpXkF^U> zfO98S$vCf;@yD^{q`ZcJe6XSDwt6<+t+^1I!y?}JyS;wK8!d!ghE;fse@vLc!A@z) zA3FJMoW}TTmx3jV{8*pG8Ygw)jjHwMbkd(b9)vA6M;!JG><~}-J8S`f0s-HYFK05< z9V)N_Kqz2=009C@1DBSTW_Rt{bsW4Bi;?%iD3kR{+pF*$HO7zq#NF-aMal&nrORcMb%6=Rk?yxZd94ypq8{1Q-#l`>KI14ZrKW z8X_qo~?^7(n?$eLuwgQ?EhS#vlIkpqjQyTwo9&YyVumb=J1c0j3)iKvcP9jhgoWm4ac@S%;eDC%AE&7-pZ&Am!nFCF=yZRXrZEHuBA-~Sx()iwUi%J#R6zp$7Up&YhjBM*JG zH2!xSu6FRS1po>JTqj@N9`V*877EhgWD5iob8zq8y<2zh-hCv@(^wEt#`SqsE;t+x z3O2y<+HJ3ZbC+Z5mI&ppvOwoUh*lSY*Rryrz`fvb+LKuYA(<4)d)kF4*2|vi3nt3( z9!QU$JB+J4R8W^zazE%XnHPIsxYtDsqwfQcvGd96{Wtd$!)P>fd9dNP=xs}ItNk1- z8~KpJKEZvJ5r+TMTQII9)k@8tUl)hKF; zeaGB`6y-mb_?<(n6WRSzh)96R9bW$w&w-s9R6V>(%ioje7Xc*rH0K&`{848=hYpKY z`Gc5$jL^^Tz52?lt~=cA-(e2`)PT~DJrVDNra{NSgM&+vplhJT1i&QCpj2@G{{35Y zEYQ}RCrfWss#?)n7}x*+0fU+i2-FXof#9kskQW3n?9CV92f8edhzz} z$(Luc2#o+apseJ0xn({Opq0@U7Z*3~*|X=U<>loen)rSYpcWDE{xuLN*#J@j%?kp` zI{(b3lnD?}pHt>&Bk0q-w?F_XlS^owiz)g5bZOwH$?z8)oug6GeSnPrs)Q4i;jg}* z_T*Svo(G@u9`F`(BHH8k12-NK`l{Q6*oGhJhZI#ky03@U@TfXlMi^LFx5ne~=80R_ zM}H@L&53g0wK4uo8~?h_-!}faGXCKC#|Yc~B{+P=^*q4adKib35}f z!87EKe<8OG5g-82<`2{a)DI%e&QOkdU9}Tsc^(_4P`)s4yB}RiEe_4g zAlWYw=n;88t8VZG3>Y9ssPv@$z5W@fQjN z#EdNpeMW%*R-o|7;E_ijIdb2=eMdOFWm{2Dd+K2HMW%qjpvVsxHl%!=AV3KN6*nD$ z09Yid$}8nW;FHRD8A536Pf&}F_klJ%YfpB2S3qD@L|s}M;M7d?wH$538RYeiEp1#U zBUK9^$ZVN^i!gd#L+&!}Tr?|1Xi0DAWGXjUr-oE@iRat|dCR#QuykkqqdfmiDg4Fg z`!C7unbP=wjhes*YU=^O`UL@Z%a?N_VfztslEyLt)kI%fTH3I4=gwo7mzOt0JbYz0 zHDx>p6$q#q0sJtNAVA5Ga0>)vIzIp>0W=l{(QMJ%I0P4X7i1l@tN9L3HqbE!t4%h0 z%>7O&(Tf2jUF8$3<=`n^KQR}&_?}%PWBR^R)Vbnr;1t_sCE()Dekvb=-ls#j#|swl zF~H!+yDV08^q}00LH4R`L=9)Z!J;4RG(?y+=Iq$RmJc2O$ukkU+%-3i6wWF(mlInhAAn93cBzw7sUG`;#gjA*@AmDcS^0LUc zZ&D?KL#xE8F#Ny*00>}&f|dOJhKC=1_~@mjrG*#}P$U2rh8{u!85<6)YJrln=ggnY zUo9`3o5mhzA!Fp9LfOl6@Otl*h>;NSokBbI{-%0wfY$EPaIYy45SF3Qw5-Fp3#{Ak zxF*v8p;SPXq6N?jWFb+$C;%HznOg?80oZBfJ-Nor$h8E#{tZ*yZr|Aij6bW|j)gpb zv(iBr)gd0^uj~9{zu$|b@&EJQlJx+f^|)-u%ib?vUXcg_23Cz~0R%J%u$cfYi@JaR z{;iKZ^2pH=z0byg09E5JIIscPz$zIW6s0DdHG9endchL7Jg{v2TSYO0RChz1l`|h3 z;9PL9v~8QkY8Al%2~-kw=hsq*XzJl(HtcHg8=!k76<@?06m~&5Cnww?c2AF z)nNg$-LNhP)ZUqn>lgU}gG?C7enfw5Y};&&0A==@55G$-m^Cx`QEajT0$8`6S_UyA zxXNvWWB27ZUVQ3fb|HeZW+OtG(0A?HhBms;0bp$Ul0qWUPq=h$@3oI@!ZXy;gz&KvZ(Vh{DZKRzJrNE(QTj>0&2!@AsIS6$;ps!9C1Fi|MqUd_=+?6|eRh0-Nrf zqD*g9`wMhw_@@9lZLdpL`52>`Z0UjYdSyh$&RP*^;GI=jCP z2?z}YDlm{~wg91bI^j>)O?{ZvJD~n24|=OuC!nEejI!%p1}6Xnf=jG@j6|bDg8)GT zjRY_-O@DzdH2ulzy^hNE4tRA$W8M%zH8?j>x&%+0vqp>IvTNTKxou2Yzpa+mQP*6z z8jhu-QZoI8GX6FbYUPz^;~$yKSkV5r^p>gz0ILZC-pAq-1AzfJNpABP1ZYYeX1;DG zj0J-%Zy3f9x(q^S073+=1q1aQC0HhUAwG$qVM-v?quoH9i10>%!2T6ka_U+q>^wo_A9Lofo z8sZsf6Du~L3=#0G&K1!Td5Kp~vn1KnPQWAUA))GWyp z_iMc+g&ex{bfXQwNRz6Y%J5GaB01p0rmS+2$5n`!cSgh%4dQBCp!ucYFXH@>V4=;t zZax13iK2!l!yILP$oRuAfZaGQ^6(?Moq5rYOZtSr9ssN%2srP;JAiVEz0QgW*ecAC zGhHS)o`~^ioLiI_`A|YQ0Kx()KY)DLF$kzxflTKIND!bTh0CyjP>cZ6i~!OTs2Bmf ze{SP|;DS<%6jiwZicy$$5wa4lrnN9_46>t{kLcp~Z4&09jCtZUjyCW<#ksZsqJ{8M zx5mGyQqRq)7R98e&uzs1%_+k_pE|D*QIVP;$y4tMHT4>vefivnYW!gbP|0v~jgnW`ia^Q&R#6X=FfFC6EImi## z{P4pMAG5TyWG@g@l|WSzsJn`30H8Nsdg(?7K~}KR6R`T;w4?mS8XfyRSvvjME=_LpHs zKySQY1O}+>sM&H9R}BcWt#QzK&|wdoT@ZG%VGA#wQm7L#U?9;*$JUrP3>Ic$XzwZg zJ0^RuB1Y?Fb-d>kP|*cr(UsAs=8wqp=A^K%FHy%IGXC;8osmueH0{|e;uGH*r{@W^ zh32{24Zo+53#7q2D1tX@CpcI zWwW6|Oh^L(6)M2m0@c0{n;%(f*H{R7JBimQ!`v|#;HiJJJ$-jP+u!$p5_^@}v_`8+ z?M+)GYL6DRt9GeXYEvY!N6lJQN>H^&QEJapyQ)_0+M5tbgyfrdKflK>|0b_TUiaQ} z?>Xmr&+~cQwyHKCPjc6P1zNvaOV-_b$`K(q(p}CGy4sPsyUM>+=-j~J9iGF1&CQAi zvrRTQvl+=%xUQn4D?xXUUpWZQ>3_-gZ?mDfgjW+j|M0oeZT@)ij%FP9%roc>pw^@R z?Kqg{>~ntkPnVI|QY6Ufq^Ahdr8Z2VT84na_uBxjU)~+%r$TJ!4;NdL8^hoWJWeDc z-Fde;42%;1ZP&iEy(VLgN0*U4YLj3$275eVL~xdg#;@0`#k>e?(q%0mf-wz7X4-4O)j+dtQuUWuihhB^`b@M@BKujLv*y1tVG7}y_ug_D9<_z*nyv>a2xok_|9D1 z_VerY^MCi);}k%NN77kXMUyt?D;2_H$$Vv&4r&YNp&sS_!4gj$g(|X9Nk+nENUS^K1t{x5&5$O4La&s^0^v`o9kJl zD7TTN%R%A<#(A#qpp0;h=?|jlBI7-vj;=-?U)&Y4Is7So30_posKo!|fqhLpt%&CT zGohjyy5VN`_pXSXUYRj zykD;@D}j7$XGx4>YCkX~PYKGeZNKJQKaPifI0m?s`swvC*2T@0G$HwVwkf7?XZoc# zXMip_=y$3$9)?sHzzVmo?}22>;fp8Hv|xKkWw4=3Hs%9%d!{g>#_0jy0DmWDxbwJU z>D?J%1;It^+g%e)6(T0gQeoe(QFSvmp?RW%`3yt^R%j)Gnr13j$Jn8Ah=CR;tnd`E zuw40;&Y;uS9nySw&A*l!|aOGws#O^R#NavGN_rUj6g4@Eh z{Z^22)felx?>j85n=^lFvp&?3!JI$$eo;al{Q`PBzzx0tWDmKslBOOyM3)_D82%RE zpJm-Semrjt*0CBKCXaeFoV}HPVs?Vp9ORK#%ab-Xx*8G(?`1ckSw9hdz*S)H-qNub z7U|AvakuoR6dW#^@RP>CtMT?tKyf%HDRJYnFm|be!kUSADfg`=J~kryBIp#>O3OY? z0qI}1C6Qr(Pc~Tq4r{Mb9l%QNu`pY;jeEqsWgU85a%+h}Nk?wz{nwo?g&Nasgk7mc zNMYe5JO8hXR4(o*W%Y*|TFODvNT~bMXjszshS7Cvr;@-iEb}k=Np|R7D-BHp3x9}u zC`_!B5uBR^Ji$9Kjolc!%nO{Jdx-*pl>iF9{qsu~{(?-G+RaxmDw-uP9R-qVkx^VOrhPcaIU`$cTNHY3U60jp$buV|{#R!Z~jB%KgjVq!!FCaO?u>+jA->1m z4T3v`t~y)7i18aXH7zP8zj$Oo`Gtv2vChKQkH}Me?ZTKMuCg8{)hWx_i)XC7#q|2% zD*@LX!yU3YD!)MaD$0h##XZU3_Z)qf#+tg`0L;Wp?HwDkMC->Ly+SpG=r(xezgQjvW$h8guVOedjW8>H4}^2aZYjwdMTHsg;B zN&g5;HJ(a_;J&)knp^6tT_JWDbzDc6Ft9|SGGbeLP{aA&rDplV@`c|M4t#c`4{XgR z&u7ywr0Zfr8W3~-?V`M*;M2x#WHRL8YMM!wh@AvDTEy_>92YyANL}Z)R|!*^LJ-jlA&xO|KSB(Spn+-3V%FVQD<+ zlT=wb_6^dd|3*{v=Y9n5f`jaPiupayHk*{e%QjHKDB26L(6#NE6n8{3U1-)NR7_NoC`f>e@qJ8pul8H4l;6^wm3MdsFgwxsPIx`0RPC*e@~7 zydTgBQj1;DgIkn!*d~<(9wFZb6}q8E@h0+@2?co0dmBa9Y~wbYpTtWvToOg-#N};; zleHDlu=M%KJXEDkdvOybaTo0O)7W6bTP@0NVwy?yceOHjzp2o8Z2QqTX43HUJ^w+) zVZcvzEoHSP4DLj;9Th*1?CIGutVV+7 zWz@bjuX63+g5R{I^`~mW)9(3D&>DoRr<1<57Y&Rou$kH=4VVu0^3HbsPV?nTRHsbZ zt%uDcXm;U4@8Y;FpEiSwr>{&FLpZZ7MpzU^t3gF9z}yfnPRA??H3{gfEz6(XhZl_l z6}r|I*xE5RXmL?Iaxvg)$8rN)lX%Il_*!6i2Dd@d!uI_-br#qj)a0(1!rHUK&0<}488t3KTabFS<-{7oT-}bb z3gxp`NdK3DTU>%D;3zmvF`%akS|W8Zc{sr5`ASK%Secz~-bscQ+!o|uUq~cjYxfe8 zpeQ0F3N1ZZs2_Wi77}FfS2Z;Ed1ca+6`7>Yj2_%W=#vmxq^tBk3yqFNn@m>VddfYG=Bpz309-^9O3Wt!oRgW;`k7u|V#r3o?J?LXPG_-+H+fp(x`obGE1MNux4v{{eLOf zICzKDTaPh`*|*EbnAJ&$nk6x9cn+2%JxP3uRtVJdG8pia-aa>=Tm2|YytD7`L+YN{ z1V+;A;!dk|vTe-AMSFJi*5NO=Nn2a=;UXjL{ye$lZ`y5&=33f16|VY7gDylmWAt*? zt+0e2G+M0S)q56BsomSr=OJ2enpFRM6v#Cjum_L&$aiz-SI5V;TU$Y&yhnVMxDXcp zwVgb0e3`DXTs>T~aIuctY};J0!PPLZdD8K^*Rp}`=q%IYh1;Q~@+V%~^4n@)hI0!o zg{t87^S+n)&NRmYciub$J$*pk19);}C5+6y%dU6Nn-IA~^?d#*n7pl)4 z+SDf_v)ZJtWHq`jlI`YFX*FZ};e~FicM+xhVMeVOl_*3O@v)Q(j{1@GUXr{d5zLGrep|y^sn&UmUv-^NOCejf7 zj$G&!v3=`Z8&55B06dB0?urT)BL0kAfATMf!8ZV?i$T=`{>=ERq0iL9fY`H7U&5@O z^gC!(esIv$+s+MJef_NvKs|{CsK<2QBr;x1;PZtazCH)60bh#q+jL2Ao8naxZoT4~ z)c?Wa>|^u!bG=alp}`LBf{uBk*QFTK*jir>)bD4P_7!j+4;i|@JjF2iTwK&$#&CU_ih#&?}^LC^;J%)>?-Gn-q^ zI7{vhPh7tz2dAVlSn_rqgV(rm-Vq$r_H-X!Y7b6c>Vy$ekz0wiwtIL`2%?-+s{AR! zke}fJy-f$eWd-E;?$rzD!G6`ahM*gvgij0**N@Pc46+Fv(|PCbsB|Bp9KHTiZ)H!D zJm~dFv#pojG{6()|12Rk>=pF!1!p+fgMbIb04O*84(HuGgY6%JRL5;MFz?)CEs z<#5CLi4IJ&^BtNN0)Cdn_0a!vacBthd)~7XwJ4&ZNcq5XFsmw_ZuVu-8?B#{ZY9qZ z-kS0M>P?roM)49g`I%8l$oAP*@){d%9@nav`1T($y{lRs6zt)tm$*-^PmN=_)-~wl zC-P*zQ=i;Z)KA@ZNvqVlL-TVe^K)QUy7}q@s7QGk6~eF#IcjW;-`(MjCHbO1gO+3# zQpy1NnK`g*3g43fHrnt}gde^vzr1e#-8i&E{<-g}?otPC^~#-jk)(=;Sj|korLeWY z))}ACqk>~pmMiimN&0o5FXZh%Hsj?q&@4K%3L=HW3Jd& zDtE|CW7Z~|3yzUlS;^^OPjb1zV`Qsemzi!_7Ys7KFxffNOjUi~YSPjcv)9=E$dA4^ zBpEeVKrO(X=6p$Gmp`C%=;e82w$BYXD{_-tVG*C{QOVbSu2pbvfPU~*+C{*VbXqw$ zBu~FDP-;xcF3gyBMKYvVuXdFyD@ljkgtRDQ!W2MKn>eHnQw6%-JczC$tMYr#d!6Am z?#jwa;D!u8r)@ssX-?=TW6n=-DpgypAWXftn3&)*f)>Nppa8b*@~T$1qB7L3Le2kc zg7P${$CzmHggjkm{WC;I(%nT*#bMG#`lN1vw5rP~*u@O39Ya8dW&lR~d=VJ@or&uU_&P!asp<&UlnY7kuM;cf; zv#*ouTUy;r8#LRZukY!WX)h{>|0WTsvz3l!-W04g?d|Cjo_0;th%|0=`e-0!S1h$I zkp4%C_uPl^i;da5V|ft$(mSMAVDh+~O=^Yy|-zuo?M7B`mV04dV=Ip7f# z>ex_cu_Ukchh#K1(YtW8D)mJF>`a({`-0pQ{PxLViLPy&0g3zF9qQ=eAg^rK!_$lI zd5SGW)QLFb6?C`c$&MQ?m>j|5Hb$OH`0MFzK6`vd4_%RqZEnvjo=S)aUkQJ1?qBpy z<;fS|o3wuUm^u~y)nTQ$aLGJ|fI6V7T@*E-7F2pF=n{xJF|=o=Y+YH}M~F*gdX~vy z0zlGT<~L;7&A9C5yvN8ufmiRQ@$%99yzlO|BKJS4F3}(FQ~aqIa14@VXZ%_n`o>Xh z=zf)47h{=cKrLfCr-jKRas-g?mk zF1!%^7BBNr7vT0&ePC3CQ`aP;a*Or`nc^LJO1cD=#U#2`E>=KqeprlkBqamGPYZ0E zEj(?S3>YS*l48c#pa87JeDrtQ8<%OJDHHK3N}v*=U8ezr$Nd4Y8-#0pd^2(@H7AA3 z*3`g9%>6}Jw65b*S1AW@qSLVh?24%~ZXAbKo^f)8*;7n$EfdJzrBd!v*S z0=aR$n8P5B@)j@L%zufW0q*ojkRBlfG=3aYKH`GnP(2#h%9fHIw&;{6-i%fn|DsMo zypJ4oeKT(O>w6p-+A8CF*{tHidf1zda(eat+owW|!q?Q^x;1AeMyrkFJBY)0i8}k3 z8y~K&_0prMdV~iWFx3RHCO7?1Ak$ z6=*ZVLHhhVI#L5DB#FgT z%v?Gpg=fL3xX#ten>u5q_kUKCw2EvY`U?jBX2TlH)mO!SKsD))K7d3JQEM zsK5MmLHvsnV0++#d%o!ibN<5Rta2XgJ+B^5r*MCDji#L0Ir(|6t)<87ua5bz={Wn; zKw&rGaG6VDo+#BN8U~-&IZ4Pkh#D@Y`#`m7H2k~VI+{$aSwY^Cy5wp9f`dfFc(SQA zEs&I1dE!W)RR(!y%#H+X>qpUD#(2^=Uq1FOFFF_{BN>DpJNJXz-KW$1l3XU+#Jk_X!%pyl7;iq zOMqPznGi34U_=)!!>}UC9!6PYCjZiFhlnAE70y+pO8=C?g_eiFdwj@KIc@bv0t2%g zMlI${jOQ)Iwoa_mjI*LWiiQ5ew@9-_Kh7`94u{GxO4z~mQ3-yHk~@H08WCl|Iv z)+#qL-hs)q^a#7ty_`Akz{F;bptB2xc;GhV`~#aSQ`j2e@Q)(w0Fd_Cw=t?xLD=@i z-5K@8?y81%b4Zlim$YhH)!bDS2cWw}V)>Q*#vxY~(^^NSTuTL)_bIGw;QBU9r8gOn z@^MAJCU0JDt-`w%nQjE#9oBUJGomJD4!*WHu?M($cP+!r`kKtrJ!(?LUpn+19<=Cfm~$Cr3g}sgH|0Z3ks>miU$=hlWfiKT&Y?a{dY@ERy;R zzmgqUp2tMwC2T`3Zh)`)^6}Z41dK!HLe2aCtm>AW^_R8cw724I+o(!rh6~?{sk-0v zm|J9|Qto|{41(rib>X!VSWe1HBFBPXRl+5fI;abB>0PNyDs7EoH^5TfWf1J8d?mX# z^I62;BMa5FN0wNg8tCn%`bMf#t9e)*g<5$x+ey>>R8fI!7Xw|dCtj?C7Dm>#r(Hxx zh|}rp9L1=bGmR;%B`oim4feX7-GcX~(tZ1zvCNfh_M~(5bI#Zc$9F_DCX5dN+&GDd z?y@!&Xj$cKdp>#jOcy{d!|`4({umPN2(7vdf5G>*7x<@k+AjRBuTBux(r;$15&G>1 zG%(ql>o1NQG7%+do@_AaZ97K46G!!t<=Cg~Am)mo+~B1eIs&;#&%A$#p+`;7yZkACa$-B|~>?@Rr- zDSPv;t1O8w4KEGQDKGc;R|17B34Fayh+Z(Ubb z{#g?qgWgsJT-kP>d?0Lntkk;}lDuI0Y#+qDoRrajcE^DAx9pl)An%snZKG;MZZ7Vh zR+LdXx7~f^dT|NNBW2sL$1{Il11>D5SR}uoJcL#6g*0?%;sfGYyRF!p?bL6%of(ocC@P+QDX$(#2Tjc%AEbXJ`ZIu@4hG#R~FTTcEjV&klV zPF0+DoPThGs2@*woJEtbIvUOp_IW>}px+K?gqANCtr22+p`)Wr&SpB|X_ul>$5L3&16fRB#UF^*<3TGs9RiKr%nnVAGPoXTg^*sZCFvyy3>_1UU@vhK zByea0XHDxSoy`w{H1W1m^b(MTgyA*@n+aKAN41!VHPnj!RZo4&{#*NUh~b7U#Ds~G zUo7%o$P>|DoV2ziPdHF@Mm2&L)Z6jEcBkH&5I7?03(Ib#C0Us;z~^-x>E6P2+KkaR zIG3Jb5;Vr!+`cWe`q}BDmt)v_oxk!24@SLVu;t~&f3x#3VaD2Lbn&)BK<8=GG8F_+ zPM-J6+bqYpkhd1ty57n5bwmOgo>F2=Med}-^A!X{$d%foo)4VJve$9|3iHG25r}Pn z<9^B^ay698P6|MN7U&2R$^_nzGV~8&(eS)%?^ph_&6m9NvQM|j=EtTiBS{p6TGs`Q z3_;Gxzt~+GGP73YZPOmB|Id9)ad@_!|7m?mE*;Q`74>{z2@K1B)m*{xJCpf>PDF*` zkwc=4+v(jOZ^t8DQ$9ZcMq1fmNvplG_El26N_USNB_b^2{%!k6}%*VU+$>FIu zZmNvuzf<}{f_&%BXu!4T{W1CW9rxg41X=CL z$ayt+9X5`1Ab@Us0vS*xNaTf@;jr1Lm|(KL7juirjhO=KcN~=LlVfL>^mlSre%ZZe z?MuOxssaAC?%iMFKNcf>bvz-;lJH}JJ_z@q%+rlxNjYKs$HANC&Q{!1>AzP3a&)4m zo1jmh6zO-IJg%Y8SlOs_tmEyb-ZzDG{a~>*WeLkfP1!<%?4CJmnQ=)hCX{Z25YI2A zLgB4lfB+88jEUkuOlH-#y{|LG?6@Xg-(~ENp}CeI-afREB8f{@jM=W3joqTD7_KRW zZpd5&g17UGzJwlQ8YC@;>I_vCC^*RBlA|kq@aJ<&C2gQv&PdI8jPn=O! z!JmHh4(?9376{a_+}JZ0t40{M+_o`eVE_4bbJk}jy#`Vo9;KgpRs3~c$aw*4l-u1?i);K~_N_>oW8Vl)PK}4o$5ocP zAi%@a<(FrPMv^f3iY^feODmVE5C-o0Cj4ejN9!KN+pF-&|4v2+E-BE=)%0@x$$FG$ z$Z3v#CZR1ngRyrq5Ug5tkJko0tbTc&j9gyqPoENl__w}VxX+9!vZa?~UkhQOf{g!q?_W9ppUhsG}pN|qcf|v1bOIOv) zp{J4OHd5@H)SUP%K5`ZuVB~m*FDP2cK&j>_xl~D&jB)38R^?}mt;+XnB zn%!1PFdL2hnx#K@q2XIXD$X-)AaOy%Wu09!L^QY1GvZyiC3b?aH?cpzjP>X&J^Q!R z-};5cTHiFu)Xa=~uFMdYT9 zlYBr@aYX6~TchNbML(IVaYW&dMziD-xP%4STYiS6zOxi=KojkTP!R4!L6zO&-DooQ zJxSJ?^v0-Vr+9>H{KNoAX&UwE+AR?cS5uD5s3h^V0Xy7c0DkPF4SqLkPM(0}!I6FX zH=zZRVlBr#$#1>zC*aHN82xm@f>;xm_ros^634MRH!F!NyizgU_akL1`b*_$_x-1Q zzt3J5KO5N#;{b>uwd=3x+0%+D>je_7_h-nD7~Zm~v|9fx+o#D`Hjwi{wzh6JVp)J( z#M~yoe7J;oA!#Bb=BD;E1O#@oJ?rFeLJuEgZYiOgiHwXtbK=Zb+r%vG+Txc`96wr2 zl*s!^rH5f2VD$z0NS#*pllp#K$5;@ zdE__>;AQRveFlVqv>4B@OCKKdZp3S&I1TIQzV2BOir0mAZ{A;(soZgDr(#eG_}M)- ztsU7GlGR;PO7*-*ps+L)L;fI@o)%B!&(I#G%zvC|N68;u#^qmXpC`gF!U;d!FBX%~-aX8jlHJMiZ;P_eg z&VrPp$r3;gsUzZ8ud!s4RjDq|Z}mh1X2=_-bzIxR&QD&g z<_bY>f0j5`<}Sd7N^NY=n_!%)`K1}9JyPcvJKtQm1b}gKFKb0}WJpIMV4FJXw_ZAy5&d7^RO4nx|_D)G=4 zabS5Uci!{suAnYkJ;0H-kJFik$|(p{VBRw6{MPB-3&AHmwdhaX`G({?)=ibj?^vJa z(5D^&*04#s)|WA={xkUtbKr%4CU$XM+8Ptiwm+th$7d8@9d+Wyl|vKk$q?}?gCBFG z)NzIEQtR~lMz=_Q@UYL*0_%lnDV^Z)Inp%jyaON8suFHUeIF@*?E3`>JV)qy&bY

j&*Uj{vnb(As~GOt6|+UuG$PK>@hrf7xdZn*|H|Tg)`)umkInCx z0yq3`~KNyoWaN0$v@JaBpX8le*@qnRVfFdHl`BQ6AXvf0$Ob0jx5 zR?h>w)UR#Dw;grN|8LFrpo#}&`h;JR8#W)-|N6mBzX$WiG2DrTrDkABa;7gV(xMNv zlF`%|6u1?W?{!~ar;<~wQAQj|f4J#5;gyUY6VCD6-TG8ZTbm4g=I{-q807LE_;H^d*YzmfEq5sVPgBKk|VR>eZ9c1zEY;y%9uAf9IEX(*$9O z3w?ZZNr?0?`F^QGvJ>_%B}n>8bt1Xf<5+cl^-vSs5-Onp)|Mmzm%zL45!(=y_Ad(^ z;kg4d_U7m>bblrP3)-)oNhsxnomR?rSsqbZUI^EV^+yb?z_dB8#KX!RU=~D+_%9*k zsUV|#}5=C8U{x|bWh@~*lV$NwV!fEeKtMBExcVih&eIRl=`h~fVYrj1Zw#>8lxV$UfTWc5U$%3k;%3JNPmSB8 z?TvrSm%ZBlpH21xdEwBjHGHOW0%EYdyzCvurm==fcXKR(om811@_yRt8p@V8P z^P+X>{|#(glbv__C2FFQRT7kf_(}p$aOYQkQ1d=Uj^yuw@v_RRA9njDd5W~9RfME% iHsV8T2zEjzQ9AbZa1(xiW*!S5ezet}s8v6*4gWvumcWYu literal 0 HcmV?d00001 diff --git a/Source/Android/res/drawable/gcpad_b.png b/Source/Android/res/drawable/gcpad_b.png new file mode 100644 index 0000000000000000000000000000000000000000..7445a3cb347b7cb396f32c4f85c119e8ccb2c2d1 GIT binary patch literal 171702 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 literal 0 HcmV?d00001 diff --git a/Source/Android/res/drawable/gcpad_b_pressed.png b/Source/Android/res/drawable/gcpad_b_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..bcd109725355d1898ce4ad34c7c4f6e98c2b1d3e GIT binary patch literal 126035 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 s~^k_VsE(xxcP-tWecoaYjM_qg+R$^x>Z% z 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