├── Resources └── Icon128.png ├── doc ├── images │ ├── ggpo_header.png │ ├── overview_image1.png │ ├── overview_image2.png │ ├── overview_image3.png │ ├── overview_image4.png │ ├── overview_image5.png │ └── ggpo_open_graph_cropped.png ├── README.ja.md ├── README.md ├── DeveloperGuide.ja.md └── DeveloperGuide.md ├── Source ├── ThirdParty │ └── EOSSDK │ │ └── .gitignore └── GGPOUE │ ├── Private │ ├── GGPOUE_Settings.cpp │ ├── GGPOUE.cpp │ ├── bitvector.h │ ├── log.h │ ├── platform_linux.h │ ├── platform_linux.cpp │ ├── platform_windows.cpp │ ├── static_buffer.h │ ├── timesync.h │ ├── GGPOGameInstance.cpp │ ├── ring_buffer.h │ ├── platform_windows.h │ ├── network │ │ ├── udp.h │ │ ├── udp_msg.h │ │ ├── udp.cpp │ │ ├── udp_proto.h │ │ └── udp_proto.cpp │ ├── bitvector.cpp │ ├── game_input.h │ ├── backends │ │ ├── backend.h │ │ ├── synctest.h │ │ ├── spectator.h │ │ ├── p2p.h │ │ ├── spectator.cpp │ │ ├── synctest.cpp │ │ └── p2p.cpp │ ├── input_queue.h │ ├── log.cpp │ ├── poll.h │ ├── types.h │ ├── game_input.cpp │ ├── timesync.cpp │ ├── sync.h │ ├── poll.cpp │ ├── main.cpp │ ├── sync.cpp │ ├── zconf.h │ └── input_queue.cpp │ ├── Public │ ├── GGPOUE.h │ ├── GGPOUE_Settings.h │ ├── GGPOGameInstance.h │ └── include │ │ └── ggponet.cpp │ └── GGPOUE.Build.cs ├── GGPOUE.uplugin ├── .gitignore ├── README.md └── LICENSE /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BwdYeti/GGPOUE/HEAD/Resources/Icon128.png -------------------------------------------------------------------------------- /doc/images/ggpo_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BwdYeti/GGPOUE/HEAD/doc/images/ggpo_header.png -------------------------------------------------------------------------------- /doc/images/overview_image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BwdYeti/GGPOUE/HEAD/doc/images/overview_image1.png -------------------------------------------------------------------------------- /doc/images/overview_image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BwdYeti/GGPOUE/HEAD/doc/images/overview_image2.png -------------------------------------------------------------------------------- /doc/images/overview_image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BwdYeti/GGPOUE/HEAD/doc/images/overview_image3.png -------------------------------------------------------------------------------- /doc/images/overview_image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BwdYeti/GGPOUE/HEAD/doc/images/overview_image4.png -------------------------------------------------------------------------------- /doc/images/overview_image5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BwdYeti/GGPOUE/HEAD/doc/images/overview_image5.png -------------------------------------------------------------------------------- /doc/images/ggpo_open_graph_cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BwdYeti/GGPOUE/HEAD/doc/images/ggpo_open_graph_cropped.png -------------------------------------------------------------------------------- /Source/ThirdParty/EOSSDK/.gitignore: -------------------------------------------------------------------------------- 1 | # EOS SDK folders are ignored, since they shouldn't be distributed and need to be downloaded from Epic. 2 | Bin/ 3 | Include/ 4 | Lib/ -------------------------------------------------------------------------------- /Source/GGPOUE/Private/GGPOUE_Settings.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 BwdYeti. 2 | 3 | 4 | #include "GGPOUE_Settings.h" 5 | 6 | UGGPOUE_Settings::UGGPOUE_Settings(const FObjectInitializer& obj) 7 | { 8 | CategoryName = "Plugins"; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /Source/GGPOUE/Public/GGPOUE.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Modules/ModuleManager.h" 7 | 8 | class FGGPOUEModule : public IModuleInterface 9 | { 10 | public: 11 | 12 | /** IModuleInterface implementation */ 13 | virtual void StartupModule() override; 14 | virtual void ShutdownModule() override; 15 | }; 16 | -------------------------------------------------------------------------------- /GGPOUE.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 8, 4 | "VersionName": "1.3", 5 | "FriendlyName": "GGPOUE", 6 | "Description": "A port of the GGPO netcode library for Unreal Engine", 7 | "Category": "Netcode", 8 | "CreatedBy": "BwdYeti", 9 | "CreatedByURL": "https://bwdyeti.com", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "IsBetaVersion": false, 15 | "IsExperimentalVersion": false, 16 | "Installed": false, 17 | "Modules": [ 18 | { 19 | "Name": "GGPOUE", 20 | "Type": "Runtime", 21 | "LoadingPhase": "Default" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /Source/GGPOUE/Private/GGPOUE.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "GGPOUE.h" 4 | 5 | #define LOCTEXT_NAMESPACE "FGGPOUEModule" 6 | 7 | void FGGPOUEModule::StartupModule() 8 | { 9 | // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module 10 | } 11 | 12 | void FGGPOUEModule::ShutdownModule() 13 | { 14 | // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, 15 | // we call this function before unloading the module. 16 | } 17 | 18 | #undef LOCTEXT_NAMESPACE 19 | 20 | IMPLEMENT_MODULE(FGGPOUEModule, GGPOUE) -------------------------------------------------------------------------------- /Source/GGPOUE/Private/bitvector.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _BITVECTOR_H 9 | #define _BITVECTOR_H 10 | 11 | #define BITVECTOR_NIBBLE_SIZE 8 12 | 13 | void BitVector_SetBit(uint8 *vector, int *offset); 14 | void BitVector_ClearBit(uint8 *vector, int *offset); 15 | void BitVector_WriteNibblet(uint8 *vector, int nibble, int *offset); 16 | int BitVector_ReadBit(uint8 *vector, int *offset); 17 | int BitVector_ReadNibblet(uint8 *vector, int *offset); 18 | 19 | #endif // _BITVECTOR_H 20 | -------------------------------------------------------------------------------- /Source/GGPOUE/Public/GGPOUE_Settings.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 BwdYeti. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "UObject/NoExportTypes.h" 7 | #include "log.h" 8 | #include "GGPOUE_Settings.generated.h" 9 | 10 | /** 11 | * 12 | */ 13 | UCLASS(Config = "GGPOSettings", meta = (DisplayName = "GGPO")) 14 | class GGPOUE_API UGGPOUE_Settings : public UDeveloperSettings 15 | { 16 | GENERATED_BODY() 17 | 18 | public: 19 | UGGPOUE_Settings(const FObjectInitializer& obj); 20 | 21 | UPROPERTY(Config, BlueprintReadOnly, EditAnywhere, Category = "Logging") 22 | bool LoggingEnabled = false; 23 | 24 | UPROPERTY(Config, BlueprintReadOnly, EditAnywhere, Category = "Logging") 25 | EGGPOLogVerbosity LogVerbosity = EGGPOLogVerbosity::Info; 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/log.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | UENUM(BlueprintType) 11 | enum class EGGPOLogVerbosity : uint8 12 | { 13 | Info = 0 UMETA(DisplayName = "Info"), 14 | Verbose = 1 UMETA(DisplayName = "Verbose"), 15 | VeryVerbose = 2 UMETA(DisplayName = "Very Verbose"), 16 | }; 17 | 18 | extern void Log(const char *fmt, ...); 19 | extern void Log(EGGPOLogVerbosity Verbosity, const char *fmt, ...); 20 | extern void Logv(EGGPOLogVerbosity Verbosity, const char *fmt, va_list list); 21 | 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build results 2 | Bin/ 3 | Binaries/ 4 | DerivedDataCache/ 5 | Intermediate/ 6 | Plugins/*/Intermediate/ 7 | Saved/ 8 | 9 | # Built data for maps 10 | *_BuiltData.uasset 11 | 12 | # Ignore visual studio directies and local files 13 | .vscode 14 | .vs 15 | *.VC.db 16 | *.log 17 | *.opensdf 18 | *.opendb 19 | *.sdf 20 | *.sln 21 | *.suo 22 | *.xcodeproj 23 | *.xcworkspace 24 | 25 | # Prerequisites 26 | *.d 27 | 28 | # Compiled Object files 29 | *.slo 30 | *.lo 31 | *.o 32 | *.obj 33 | 34 | # Precompiled Headers 35 | *.gch 36 | *.pch 37 | 38 | # Compiled Dynamic libraries 39 | *.so 40 | *.dylib 41 | *.dll 42 | 43 | # Fortran module files 44 | *.mod 45 | *.smod 46 | 47 | # Compiled Static libraries 48 | *.lai 49 | *.la 50 | *.a 51 | *.lib 52 | 53 | # Executables 54 | *.exe 55 | *.out 56 | *.app -------------------------------------------------------------------------------- /Source/GGPOUE/Private/platform_linux.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifdef __GNUC__ 9 | #ifndef _GGPO_LINUX_H_ 10 | #define _GGPO_LINUX_H_ 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | class Platform { 19 | public: // types 20 | typedef pid_t ProcessID; 21 | 22 | public: // functions 23 | static ProcessID GetProcessID() { return getpid(); } 24 | static void AssertFailed(char *msg) { } 25 | static uint32 GetCurrentTimeMS(); 26 | }; 27 | 28 | #endif 29 | #endif 30 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/platform_linux.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifdef __GNUC__ 9 | #include "platform_linux.h" 10 | 11 | struct timespec start = { 0 } 12 | 13 | uint32 Platform::GetCurrentTimeMS() { 14 | if (start.tv_sec == 0 && start.tv_nsec == 0) { 15 | clock_gettime(CLOCK_MONOTONIC, &start); 16 | return 0 17 | } 18 | struct timespec current; 19 | clock_gettime(CLOCK_MONOTONIC, ¤t); 20 | 21 | return ((current.tv_sec - start.tv_sec) * 1000) + 22 | ((current.tv_nsec - start.tv_nsec ) / 1000000) + 23 | } 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/platform_windows.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifdef _WINDOWS 9 | #include "platform_windows.h" 10 | 11 | int 12 | Platform::GetConfigInt(const char* name) 13 | { 14 | char buf[1024]; 15 | if (GetEnvironmentVariableA(name, buf, ARRAY_SIZE(buf)) == 0) { 16 | return 0; 17 | } 18 | return atoi(buf); 19 | } 20 | 21 | bool Platform::GetConfigBool(const char* name) 22 | { 23 | char buf[1024]; 24 | if (GetEnvironmentVariableA(name, buf, ARRAY_SIZE(buf)) == 0) { 25 | return false; 26 | } 27 | return atoi(buf) != 0 || _stricmp(buf, "true") == 0; 28 | } 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GGPOUE 2 | 3 | A port of [GGPO](http://ggpo.net) to an Unreal Engine plugin. 4 | 5 | ## Setup & Usage 6 | 7 | Add to the Plugins folder of your Unreal project. 8 | 9 | See [doc/README.md](doc/README.md), [doc/DeveloperGuide.md](doc/DeveloperGuide.md), and the [GGPO GitHub](https://github.com/pond3r/ggpo) for more information. 10 | 11 | ### Sample Application 12 | 13 | [VectorWar UE](https://github.com/BwdYeti/VectorWarUE) is a port of the GGPO sample game VectorWar, using GGPOUE for netcode. 14 | 15 | ### Issues 16 | 17 | Currently only usable with Windows, as the GGPO source and network layer depend on Win32 APIs. May be able to reuse some UE functionality for the underlying connection? 18 | 19 | ## Licensing 20 | 21 | GGPO is available under The MIT License. This means GGPO is free for commercial and non-commercial use. Attribution is not required, but appreciated. 22 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/static_buffer.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _STATIC_BUFFER_H 9 | #define _STATIC_BUFFER_H 10 | 11 | #include "types.h" 12 | 13 | template class StaticBuffer 14 | { 15 | public: 16 | StaticBuffer() : 17 | _size(0) { 18 | } 19 | 20 | T& operator[](int i) { 21 | ASSERT(i >= 0 && i < _size); 22 | return _elements[i]; 23 | } 24 | 25 | void push_back(const T &t) { 26 | ASSERT(_size != (N-1)); 27 | _elements[_size++] = t; 28 | } 29 | 30 | int size() { 31 | return _size; 32 | } 33 | 34 | 35 | protected: 36 | T _elements[N]; 37 | int _size; 38 | }; 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /Source/GGPOUE/Public/GGPOGameInstance.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 BwdYeti. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Engine/GameInstance.h" 7 | #include "GGPOGameInstance.generated.h" 8 | 9 | // Forward declarations 10 | class UGGPONetwork; 11 | 12 | /** 13 | * 14 | */ 15 | UCLASS() 16 | class GGPOUE_API UGGPOGameInstance : public UGameInstance 17 | { 18 | GENERATED_BODY() 19 | 20 | public: 21 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 22 | TObjectPtr NetworkAddresses; 23 | 24 | /// 25 | /// Creates a collection of network addresses. 26 | /// 27 | UFUNCTION(BlueprintCallable, Category = "GGPO") 28 | void CreateNetwork(int32 NumPlayers, int32 PlayerIndex, int32 LocalPort, TArray RemoteAddresses, TArray SpectatorAddresses); 29 | 30 | /// 31 | /// Creates a collection of network addresses for a spectator. 32 | /// 33 | UFUNCTION(BlueprintCallable, Category = "GGPO") 34 | void CreateSpectatorNetwork(int32 NumPlayers, int32 LocalPort, FString HostAddress); 35 | 36 | }; 37 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/timesync.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _TIMESYNC_H 9 | #define _TIMESYNC_H 10 | 11 | #include "types.h" 12 | #include "game_input.h" 13 | 14 | #define FRAME_WINDOW_SIZE 40 15 | #define MIN_UNIQUE_FRAMES 10 16 | #define MIN_FRAME_ADVANTAGE 3 17 | #define MAX_FRAME_ADVANTAGE 9 18 | #define BUFFER_SIZE 384 // Just over 6 seconds at 60fps 19 | 20 | class TimeSync { 21 | public: 22 | TimeSync(); 23 | virtual ~TimeSync (); 24 | 25 | void advance_frame(GameInput &input, int advantage, int radvantage); 26 | int recommend_frame_wait_duration(bool require_idle_input); 27 | 28 | protected: 29 | int _local[FRAME_WINDOW_SIZE]; 30 | int _remote[FRAME_WINDOW_SIZE]; 31 | GameInput _last_inputs[MIN_UNIQUE_FRAMES]; 32 | int _next_prediction; 33 | }; 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/GGPOGameInstance.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 BwdYeti. 2 | 3 | 4 | #include "GGPOGameInstance.h" 5 | #include "include/ggponet.h" 6 | 7 | void UGGPOGameInstance::CreateNetwork(int32 NumPlayers, int32 PlayerIndex, int32 LocalPort, TArray RemoteAddresses, TArray SpectatorAddresses) 8 | { 9 | // Add the spectators to the end of the remote players 10 | // CreateNetwork() will count out the NumPlayers, 11 | // and then handle the spectators after that 12 | RemoteAddresses.Append(SpectatorAddresses); 13 | TObjectPtr addresses = UGGPONetwork::CreateNetwork( 14 | this, 15 | FName(FString(TEXT("GGPONetwork"))), 16 | NumPlayers, 17 | PlayerIndex, 18 | LocalPort, 19 | RemoteAddresses); 20 | NetworkAddresses = addresses; 21 | } 22 | 23 | void UGGPOGameInstance::CreateSpectatorNetwork(int32 NumPlayers, int32 LocalPort, FString HostAddress) 24 | { 25 | TObjectPtr addresses = UGGPONetwork::CreateNetwork( 26 | this, 27 | FName(FString(TEXT("GGPONetwork"))), 28 | NumPlayers, 29 | -1, 30 | LocalPort, 31 | { HostAddress }); 32 | NetworkAddresses = addresses; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2009-2019 GroundStorm Studios, LLC. (http://ggpo.net) 4 | Copyright (c) 2020 BwdYeti 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/ring_buffer.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _RING_BUFFER_H 9 | #define _RING_BUFFER_H 10 | 11 | #include "types.h" 12 | 13 | template class RingBuffer 14 | { 15 | public: 16 | RingBuffer() : 17 | _head(0), 18 | _tail(0), 19 | _size(0) { 20 | } 21 | 22 | T &front() { 23 | ASSERT(_size != N); 24 | return _elements[_tail]; 25 | } 26 | 27 | T &item(int i) { 28 | ASSERT(i < _size); 29 | return _elements[(_tail + i) % N]; 30 | } 31 | 32 | void pop() { 33 | ASSERT(_size != N); 34 | _tail = (_tail + 1) % N; 35 | _size--; 36 | } 37 | 38 | void push(const T &t) { 39 | ASSERT(!full()); 40 | _elements[_head] = t; 41 | _head = (_head + 1) % N; 42 | _size++; 43 | } 44 | 45 | int size() { 46 | return _size; 47 | } 48 | 49 | bool empty() { 50 | return _size == 0; 51 | } 52 | 53 | bool full() { 54 | return _size == (N - 1); 55 | } 56 | 57 | protected: 58 | T _elements[N]; 59 | int _head; 60 | int _tail; 61 | int _size; 62 | }; 63 | 64 | #endif 65 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/platform_windows.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifdef _WINDOWS 9 | #ifndef _GGPO_WINDOWS_H_ 10 | #define _GGPO_WINDOWS_H_ 11 | 12 | // UE: allow Windows platform types; to avoid naming collisions 13 | // this must be undone at the bottom of this file 14 | #include "Windows/AllowWindowsPlatformTypes.h" 15 | #include "Windows/prewindowsapi.h" 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include "types.h" 23 | 24 | class Platform { 25 | public: // types 26 | typedef DWORD ProcessID; 27 | 28 | public: // functions 29 | static ProcessID GetProcessID() { return GetCurrentProcessId(); } 30 | static void AssertFailed(char *msg) { MessageBoxA(NULL, msg, "GGPO Assertion Failed", MB_OK | MB_ICONEXCLAMATION); } 31 | static uint32 GetCurrentTimeMS() { return timeGetTime(); } 32 | static int GetConfigInt(const char* name); 33 | static bool GetConfigBool(const char* name); 34 | }; 35 | 36 | // UE: disallow windows platform types 37 | // this was enabled at the top of the file 38 | #include "Windows/PostWindowsApi.h" 39 | #include "Windows/HideWindowsPlatformTypes.h" 40 | 41 | #endif 42 | #endif 43 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/network/udp.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _UDP_H 9 | #define _UDP_H 10 | 11 | #include "../poll.h" 12 | 13 | // Forward declarations 14 | struct UdpMsg; 15 | 16 | #define MAX_UDP_ENDPOINTS 16 17 | 18 | static const int MAX_UDP_PACKET_SIZE = 4096; 19 | 20 | class Udp : public IPollSink 21 | { 22 | public: 23 | struct Stats { 24 | int bytes_sent; 25 | int packets_sent; 26 | float kbps_sent; 27 | }; 28 | 29 | struct Callbacks { 30 | virtual ~Callbacks() { } 31 | virtual void OnMsg(sockaddr_in &from, UdpMsg *msg, int len) = 0; 32 | }; 33 | 34 | 35 | protected: 36 | void Log(EGGPOLogVerbosity Verbosity, const char *fmt, ...); 37 | 38 | public: 39 | Udp(); 40 | 41 | void Init(uint16 port, Poll *p, Callbacks *callbacks); 42 | 43 | void SendTo(char *buffer, int len, int flags, struct sockaddr *dst, int destlen); 44 | 45 | virtual bool OnLoopPoll(void *cookie); 46 | 47 | public: 48 | ~Udp(void); 49 | 50 | protected: 51 | // Network transmission information 52 | SOCKET _socket; 53 | 54 | // state management 55 | Callbacks *_callbacks; 56 | Poll *_poll; 57 | }; 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/bitvector.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "bitvector.h" 9 | #include "types.h" 10 | 11 | void 12 | BitVector_SetBit(uint8 *vector, int *offset) 13 | { 14 | vector[(*offset) / 8] |= (1 << ((*offset) % 8)); 15 | *offset += 1; 16 | } 17 | 18 | void 19 | BitVector_ClearBit(uint8 *vector, int *offset) 20 | { 21 | vector[(*offset) / 8] &= ~(1 << ((*offset) % 8)); 22 | *offset += 1; 23 | } 24 | 25 | void 26 | BitVector_WriteNibblet(uint8 *vector, int nibble, int *offset) 27 | { 28 | ASSERT(nibble < (1 << BITVECTOR_NIBBLE_SIZE)); 29 | for (int i = 0; i < BITVECTOR_NIBBLE_SIZE; i++) { 30 | if (nibble & (1 << i)) { 31 | BitVector_SetBit(vector, offset); 32 | } else { 33 | BitVector_ClearBit(vector, offset); 34 | } 35 | } 36 | } 37 | 38 | int 39 | BitVector_ReadBit(uint8 *vector, int *offset) 40 | { 41 | int retval = !!(vector[(*offset) / 8] & (1 << ((*offset) % 8))); 42 | *offset += 1; 43 | return retval; 44 | } 45 | 46 | int 47 | BitVector_ReadNibblet(uint8 *vector, int *offset) 48 | { 49 | int nibblet = 0; 50 | for (int i = 0; i < BITVECTOR_NIBBLE_SIZE; i++) { 51 | nibblet |= (BitVector_ReadBit(vector, offset) << i); 52 | } 53 | return nibblet; 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/game_input.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _GAMEINPUT_H 9 | #define _GAMEINPUT_H 10 | 11 | #include 12 | #include 13 | 14 | // GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS * 8 must be less than 15 | // 2^BITVECTOR_NIBBLE_SIZE (see bitvector.h) 16 | 17 | #define GAMEINPUT_MAX_BYTES 9 18 | #define GAMEINPUT_MAX_PLAYERS 2 19 | 20 | struct GameInput { 21 | enum Constants { 22 | NullFrame = -1 23 | }; 24 | int frame; 25 | int size; /* size in bytes of the entire input for all players */ 26 | char bits[GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS]; 27 | 28 | bool is_null() { return frame == NullFrame; } 29 | void init(int frame, char *bits, int size, int offset); 30 | void init(int frame, char *bits, int size); 31 | bool value(int i) const { return (bits[i/8] & (1 << (i%8))) != 0; } 32 | void set(int i) { bits[i/8] |= (1 << (i%8)); } 33 | void clear(int i) { bits[i/8] &= ~(1 << (i%8)); } 34 | void erase() { memset(bits, 0, sizeof(bits)); } 35 | void desc(char *buf, size_t buf_size, bool show_frame = true) const; 36 | void log(char *prefix, bool show_frame = true) const; 37 | bool equal(GameInput &input, bool bitsonly = false); 38 | }; 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /Source/GGPOUE/GGPOUE.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class GGPOUE : ModuleRules 6 | { 7 | public GGPOUE(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicIncludePaths.AddRange( 12 | new string[] { 13 | // ... add public include paths required here ... 14 | } 15 | ); 16 | 17 | PrivateIncludePaths.AddRange( 18 | new string[] { 19 | // ... add other private include paths required here ... 20 | } 21 | ); 22 | 23 | 24 | PublicDependencyModuleNames.AddRange(new string[] { "Core", "DeveloperSettings" }); 25 | 26 | PrivateDependencyModuleNames.AddRange(new string[] { "CoreUObject", "Engine", "InputCore" }); 27 | 28 | 29 | if (Target.Platform == UnrealTargetPlatform.Win64) 30 | { 31 | PublicDefinitions.Add("_WINDOWS"); 32 | } 33 | else if (Target.Platform == UnrealTargetPlatform.Mac) 34 | { 35 | PublicDefinitions.Add("MACOS"); 36 | } 37 | else if (Target.Platform == UnrealTargetPlatform.Linux) 38 | { 39 | PublicDefinitions.Add("__GNUC__"); 40 | } 41 | 42 | // Uncomment if you are using Slate UI 43 | // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); 44 | 45 | // Uncomment if you are using online features 46 | // PrivateDependencyModuleNames.Add("OnlineSubsystem"); 47 | 48 | 49 | DynamicallyLoadedModuleNames.AddRange( 50 | new string[] 51 | { 52 | // ... add any modules that your module loads dynamically here ... 53 | } 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/backends/backend.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _BACKEND_H 9 | #define _BACKEND_H 10 | 11 | #include "include/ggponet.h" 12 | #include "../log.h" 13 | 14 | struct GGPOSession { 15 | virtual ~GGPOSession() { } 16 | virtual GGPOErrorCode DoPoll(int timeout) { return GGPO_OK; } 17 | virtual GGPOErrorCode AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle) = 0; 18 | virtual GGPOErrorCode AddLocalInput(GGPOPlayerHandle player, void *values, int size) = 0; 19 | virtual GGPOErrorCode SyncInput(void *values, int size, int *disconnect_flags) = 0; 20 | virtual GGPOErrorCode IncrementFrame(void) { return GGPO_OK; } 21 | virtual GGPOErrorCode Chat(char *text) { return GGPO_OK; } 22 | virtual GGPOErrorCode DisconnectPlayer(GGPOPlayerHandle handle) { return GGPO_OK; } 23 | virtual GGPOErrorCode GetNetworkStats(FGGPONetworkStats *stats, GGPOPlayerHandle handle) { return GGPO_OK; } 24 | virtual GGPOErrorCode Logv(EGGPOLogVerbosity Verbosity, const char *fmt, va_list list) { ::Logv(Verbosity, fmt, list); return GGPO_OK; } 25 | 26 | virtual GGPOErrorCode SetFrameDelay(GGPOPlayerHandle player, int delay) { return GGPO_ERRORCODE_UNSUPPORTED; } 27 | virtual GGPOErrorCode SetDisconnectTimeout(int timeout) { return GGPO_ERRORCODE_UNSUPPORTED; } 28 | virtual GGPOErrorCode SetDisconnectNotifyStart(int timeout) { return GGPO_ERRORCODE_UNSUPPORTED; } 29 | virtual GGPOErrorCode TrySynchronizeLocal() { return GGPO_ERRORCODE_UNSUPPORTED; } 30 | }; 31 | 32 | typedef struct GGPOSession Quark, IQuarkBackend; /* XXX: nuke this */ 33 | 34 | #endif 35 | 36 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/input_queue.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _INPUT_QUEUE_H 9 | #define _INPUT_QUEUE_H 10 | 11 | #include "game_input.h" 12 | 13 | #define INPUT_QUEUE_LENGTH 128 14 | #define DEFAULT_INPUT_SIZE 4 15 | 16 | class InputQueue { 17 | public: 18 | InputQueue(int input_size = DEFAULT_INPUT_SIZE); 19 | ~InputQueue(); 20 | 21 | public: 22 | void Init(int id, int input_size); 23 | int GetLastConfirmedFrame(); 24 | int GetFirstIncorrectFrame(); 25 | int GetLength() { return _length; } 26 | 27 | void SetFrameDelay(int delay) { _frame_delay = delay; } 28 | void ResetPrediction(int frame); 29 | void DiscardConfirmedFrames(int frame); 30 | bool GetConfirmedInput(int frame, GameInput *input); 31 | bool GetInput(int frame, GameInput *input); 32 | void AddInput(GameInput &input); 33 | 34 | protected: 35 | int AdvanceQueueHead(int frame); 36 | void AddDelayedInputToQueue(GameInput &input, int i); 37 | void Log(const char *fmt, ...); 38 | 39 | protected: 40 | int _id; 41 | int _head; 42 | int _tail; 43 | int _length; 44 | bool _first_frame; 45 | 46 | int _last_user_added_frame; 47 | int _last_added_frame; 48 | int _first_incorrect_frame; 49 | int _last_frame_requested; 50 | 51 | int _frame_delay; 52 | 53 | GameInput _inputs[INPUT_QUEUE_LENGTH]; 54 | GameInput _prediction; 55 | }; 56 | 57 | #endif 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/log.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "log.h" 9 | #include "types.h" 10 | #include "GGPOUE_Settings.h" 11 | 12 | static char logbuf[4 * 1024 * 1024]; 13 | 14 | void Log(const char *fmt, ...) 15 | { 16 | va_list args; 17 | va_start(args, fmt); 18 | Logv(EGGPOLogVerbosity::VeryVerbose, fmt, args); 19 | va_end(args); 20 | } 21 | void Log(EGGPOLogVerbosity Verbosity, const char *fmt, ...) 22 | { 23 | va_list args; 24 | va_start(args, fmt); 25 | Logv(Verbosity, fmt, args); 26 | va_end(args); 27 | } 28 | 29 | void Logv(EGGPOLogVerbosity Verbosity, const char *fmt, va_list args) 30 | { 31 | #if WITH_EDITOR 32 | if (GIsEditor) 33 | { 34 | // Get the settings object 35 | // Return if logging is not enabled 36 | UGGPOUE_Settings* Settings = GetMutableDefault(); 37 | if (!Settings->LoggingEnabled) 38 | return; 39 | 40 | // If the message is too verbose for the log settings, return 41 | if (Verbosity > Settings->LogVerbosity) 42 | return; 43 | 44 | // Apply the string format 45 | vsprintf_s(logbuf, ARRAY_SIZE(logbuf), fmt, args); 46 | FString Message = FString(strlen(logbuf), logbuf); 47 | 48 | Message.InsertAt(0, FString::Printf(TEXT("GGPO :: "), GPlayInEditorID)); 49 | // If this is an instance playing in the editor, include its Id 50 | if (GPlayInEditorID >= 0) 51 | { 52 | Message.InsertAt(0, FString::Printf(TEXT("PIE %d-"), GPlayInEditorID)); 53 | } 54 | 55 | UE_LOG(LogNet, Display, TEXT("%s"), *Message); 56 | } 57 | #endif 58 | } 59 | 60 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/backends/synctest.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _SYNCTEST_H 9 | #define _SYNCTEST_H 10 | 11 | #include "backend.h" 12 | #include "../sync.h" 13 | #include "../ring_buffer.h" 14 | 15 | class SyncTestBackend : public IQuarkBackend { 16 | public: 17 | SyncTestBackend(GGPOSessionCallbacks *cb, char *gamename, int frames, int num_players); 18 | virtual ~SyncTestBackend(); 19 | 20 | virtual GGPOErrorCode DoPoll(int timeout); 21 | virtual GGPOErrorCode AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle); 22 | virtual GGPOErrorCode AddLocalInput(GGPOPlayerHandle player, void *values, int size); 23 | virtual GGPOErrorCode SyncInput(void *values, int size, int *disconnect_flags); 24 | virtual GGPOErrorCode IncrementFrame(void); 25 | virtual GGPOErrorCode Logv(EGGPOLogVerbosity Verbosity, char *fmt, va_list list); 26 | 27 | protected: 28 | struct SavedInfo { 29 | int frame; 30 | int checksum; 31 | char *buf; 32 | int cbuf; 33 | GameInput input; 34 | }; 35 | 36 | void RaiseSyncError(const char *fmt, ...); 37 | void BeginLog(int saving); 38 | void EndLog(); 39 | void LogSaveStates(SavedInfo &info); 40 | 41 | protected: 42 | GGPOSessionCallbacks _callbacks; 43 | Sync _sync; 44 | int _num_players; 45 | int _check_distance; 46 | int _last_verified; 47 | bool _rollingback; 48 | bool _running; 49 | FILE *_logfp; 50 | char _game[128]; 51 | 52 | GameInput _current_input; 53 | GameInput _last_input; 54 | RingBuffer _saved_frames; 55 | }; 56 | 57 | #endif 58 | 59 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/poll.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _POLL_H 9 | #define _POLL_H 10 | 11 | #include "static_buffer.h" 12 | 13 | #define MAX_POLLABLE_HANDLES 64 14 | 15 | 16 | class IPollSink { 17 | public: 18 | virtual ~IPollSink() { } 19 | virtual bool OnHandlePoll(void *) { return true; } 20 | virtual bool OnMsgPoll(void *) { return true; } 21 | virtual bool OnPeriodicPoll(void *, int ) { return true; } 22 | virtual bool OnLoopPoll(void *) { return true; } 23 | }; 24 | 25 | class Poll { 26 | public: 27 | Poll(void); 28 | void RegisterHandle(IPollSink *sink, HANDLE h, void *cookie = NULL); 29 | void RegisterMsgLoop(IPollSink *sink, void *cookie = NULL); 30 | void RegisterPeriodic(IPollSink *sink, int interval, void *cookie = NULL); 31 | void RegisterLoop(IPollSink *sink, void *cookie = NULL); 32 | 33 | void Run(); 34 | bool Pump(int timeout); 35 | 36 | protected: 37 | int ComputeWaitTime(int elapsed); 38 | 39 | struct PollSinkCb { 40 | IPollSink *sink; 41 | void *cookie; 42 | PollSinkCb() : sink(NULL), cookie(NULL) { } 43 | PollSinkCb(IPollSink *s, void *c) : sink(s), cookie(c) { } 44 | }; 45 | 46 | struct PollPeriodicSinkCb : public PollSinkCb { 47 | int interval; 48 | int last_fired; 49 | PollPeriodicSinkCb() : PollSinkCb(NULL, NULL), interval(0), last_fired(0) { } 50 | PollPeriodicSinkCb(IPollSink *s, void *c, int i) : 51 | PollSinkCb(s, c), interval(i), last_fired(0) { } 52 | }; 53 | 54 | int _start_time; 55 | int _handle_count; 56 | HANDLE _handles[MAX_POLLABLE_HANDLES]; 57 | PollSinkCb _handle_sinks[MAX_POLLABLE_HANDLES]; 58 | 59 | StaticBuffer _msg_sinks; 60 | StaticBuffer _loop_sinks; 61 | StaticBuffer _periodic_sinks; 62 | }; 63 | 64 | #endif 65 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/types.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _TYPES_H 9 | #define _TYPES_H 10 | /* 11 | * Keep the compiler happy 12 | */ 13 | 14 | /* 15 | * Disable specific compiler warnings 16 | * 4018 - '<' : signed/unsigned mismatch 17 | * 4100 - 'xxx' : unreferenced formal parameter 18 | * 4127 - conditional expression is constant 19 | * 4201 - nonstandard extension used : nameless struct/union 20 | * 4389 - '!=' : signed/unsigned mismatch 21 | * 4800 - 'int' : forcing value to bool 'true' or 'false' (performance warning) 22 | */ 23 | #pragma warning(disable: 4018 4100 4127 4201 4389 4800) 24 | 25 | /* 26 | * Simple types 27 | */ 28 | typedef unsigned char uint8; 29 | typedef unsigned short uint16; 30 | typedef unsigned int uint32; 31 | typedef unsigned char byte; 32 | typedef signed char int8; 33 | typedef short int16; 34 | typedef int int32; 35 | 36 | /* 37 | * Additional headers 38 | */ 39 | #if defined(_WINDOWS) 40 | # include "platform_windows.h" 41 | #elif defined(__GNUC__) 42 | # include "platform_linux.h" 43 | #else 44 | # error Unsupported platform 45 | #endif 46 | 47 | #include "log.h" 48 | 49 | 50 | 51 | /* 52 | * Macros 53 | */ 54 | #define ASSERT(x) \ 55 | do { \ 56 | if (!(x)) { \ 57 | char assert_buf[1024]; \ 58 | snprintf(assert_buf, sizeof(assert_buf) - 1, "Assertion: %s @ %s:%d (pid:%lu)", #x, __FILE__, __LINE__, Platform::GetProcessID()); \ 59 | ::Log("%s\n", assert_buf); \ 60 | Platform::AssertFailed(assert_buf); \ 61 | exit(0); \ 62 | } \ 63 | } while (false) 64 | 65 | #ifndef ARRAY_SIZE 66 | # define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) 67 | #endif 68 | 69 | #ifndef MAX_INT 70 | # define MAX_INT 0xEFFFFFF 71 | #endif 72 | 73 | #ifndef MAX 74 | # define MAX(x, y) (((x) > (y)) ? (x) : (y)) 75 | #endif 76 | 77 | #ifndef MIN 78 | # define MIN(x, y) (((x) < (y)) ? (x) : (y)) 79 | #endif 80 | 81 | #endif // _TYPES_H 82 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/game_input.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "game_input.h" 9 | #include "types.h" 10 | #include "log.h" 11 | 12 | void 13 | GameInput::init(int iframe, char *ibits, int isize, int offset) 14 | { 15 | ASSERT(isize); 16 | ASSERT(isize <= GAMEINPUT_MAX_BYTES); 17 | frame = iframe; 18 | size = isize; 19 | memset(bits, 0, sizeof(bits)); 20 | if (ibits) { 21 | memcpy(bits + (offset * isize), ibits, isize); 22 | } 23 | } 24 | 25 | void 26 | GameInput::init(int iframe, char *ibits, int isize) 27 | { 28 | ASSERT(isize); 29 | ASSERT(isize <= GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS); 30 | frame = iframe; 31 | size = isize; 32 | memset(bits, 0, sizeof(bits)); 33 | if (ibits) { 34 | memcpy(bits, ibits, isize); 35 | } 36 | } 37 | 38 | void 39 | GameInput::desc(char *buf, size_t buf_size, bool show_frame) const 40 | { 41 | ASSERT(size); 42 | size_t remaining = buf_size; 43 | if (show_frame) { 44 | remaining -= sprintf_s(buf, buf_size, "(frame:%d size:%d ", frame, size); 45 | } else { 46 | remaining -= sprintf_s(buf, buf_size, "(size:%d ", size); 47 | } 48 | 49 | for (int i = 0; i < size * 8; i++) { 50 | char buf2[16]; 51 | if (value(i)) { 52 | int c = sprintf_s(buf2, ARRAY_SIZE(buf2), "%2d ", i); 53 | strncat_s(buf, remaining, buf2, ARRAY_SIZE(buf2)); 54 | remaining -= c; 55 | } 56 | } 57 | strncat_s(buf, remaining, ")", 1); 58 | } 59 | 60 | void 61 | GameInput::log(char *prefix, bool show_frame) const 62 | { 63 | char buf[1024]; 64 | size_t c = strlen(prefix); 65 | strcpy_s(buf, prefix); 66 | desc(buf + c, ARRAY_SIZE(buf) - c, show_frame); 67 | strncat_s(buf, ARRAY_SIZE(buf) - strlen(buf), "\n", 1); 68 | Log(buf); 69 | } 70 | 71 | bool 72 | GameInput::equal(GameInput &other, bool bitsonly) 73 | { 74 | if (!bitsonly && frame != other.frame) { 75 | Log("frames don't match: %d, %d\n", frame, other.frame); 76 | } 77 | if (size != other.size) { 78 | Log("sizes don't match: %d, %d\n", size, other.size); 79 | } 80 | if (memcmp(bits, other.bits, size)) { 81 | Log("bits don't match\n"); 82 | } 83 | ASSERT(size && other.size); 84 | return (bitsonly || frame == other.frame) && 85 | size == other.size && 86 | memcmp(bits, other.bits, size) == 0; 87 | } 88 | 89 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/backends/spectator.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _SPECTATOR_H 9 | #define _SPECTATOR_H 10 | 11 | #include "../types.h" 12 | #include "../poll.h" 13 | #include "backend.h" 14 | #include "../network/udp_proto.h" 15 | 16 | // Spectators will wait forever for the host when the host is behind 17 | // This value is important for how far behind the spectator can be 18 | #define SPECTATOR_FRAME_BUFFER_SIZE BUFFER_SIZE 19 | 20 | class SpectatorBackend : public IQuarkBackend, IPollSink, Udp::Callbacks { 21 | public: 22 | SpectatorBackend(GGPOSessionCallbacks *cb, const char *gamename, uint16 localport, int num_players, int input_size, char *hostip, u_short hostport); 23 | virtual ~SpectatorBackend(); 24 | 25 | 26 | public: 27 | virtual GGPOErrorCode DoPoll(int timeout); 28 | virtual GGPOErrorCode AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle) { return GGPO_ERRORCODE_UNSUPPORTED; } 29 | virtual GGPOErrorCode AddLocalInput(GGPOPlayerHandle player, void *values, int size) { return GGPO_OK; } 30 | virtual GGPOErrorCode SyncInput(void *values, int size, int *disconnect_flags); 31 | virtual GGPOErrorCode IncrementFrame(void); 32 | virtual GGPOErrorCode DisconnectPlayer(GGPOPlayerHandle handle) { return GGPO_ERRORCODE_UNSUPPORTED; } 33 | virtual GGPOErrorCode GetNetworkStats(FGGPONetworkStats* stats, GGPOPlayerHandle handle); 34 | virtual GGPOErrorCode SetFrameDelay(GGPOPlayerHandle player, int delay) { return GGPO_ERRORCODE_UNSUPPORTED; } 35 | virtual GGPOErrorCode SetDisconnectTimeout(int timeout) { return GGPO_ERRORCODE_UNSUPPORTED; } 36 | virtual GGPOErrorCode SetDisconnectNotifyStart(int timeout) { return GGPO_ERRORCODE_UNSUPPORTED; } 37 | virtual GGPOErrorCode TrySynchronizeLocal() { return GGPO_ERRORCODE_UNSUPPORTED; } 38 | 39 | public: 40 | virtual void OnMsg(sockaddr_in &from, UdpMsg *msg, int len); 41 | 42 | protected: 43 | void PollUdpProtocolEvents(void); 44 | void CheckInitialSync(void); 45 | 46 | void OnUdpProtocolEvent(UdpProtocol::Event &e); 47 | 48 | protected: 49 | GGPOSessionCallbacks _callbacks; 50 | Poll _poll; 51 | Udp _udp; 52 | UdpProtocol _host; 53 | bool _synchronizing; 54 | int _input_size; 55 | int _num_players; 56 | int _next_input_to_send; 57 | GameInput _inputs[SPECTATOR_FRAME_BUFFER_SIZE]; 58 | }; 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/timesync.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "timesync.h" 9 | 10 | TimeSync::TimeSync() 11 | { 12 | memset(_local, 0, sizeof(_local)); 13 | memset(_remote, 0, sizeof(_remote)); 14 | _next_prediction = FRAME_WINDOW_SIZE * 3; 15 | } 16 | 17 | TimeSync::~TimeSync() 18 | { 19 | } 20 | 21 | void 22 | TimeSync::advance_frame(GameInput &input, int advantage, int radvantage) 23 | { 24 | // Remember the last frame and frame advantage 25 | _last_inputs[input.frame % ARRAY_SIZE(_last_inputs)] = input; 26 | _local[input.frame % ARRAY_SIZE(_local)] = advantage; 27 | _remote[input.frame % ARRAY_SIZE(_remote)] = radvantage; 28 | } 29 | 30 | int 31 | TimeSync::recommend_frame_wait_duration(bool require_idle_input) 32 | { 33 | // Average our local and remote frame advantages 34 | int i, sum = 0; 35 | float advantage, radvantage; 36 | for (i = 0; i < ARRAY_SIZE(_local); i++) { 37 | sum += _local[i]; 38 | } 39 | advantage = sum / (float)ARRAY_SIZE(_local); 40 | 41 | sum = 0; 42 | for (i = 0; i < ARRAY_SIZE(_remote); i++) { 43 | sum += _remote[i]; 44 | } 45 | radvantage = sum / (float)ARRAY_SIZE(_remote); 46 | 47 | static int count = 0; 48 | count++; 49 | 50 | // See if someone should take action. The person furthest ahead 51 | // needs to slow down so the other user can catch up. 52 | // Only do this if both clients agree on who's ahead!! 53 | if (advantage >= radvantage) { 54 | return 0; 55 | } 56 | 57 | // Both clients agree that we're the one ahead. Split 58 | // the difference between the two to figure out how long to 59 | // sleep for. 60 | int sleep_frames = (int)(((radvantage - advantage) / 2) + 0.5); 61 | 62 | Log("iteration %d: sleep frames is %d\n", count, sleep_frames); 63 | 64 | // Some things just aren't worth correcting for. Make sure 65 | // the difference is relevant before proceeding. 66 | if (sleep_frames < MIN_FRAME_ADVANTAGE) { 67 | return 0; 68 | } 69 | 70 | // Make sure our input had been "idle enough" before recommending 71 | // a sleep. This tries to make the emulator sleep while the 72 | // user's input isn't sweeping in arcs (e.g. fireball motions in 73 | // Street Fighter), which could cause the player to miss moves. 74 | if (require_idle_input) { 75 | for (i = 1; i < ARRAY_SIZE(_last_inputs); i++) { 76 | if (!_last_inputs[i].equal(_last_inputs[0], true)) { 77 | Log("iteration %d: rejecting due to input stuff at position %d...!!!\n", count, i); 78 | return 0; 79 | } 80 | } 81 | } 82 | 83 | // Success!!! Recommend the number of frames to sleep and adjust 84 | return MIN(sleep_frames, MAX_FRAME_ADVANTAGE); 85 | } 86 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/sync.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _SYNC_H 9 | #define _SYNC_H 10 | 11 | #include "types.h" 12 | #include "include/ggponet.h" 13 | #include "game_input.h" 14 | #include "input_queue.h" 15 | #include "ring_buffer.h" 16 | #include "network/udp_msg.h" 17 | 18 | #define MAX_PREDICTION_FRAMES 8 19 | 20 | class SyncTestBackend; 21 | 22 | class Sync { 23 | public: 24 | struct Config { 25 | GGPOSessionCallbacks callbacks; 26 | int num_prediction_frames; 27 | int num_players; 28 | int input_size; 29 | }; 30 | struct Event { 31 | enum { 32 | ConfirmedInput, 33 | } type; 34 | union { 35 | struct { 36 | GameInput input; 37 | } confirmedInput; 38 | } u; 39 | }; 40 | 41 | public: 42 | Sync(UdpMsg::connect_status *connect_status); 43 | virtual ~Sync(); 44 | 45 | void Init(Config &config); 46 | 47 | void SetLastConfirmedFrame(int frame); 48 | void SetFrameDelay(int queue, int delay); 49 | bool AddLocalInput(int queue, GameInput &input); 50 | void AddRemoteInput(int queue, GameInput &input); 51 | int GetConfirmedInputs(void *values, int size, int frame); 52 | int SynchronizeInputs(void *values, int size); 53 | 54 | void CheckSimulation(int timeout); 55 | void AdjustSimulation(int seek_to); 56 | void IncrementFrame(void); 57 | 58 | int GetFrameCount() { return _framecount; } 59 | bool InRollback() { return _rollingback; } 60 | 61 | bool GetEvent(Event &e); 62 | 63 | protected: 64 | friend SyncTestBackend; 65 | 66 | struct SavedFrame { 67 | byte *buf; 68 | int cbuf; 69 | int frame; 70 | int checksum; 71 | SavedFrame() : buf(NULL), cbuf(0), frame(-1), checksum(0) { } 72 | }; 73 | struct SavedState { 74 | SavedFrame frames[MAX_PREDICTION_FRAMES + 2]; 75 | int head; 76 | }; 77 | 78 | void LoadFrame(int frame); 79 | void SaveCurrentFrame(); 80 | int FindSavedFrameIndex(int frame); 81 | SavedFrame &GetLastSavedFrame(); 82 | 83 | bool CreateQueues(Config &config); 84 | bool CheckSimulationConsistency(int *seekTo); 85 | void ResetPrediction(int frameNumber); 86 | 87 | protected: 88 | GGPOSessionCallbacks _callbacks; 89 | SavedState _savedstate; 90 | Config _config; 91 | 92 | bool _rollingback; 93 | int _last_confirmed_frame; 94 | int _framecount; 95 | int _max_prediction_frames; 96 | 97 | InputQueue *_input_queues; 98 | 99 | RingBuffer _event_queue; 100 | UdpMsg::connect_status *_local_connect_status; 101 | }; 102 | 103 | #endif 104 | 105 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/network/udp_msg.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _UDP_MSG_H 9 | #define _UDP_MSG_H 10 | 11 | #define MAX_COMPRESSED_BITS 4096 12 | #define UDP_MSG_MAX_PLAYERS 4 13 | 14 | #pragma pack(push, 1) 15 | 16 | struct UdpMsg 17 | { 18 | enum MsgType { 19 | Invalid = 0, 20 | SyncRequest = 1, 21 | SyncReply = 2, 22 | Input = 3, 23 | QualityReport = 4, 24 | QualityReply = 5, 25 | KeepAlive = 6, 26 | InputAck = 7, 27 | }; 28 | 29 | struct connect_status { 30 | unsigned int disconnected:1; 31 | int last_frame:31; 32 | }; 33 | 34 | struct { 35 | uint16 magic; 36 | uint16 sequence_number; 37 | uint8 type; /* packet type */ 38 | } hdr; 39 | union { 40 | struct { 41 | uint32 random_request; /* please reply back with this random data */ 42 | uint16 remote_magic; 43 | uint8 remote_endpoint; 44 | } sync_request; 45 | 46 | struct { 47 | uint32 random_reply; /* OK, here's your random data back */ 48 | } sync_reply; 49 | 50 | struct { 51 | int8 frame_advantage; /* what's the other guy's frame advantage? */ 52 | uint32 ping; 53 | } quality_report; 54 | 55 | struct { 56 | uint32 pong; 57 | } quality_reply; 58 | 59 | struct { 60 | connect_status peer_connect_status[UDP_MSG_MAX_PLAYERS]; 61 | 62 | uint32 start_frame; 63 | 64 | int disconnect_requested:1; 65 | int ack_frame:31; 66 | 67 | uint16 num_bits; 68 | uint8 input_size; // XXX: shouldn't be in every single packet! 69 | uint8 bits[MAX_COMPRESSED_BITS]; /* must be last */ 70 | } input; 71 | 72 | struct { 73 | int ack_frame:31; 74 | } input_ack; 75 | 76 | } u; 77 | 78 | public: 79 | int PacketSize() { 80 | return sizeof(hdr) + PayloadSize(); 81 | } 82 | 83 | int PayloadSize() { 84 | int size; 85 | 86 | switch (hdr.type) { 87 | case SyncRequest: return sizeof(u.sync_request); 88 | case SyncReply: return sizeof(u.sync_reply); 89 | case QualityReport: return sizeof(u.quality_report); 90 | case QualityReply: return sizeof(u.quality_reply); 91 | case InputAck: return sizeof(u.input_ack); 92 | case KeepAlive: return 0; 93 | case Input: 94 | size = (int)((char *)&u.input.bits - (char *)&u.input); 95 | size += (u.input.num_bits + 7) / 8; 96 | return size; 97 | } 98 | ASSERT(false); 99 | return 0; 100 | } 101 | 102 | UdpMsg(MsgType t) { hdr.type = (uint8)t; } 103 | }; 104 | 105 | #pragma pack(pop) 106 | 107 | #endif 108 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/poll.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "poll.h" 9 | #include "types.h" 10 | 11 | Poll::Poll(void) : 12 | _handle_count(0), 13 | _start_time(0) 14 | { 15 | /* 16 | * Create a dummy handle to simplify things. 17 | */ 18 | _handles[_handle_count++] = CreateEvent(NULL, true, false, NULL); 19 | } 20 | 21 | void 22 | Poll::RegisterHandle(IPollSink *sink, HANDLE h, void *cookie) 23 | { 24 | ASSERT(_handle_count < MAX_POLLABLE_HANDLES - 1); 25 | 26 | _handles[_handle_count] = h; 27 | _handle_sinks[_handle_count] = PollSinkCb(sink, cookie); 28 | _handle_count++; 29 | } 30 | 31 | void 32 | Poll::RegisterMsgLoop(IPollSink *sink, void *cookie) 33 | { 34 | _msg_sinks.push_back(PollSinkCb(sink, cookie)); 35 | } 36 | 37 | void 38 | Poll::RegisterLoop(IPollSink *sink, void *cookie) 39 | { 40 | _loop_sinks.push_back(PollSinkCb(sink, cookie)); 41 | } 42 | void 43 | Poll::RegisterPeriodic(IPollSink *sink, int interval, void *cookie) 44 | { 45 | _periodic_sinks.push_back(PollPeriodicSinkCb(sink, cookie, interval)); 46 | } 47 | 48 | void 49 | Poll::Run() 50 | { 51 | while (Pump(100)) { 52 | continue; 53 | } 54 | } 55 | 56 | bool 57 | Poll::Pump(int timeout) 58 | { 59 | int i, res; 60 | bool finished = false; 61 | 62 | if (_start_time == 0) { 63 | _start_time = Platform::GetCurrentTimeMS(); 64 | } 65 | int elapsed = Platform::GetCurrentTimeMS() - _start_time; 66 | int maxwait = ComputeWaitTime(elapsed); 67 | if (maxwait != INFINITE) { 68 | timeout = MIN(timeout, maxwait); 69 | } 70 | 71 | res = WaitForMultipleObjects(_handle_count, _handles, false, timeout); 72 | if (res >= WAIT_OBJECT_0 && res < WAIT_OBJECT_0 + _handle_count) { 73 | i = res - WAIT_OBJECT_0; 74 | finished = !_handle_sinks[i].sink->OnHandlePoll(_handle_sinks[i].cookie) || finished; 75 | } 76 | for (i = 0; i < _msg_sinks.size(); i++) { 77 | PollSinkCb &cb = _msg_sinks[i]; 78 | finished = !cb.sink->OnMsgPoll(cb.cookie) || finished; 79 | } 80 | 81 | for (i = 0; i < _periodic_sinks.size(); i++) { 82 | PollPeriodicSinkCb &cb = _periodic_sinks[i]; 83 | if (cb.interval + cb.last_fired <= elapsed) { 84 | cb.last_fired = (elapsed / cb.interval) * cb.interval; 85 | finished = !cb.sink->OnPeriodicPoll(cb.cookie, cb.last_fired) || finished; 86 | } 87 | } 88 | 89 | for (i = 0; i < _loop_sinks.size(); i++) { 90 | PollSinkCb &cb = _loop_sinks[i]; 91 | finished = !cb.sink->OnLoopPoll(cb.cookie) || finished; 92 | } 93 | return finished; 94 | } 95 | 96 | int 97 | Poll::ComputeWaitTime(int elapsed) 98 | { 99 | int waitTime = INFINITE; 100 | size_t count = _periodic_sinks.size(); 101 | 102 | if (count > 0) { 103 | for (int i = 0; i < count; i++) { 104 | PollPeriodicSinkCb &cb = _periodic_sinks[i]; 105 | int timeout = (cb.interval + cb.last_fired) - elapsed; 106 | if (waitTime == INFINITE || (timeout < waitTime)) { 107 | waitTime = MAX(timeout, 0); 108 | } 109 | } 110 | } 111 | return waitTime; 112 | } 113 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/backends/p2p.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _P2P_H 9 | #define _P2P_H 10 | 11 | #include "../types.h" 12 | #include "../poll.h" 13 | #include "../sync.h" 14 | #include "backend.h" 15 | #include "../network/udp_proto.h" 16 | 17 | class Peer2PeerBackend : public IQuarkBackend, IPollSink, Udp::Callbacks { 18 | public: 19 | Peer2PeerBackend(GGPOSessionCallbacks *cb, const char *gamename, uint16 localport, int num_players, int input_size); 20 | virtual ~Peer2PeerBackend(); 21 | 22 | 23 | public: 24 | virtual GGPOErrorCode DoPoll(int timeout); 25 | virtual GGPOErrorCode AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle); 26 | virtual GGPOErrorCode AddLocalInput(GGPOPlayerHandle player, void *values, int size); 27 | virtual GGPOErrorCode SyncInput(void *values, int size, int *disconnect_flags); 28 | virtual GGPOErrorCode IncrementFrame(void); 29 | virtual GGPOErrorCode DisconnectPlayer(GGPOPlayerHandle handle); 30 | virtual GGPOErrorCode GetNetworkStats(FGGPONetworkStats *stats, GGPOPlayerHandle handle); 31 | virtual GGPOErrorCode SetFrameDelay(GGPOPlayerHandle player, int delay); 32 | virtual GGPOErrorCode SetDisconnectTimeout(int timeout); 33 | virtual GGPOErrorCode SetDisconnectNotifyStart(int timeout); 34 | virtual GGPOErrorCode TrySynchronizeLocal(); 35 | 36 | public: 37 | virtual void OnMsg(sockaddr_in &from, UdpMsg *msg, int len); 38 | 39 | protected: 40 | GGPOErrorCode PlayerHandleToQueue(GGPOPlayerHandle player, int *queue); 41 | GGPOPlayerHandle QueueToPlayerHandle(int queue) { return (GGPOPlayerHandle)(queue + 1); } 42 | GGPOPlayerHandle QueueToSpectatorHandle(int queue) { return (GGPOPlayerHandle)(queue + 1000); } /* out of range of the player array, basically */ 43 | void DisconnectPlayerQueue(int queue, int syncto); 44 | void DisconnectSpectatorQueue(int queue); 45 | void PollSyncEvents(void); 46 | void PollUdpProtocolEvents(void); 47 | void CheckInitialSync(void); 48 | int Poll2Players(int current_frame); 49 | int PollNPlayers(int current_frame); 50 | void AddRemotePlayer(char *remoteip, uint16 reportport, int queue); 51 | GGPOErrorCode AddSpectator(char *remoteip, uint16 reportport); 52 | virtual void OnSyncEvent(Sync::Event &e) { } 53 | virtual void OnUdpProtocolEvent(UdpProtocol::Event &e, GGPOPlayerHandle handle); 54 | virtual void OnUdpProtocolPeerEvent(UdpProtocol::Event &e, int queue); 55 | virtual void OnUdpProtocolSpectatorEvent(UdpProtocol::Event &e, int queue); 56 | 57 | protected: 58 | GGPOSessionCallbacks _callbacks; 59 | Poll _poll; 60 | Sync _sync; 61 | Udp _udp; 62 | UdpProtocol *_endpoints; 63 | UdpProtocol _spectators[GGPO_MAX_SPECTATORS]; 64 | int _num_spectators; 65 | int _input_size; 66 | 67 | bool _synchronizing; 68 | int _num_players; 69 | int _next_recommended_sleep; 70 | 71 | int _next_spectator_frame; 72 | int _disconnect_timeout; 73 | int _disconnect_notify_start; 74 | 75 | UdpMsg::connect_status _local_connect_status[UDP_MSG_MAX_PLAYERS]; 76 | }; 77 | 78 | #endif 79 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/network/udp.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "udp.h" 9 | #include "../types.h" 10 | 11 | SOCKET 12 | CreateSocket(uint16 bind_port, int retries) 13 | { 14 | SOCKET s; 15 | sockaddr_in sin; 16 | uint16 port; 17 | int optval = 1; 18 | 19 | s = socket(AF_INET, SOCK_DGRAM, 0); 20 | setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (const char *)&optval, sizeof optval); 21 | setsockopt(s, SOL_SOCKET, SO_DONTLINGER, (const char *)&optval, sizeof optval); 22 | 23 | // non-blocking... 24 | u_long iMode = 1; 25 | ioctlsocket(s, FIONBIO, &iMode); 26 | 27 | sin.sin_family = AF_INET; 28 | sin.sin_addr.s_addr = htonl(INADDR_ANY); 29 | for (port = bind_port; port <= bind_port + retries; port++) { 30 | sin.sin_port = htons(port); 31 | if (bind(s, (sockaddr *)&sin, sizeof sin) != SOCKET_ERROR) { 32 | Log(EGGPOLogVerbosity::Info, "Udp bound to port: %d.\n", port); 33 | return s; 34 | } 35 | } 36 | closesocket(s); 37 | return INVALID_SOCKET; 38 | } 39 | 40 | Udp::Udp() : 41 | _socket(INVALID_SOCKET), 42 | _callbacks(NULL) 43 | { 44 | } 45 | 46 | Udp::~Udp(void) 47 | { 48 | if (_socket != INVALID_SOCKET) { 49 | closesocket(_socket); 50 | _socket = INVALID_SOCKET; 51 | } 52 | } 53 | 54 | void 55 | Udp::Init(uint16 port, Poll *poll, Callbacks *callbacks) 56 | { 57 | _callbacks = callbacks; 58 | 59 | _poll = poll; 60 | _poll->RegisterLoop(this); 61 | 62 | Log(EGGPOLogVerbosity::Info, "binding udp socket to port %d.\n", port); 63 | _socket = CreateSocket(port, 0); 64 | } 65 | 66 | void 67 | Udp::SendTo(char *buffer, int len, int flags, struct sockaddr *dst, int destlen) 68 | { 69 | struct sockaddr_in *to = (struct sockaddr_in *)dst; 70 | 71 | int res = sendto(_socket, buffer, len, flags, dst, destlen); 72 | if (res == SOCKET_ERROR) { 73 | DWORD err = WSAGetLastError(); 74 | Log(EGGPOLogVerbosity::Info, "unknown error in sendto (erro: %d wsaerr: %d).\n", res, err); 75 | ASSERT(false && "Unknown error in sendto"); 76 | } 77 | char dst_ip[1024]; 78 | Log(EGGPOLogVerbosity::VeryVerbose, "sent packet length %d to %s:%d (ret:%d).\n", len, inet_ntop(AF_INET, (void *)&to->sin_addr, dst_ip, ARRAY_SIZE(dst_ip)), ntohs(to->sin_port), res); 79 | } 80 | 81 | bool 82 | Udp::OnLoopPoll(void *cookie) 83 | { 84 | uint8 recv_buf[MAX_UDP_PACKET_SIZE]; 85 | sockaddr_in recv_addr; 86 | int recv_addr_len; 87 | 88 | for (;;) { 89 | recv_addr_len = sizeof(recv_addr); 90 | int len = recvfrom(_socket, (char *)recv_buf, MAX_UDP_PACKET_SIZE, 0, (struct sockaddr *)&recv_addr, &recv_addr_len); 91 | 92 | // TODO: handle len == 0... indicates a disconnect. 93 | 94 | if (len == -1) { 95 | int error = WSAGetLastError(); 96 | if (error != WSAEWOULDBLOCK) { 97 | Log(EGGPOLogVerbosity::VeryVerbose, "recvfrom WSAGetLastError returned %d (%x).\n", error, error); 98 | } 99 | break; 100 | } else if (len > 0) { 101 | char src_ip[1024]; 102 | Log(EGGPOLogVerbosity::VeryVerbose, "recvfrom returned (len:%d from:%s:%d).\n", len, inet_ntop(AF_INET, (void*)&recv_addr.sin_addr, src_ip, ARRAY_SIZE(src_ip)), ntohs(recv_addr.sin_port) ); 103 | UdpMsg *msg = (UdpMsg *)recv_buf; 104 | _callbacks->OnMsg(recv_addr, msg, len); 105 | } 106 | } 107 | return true; 108 | } 109 | 110 | 111 | void 112 | Udp::Log(EGGPOLogVerbosity Verbosity, const char *fmt, ...) 113 | { 114 | char buf[1024]; 115 | size_t offset; 116 | va_list args; 117 | 118 | strcpy_s(buf, "udp | "); 119 | offset = strlen(buf); 120 | va_start(args, fmt); 121 | vsnprintf(buf + offset, ARRAY_SIZE(buf) - offset - 1, fmt, args); 122 | buf[ARRAY_SIZE(buf)-1] = '\0'; 123 | ::Log(Verbosity, "%s", buf); 124 | va_end(args); 125 | } 126 | -------------------------------------------------------------------------------- /doc/README.ja.md: -------------------------------------------------------------------------------- 1 | # GGPOとは 2 | 3 | 2009年に開発されたGGPOネットワーキングSDKは、P2Pゲームにおけるロールバックネットワーキング実用化の先駆けとなったシステムです。正確な入力や、フレームごとの完璧な処理を必要とし、ゲーム展開が速くかつ配信に適したゲームにおいて、ネットワーク遅延を目立たなくさせることに重点を置いて開発されました。 4 | 5 | 従来の技術はプレイヤーの入力に遅延を織り込んで通信を行っており、その結果反応が遅く、ラグを感じるプレイ感になっていました。ロールバックネットワーキングは入力予測と投機的実行を行って、プレイヤーの入力を即座に送信するため、遅延を感じさせないネット環境をもたらします。ロールバックがあれば、タイミングや相手の動きや効果音に対する反応、指が覚えている入力、これらオフラインで行えた内容が、そのままオンラインでも行えます。GGPOネットワーキングSDKは、ロールバックネットワーキングを新作や発売されているゲームに極力簡単に組み込めるよう作られています。 6 | 7 | # 仕組み 8 | 9 | ロールバックネットワーキングは決定的P2Pエンジンに統合できるよう設計されています。完全に決定的なエンジンなら、同じ入力をした場合にゲームは必ず同じ内容のプログラム再生をします。その内容を実現する一つの方法としては、ネットワーク上の全プレイヤーと入力のやりとりをする方法があげられますが、これは全プレイヤーがピアから入力を全て受け取った時にのみゲームプレイロジックが1フレームだけ実行される形になります。この方法ではゲーム内でキャラの動きがぎくしゃくし、反応の悪いゲーム内容になりがちです。ネットワークを介して入力を受け取る時間が長くなるほど、ゲーム展開も遅くなってしまいます。 10 | 11 | ## 入力遅延を用いたネットワーキング 12 | 13 | ### 理論上は… 14 | 15 | 下の図を見てください。2つのクライアントが遅延0msの理想的なネットワークで同期されている図になっています。1プレイヤー側の入力が青、2プレイヤー側の入力は赤、ネットワーク層は緑です。黒の矢印は入力がシステム内で送信され、ゲームステートが推移する流れを表します。各フレームは破線で区切られています。図は1プレイヤー側から見たものになっていますが、2プレイヤー側も全く同じ手順になっています。 16 | 17 | ![](images/overview_image1.png) 18 | 19 | 1プレイヤーの入力は、ネットワーク層によって2プレイヤーの入力とマージされ、ゲームエンジンに送信されます。エンジンはその入力を用いて現在のフレームのゲームステートを変更します。2プレイヤー側も同様に自分と1プレイヤーの入力をマージしてゲームエンジンに送信します。プレイヤーの入力に応じたロジックを適用し、過去のフレームのゲームステートを変更しながら、フレームごとにゲームが進行します。1プレイヤーと2プレイヤー両方が同じゲームステートで、それぞれのエンジンに送信される入力が同じなので、両プレイヤーのゲームステートは毎フレーム同期されたままになります。 20 | 21 | ### 実際は… 22 | 23 | 理想的なネットワークの例では、パケットがネットワークを介して即時に送信されるものとされていますが、現実はそう甘くはありません。一般的なブロードバンド接続では、プレイヤー間の距離や回線の品質に応じて、パケットの送信に5~150msかかります。ゲームが1秒間に60フレームで実行されるとするならば、遅延は1~9フレーム相当になります。 24 | 25 | ゲームは両プレイヤーの入力を受信するまでフレームを処理することができないので、各プレイヤーの入力に1~9フレームの遅延、つまり「ラグ」を適用しなければなりません。遅延を考慮に入れ、先程の図を変更してみると… 26 | 27 | ![](images/overview_image3.png) 28 | 29 | この例では、パケットの送信に3フレームかかります。2プレイヤーによって遠隔から送信された入力は、1フレーム目に1プレイヤー側に届かず、3フレーム後になるまでゲーム機に届きません。1プレイヤー側のゲームエンジンは入力を受信するまでゲームを進めることができないので、1フレーム目を3フレーム遅延せざるを得なくなります。続きのフレームも同様に3フレームの遅延が発生します。ネットワーク層は両プレイヤー間で送信されるパケットの最長転送時間だけ、マージされた入力を遅延せざるを得なくなります。理想的なネットワーク環境を除いては、大半のゲームジャンルにおいてこのラグはプレイ感に大きく影響を与えることとなります。 30 | 31 | ## ロールバックネットワーキングで入力遅延を取り除く 32 | 33 | ### 投機的実行 34 | 35 | GGPOは投機的実行を用いることで、パケット送信に必要な遅延を隠し、入力ラグの発生を防ぎます。それでは、もう一つの図を見てみましょう。 36 | 37 | ![](images/overview_image2.png) 38 | 39 | GGPOは遠隔のプレイヤーから入力が届くのを待つ代わりに、過去の入力に基づいて他プレイヤーが行いそうな入力を予測します。予測された入力と1プレイヤーの入力をマージし、すぐにゲームエンジンへ渡すので、仮に他プレイヤーの入力が届かなくとも次のフレームへ進めることができます。 40 | GGPOの予測が完璧であれば、オンラインで遊ぶユーザー体験はオフラインと同一のものになります。もちろん、未来を予測することは誰にもできません!GGPOも2プレイヤーの入力を間違って予測することがあります。上の図をもう一度見てください。もしGGPOが1フレーム目に2プレイヤーに間違った入力を送信したらどうなるでしょうか。1プレイヤー側に表示される2プレイヤーの入力は、2プレイヤー側で表示されるものと異なってしまいます。両サイドのゲームは同期を失い、プレイヤーは違ったゲーム画面を見ながら相手の動きに反応することになります。同期のズレは、1プレイヤー側が2プレイヤーの正しい入力が届く4フレーム目まで検出することができませんが、それでは遅すぎます。 41 | そういうことから、GGPOの手法は「投機的実行(speculative execution)」と呼ばれます。遊んでいるプレイヤーがその時に見ているものは正しいかもしれませんが、そうでないこともあります。GGPOが遠隔プレイヤーの入力を誤って予測した場合、次のフレームへ進める前にエラーを修正する必要があります。次の例では、その方法を説明します。 42 | 43 | ### 投機的実行エラーをロールバックで修正する 44 | 45 | GGPOは遠隔プレイヤーの入力を間違って予測する度に、ロールバックを使ってクライアントを再同期します。「ロールバック」という単語は、ステートを巻き戻し、プレイヤーの入力に関する、より正しく新しい情報を元に結果を予測する過程を指します。前のセクションでは、遠隔の入力1における予測したフレームが間違っていたらどうなるか、ということについて考えました。それでは、GGPOがエラーを修正する過程を見てみましょう。 46 | 47 | ![](images/overview_image5.png) 48 | 49 | GGPOは遠隔の入力を受信したら、その都度前回のフレームで予測した品質をチェックします。先程触れたように、GGPOは4フレーム目まで2プレイヤー側の入力が届きません。4フレーム目で、GGPOは以前に予測した入力とネットワークから受信した入力が一致しないことに気付きます。両サイドのゲームを再同期するため、GGPOは3フレーム分の誤った入力によって発生したダメージや間違いを取り消す必要があります。誤って予測した入力を送信する前のフレームまで戻るよう、ゲームエンジンに要求します(つまり過去のステートまで「ロールバック」します)。以前のステートを復元したら、GGPOはエンジンに正しい入力で1フレーム進めるよう要求します。このフレームは水色で示しています。ゲームエンジンはこのフレームをユーザーに見えない形で出来る限り素早く進める必要があります。例えば、ビデオレンダラーはこのフレームを画面に描写するべきではありません。オーディオレンダラーは原則、音声を生成し続けるべきですが、ロールバックが終わるまでレンダーすべきではなく、サンプルが生成されたフレームを引いた現在のフレームであるnフレームでサンプルがスタートする必要があります。 50 | エンジンがGGPOがエラーを見つける前のフレームまで到達したら、GGPOはロールバックモードを止め、ゲームを通常どおり進めることを許可します。図の5フレームと6フレーム目はGGPOの予測が正しく行われた場合を示しています。ゲームステートが正しいので、ロールバックをする理由はありません。 51 | 52 | # コード構造 53 | 54 | 次の図はGGPOセッションで主に動作するパーツ、また各パーツごとの関連性を示しています。各コンポーネントの詳細は以下に示しています。 55 | 56 | ![](images/overview_image4.png) 57 | 58 | ## GGPOインタフェース(GGPO Interface) 59 | 60 | GGPOインターフェースはP2Pと同期テストバックエンド間の詳細な実装を抽象化しています。適切なバックエンドは`ggpo_start_session`か`ggpo_start_synctest`エントリーポイントを呼び出した時に、自動的に生成されます。 61 | 62 | ## P2Pバックエンド(P2P Backend) 63 | 64 | P2Pバックエンドはプレイヤー間でゲームを調整します。`ggpo_start_session` APIの呼び出しによって生成されます。大きな情報の処理の大半は含まれているヘルパークラスによって行われます。 65 | 66 | ## ポーリングオブジェクト(Poll Object) 67 | 68 | (図にはありません)ポーリングオブジェクトはコード内で他のオブジェクトによって用いられる登録方式です。待機可能なオブジェクトが準備できたときに通知とタイマーを送信します。例としてUDPバックエンドは新たなパケットが到着したときに、通知を受信するためポーリングオブジェクトを使用します。 69 | 70 | ## 同期オブジェクト(Sync Object) 71 | 72 | 同期オブジェクトはゲームステートのnフレームを追跡するために用いられます。埋め込まれた予測(prediction)オブジェクトが予測エラーを通知された時、同期バックエンドがより正確なステートまでゲームを巻き戻し、予測エラーを修正するためシングルステップ処理を進めます。 73 | 74 | ## 入力キューオブジェクト(Input Queue Object) 75 | 76 | 入力キューオブジェクトはローカル、または遠隔プレイヤー用に受信した全入力を追跡します。所持していない入力を要求された場合、入力キューは次の入力を予測し、後の情報を追跡します。そうすることで同期オブジェクトは予測が誤った場合にどこまでロールバックすればよいのか分かります。リクエストがあった場合、入力キューはフレーム遅延も実行します。 77 | 78 | ## UDPプロトコルオブジェクト(UDP Protocol Object) 79 | 80 | UDPプロトコルオブジェクトは両プレイヤー間の同期と入力交換プロトコルを扱います。また、ゲーム入力の圧縮と信頼できるUDP層も実装しています。各UDPプロトコルオブジェクトにはTimeSyncオブジェクトが含まれ、プレイヤー間の時間のずれを推測するために利用しています。 81 | 82 | ## UDPオブジェクト(UDP Object) 83 | 84 | UDPオブジェクトは単純なUDPパケットの送受信を行います。他のプラットフォームへの移植を簡単にするため、UDPプロトコルから切り離されています。 85 | 86 | ## 同期テストバックエンド(Sync Test Backend) 87 | 88 | (図にはありません)同期テストバックエンドは、P2Pバックエンドがアプリのセーブステートと決定的に機能上実行していることを確認するときに同じ同期オブジェクトを使用します。同期テストの使用に関する詳しい情報は、開発者ガイドを参照してください。 89 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/backends/spectator.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "spectator.h" 9 | 10 | SpectatorBackend::SpectatorBackend(GGPOSessionCallbacks *cb, 11 | const char* gamename, 12 | uint16 localport, 13 | int num_players, 14 | int input_size, 15 | char *hostip, 16 | u_short hostport) : 17 | _num_players(num_players), 18 | _input_size(input_size), 19 | _next_input_to_send(0) 20 | { 21 | _callbacks = *cb; 22 | _synchronizing = true; 23 | 24 | for (int i = 0; i < ARRAY_SIZE(_inputs); i++) { 25 | _inputs[i].frame = -1; 26 | } 27 | 28 | /* 29 | * Initialize the UDP port 30 | */ 31 | _udp.Init(localport, &_poll, this); 32 | 33 | /* 34 | * Init the host endpoint 35 | */ 36 | _host.Init(&_udp, _poll, 0, hostip, hostport, NULL); 37 | _host.Synchronize(); 38 | 39 | /* 40 | * Preload the ROM 41 | */ 42 | _callbacks.begin_game(gamename); 43 | } 44 | 45 | SpectatorBackend::~SpectatorBackend() 46 | { 47 | } 48 | 49 | GGPOErrorCode 50 | SpectatorBackend::DoPoll(int timeout) 51 | { 52 | _poll.Pump(0); 53 | 54 | PollUdpProtocolEvents(); 55 | return GGPO_OK; 56 | } 57 | 58 | GGPOErrorCode 59 | SpectatorBackend::SyncInput(void *values, 60 | int size, 61 | int *disconnect_flags) 62 | { 63 | // Wait until we've started to return inputs. 64 | if (_synchronizing) { 65 | return GGPO_ERRORCODE_NOT_SYNCHRONIZED; 66 | } 67 | 68 | GameInput &input = _inputs[_next_input_to_send % SPECTATOR_FRAME_BUFFER_SIZE]; 69 | if (input.frame < _next_input_to_send) { 70 | // Haven't received the input from the host yet. Wait 71 | return GGPO_ERRORCODE_PREDICTION_THRESHOLD; 72 | } 73 | if (input.frame > _next_input_to_send) { 74 | // The host is way way way far ahead of the spectator. How'd this 75 | // happen? Anyway, the input we need is gone forever. 76 | return GGPO_ERRORCODE_GENERAL_FAILURE; 77 | } 78 | 79 | ASSERT(size >= _input_size * _num_players); 80 | memcpy(values, input.bits, _input_size * _num_players); 81 | if (disconnect_flags) { 82 | *disconnect_flags = 0; // xxx: should get them from the host! 83 | } 84 | _next_input_to_send++; 85 | 86 | return GGPO_OK; 87 | } 88 | 89 | GGPOErrorCode 90 | SpectatorBackend::IncrementFrame(void) 91 | { 92 | Log("End of frame (%d)...\n", _next_input_to_send - 1); 93 | DoPoll(0); 94 | PollUdpProtocolEvents(); 95 | 96 | return GGPO_OK; 97 | } 98 | 99 | GGPOErrorCode 100 | SpectatorBackend::GetNetworkStats(FGGPONetworkStats* stats, GGPOPlayerHandle player) 101 | { 102 | if (!_host.IsRunning()) { 103 | return GGPO_ERRORCODE_NOT_SYNCHRONIZED; 104 | } 105 | 106 | memset(stats, 0, sizeof * stats); 107 | _host.GetNetworkStats(stats); 108 | 109 | return GGPO_OK; 110 | } 111 | 112 | void 113 | SpectatorBackend::PollUdpProtocolEvents(void) 114 | { 115 | UdpProtocol::Event evt; 116 | while (_host.GetEvent(evt)) { 117 | OnUdpProtocolEvent(evt); 118 | } 119 | } 120 | 121 | void 122 | SpectatorBackend::OnUdpProtocolEvent(UdpProtocol::Event &evt) 123 | { 124 | GGPOEvent info; 125 | 126 | switch (evt.type) { 127 | case UdpProtocol::Event::Connected: 128 | info.code = GGPO_EVENTCODE_CONNECTED_TO_PEER; 129 | info.u.connected.player = 0; 130 | _callbacks.on_event(&info); 131 | break; 132 | case UdpProtocol::Event::Synchronizing: 133 | info.code = GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER; 134 | info.u.synchronizing.player = 0; 135 | info.u.synchronizing.count = evt.u.synchronizing.count; 136 | info.u.synchronizing.total = evt.u.synchronizing.total; 137 | _callbacks.on_event(&info); 138 | break; 139 | case UdpProtocol::Event::Synchronzied: 140 | if (_synchronizing) { 141 | info.code = GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER; 142 | info.u.synchronized.player = 0; 143 | _callbacks.on_event(&info); 144 | 145 | info.code = GGPO_EVENTCODE_RUNNING; 146 | _callbacks.on_event(&info); 147 | _synchronizing = false; 148 | } 149 | break; 150 | 151 | case UdpProtocol::Event::NetworkInterrupted: 152 | info.code = GGPO_EVENTCODE_CONNECTION_INTERRUPTED; 153 | info.u.connection_interrupted.player = 0; 154 | info.u.connection_interrupted.disconnect_timeout = evt.u.network_interrupted.disconnect_timeout; 155 | _callbacks.on_event(&info); 156 | break; 157 | 158 | case UdpProtocol::Event::NetworkResumed: 159 | info.code = GGPO_EVENTCODE_CONNECTION_RESUMED; 160 | info.u.connection_resumed.player = 0; 161 | _callbacks.on_event(&info); 162 | break; 163 | 164 | case UdpProtocol::Event::Disconnected: 165 | info.code = GGPO_EVENTCODE_DISCONNECTED_FROM_PEER; 166 | info.u.disconnected.player = 0; 167 | _callbacks.on_event(&info); 168 | break; 169 | 170 | case UdpProtocol::Event::Input: 171 | GameInput& input = evt.u.input.input; 172 | 173 | // If the input buffer would overflow, have to disconnect 174 | if (input.frame >= _next_input_to_send + SPECTATOR_FRAME_BUFFER_SIZE) 175 | { 176 | _host.Disconnect(); 177 | info.code = GGPO_EVENTCODE_DISCONNECTED_FROM_PEER; 178 | info.u.disconnected.player = 0; 179 | _callbacks.on_event(&info); 180 | } 181 | else 182 | { 183 | _host.SetLocalFrameNumber(_next_input_to_send); 184 | _host.SendInputAck(); 185 | _inputs[input.frame % SPECTATOR_FRAME_BUFFER_SIZE] = input; 186 | } 187 | break; 188 | } 189 | } 190 | 191 | void 192 | SpectatorBackend::OnMsg(sockaddr_in &from, UdpMsg *msg, int len) 193 | { 194 | if (_host.HandlesMsg(from, msg)) { 195 | _host.OnMsg(msg, len); 196 | } 197 | } 198 | 199 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/network/udp_proto.h: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #ifndef _UDP_PROTO_H_ 9 | #define _UDP_PROTO_H_ 10 | 11 | #include "../poll.h" 12 | #include "udp.h" 13 | #include "udp_msg.h" 14 | #include "../game_input.h" 15 | #include "../timesync.h" 16 | #include "include/ggponet.h" 17 | #include "../ring_buffer.h" 18 | 19 | #define UDP_BUFFER_SIZE BUFFER_SIZE 20 | 21 | class UdpProtocol : public IPollSink 22 | { 23 | public: 24 | struct Stats { 25 | int ping; 26 | int remote_frame_advantage; 27 | int local_frame_advantage; 28 | int send_queue_len; 29 | Udp::Stats udp; 30 | }; 31 | 32 | struct Event { 33 | enum Type { 34 | Unknown = -1, 35 | Connected, 36 | Synchronizing, 37 | Synchronzied, 38 | Input, 39 | Disconnected, 40 | NetworkInterrupted, 41 | NetworkResumed, 42 | }; 43 | 44 | Type type; 45 | union { 46 | struct { 47 | GameInput input; 48 | } input; 49 | struct { 50 | int total; 51 | int count; 52 | } synchronizing; 53 | struct { 54 | int disconnect_timeout; 55 | } network_interrupted; 56 | } u; 57 | 58 | UdpProtocol::Event(Type t = Unknown) : type(t) { } 59 | }; 60 | 61 | public: 62 | virtual bool OnLoopPoll(void *cookie); 63 | 64 | public: 65 | UdpProtocol(); 66 | virtual ~UdpProtocol(); 67 | 68 | void Init(Udp *udp, Poll &p, int queue, char *ip, u_short port, UdpMsg::connect_status *status); 69 | 70 | void Synchronize(); 71 | bool GetPeerConnectStatus(int id, int *frame); 72 | bool IsInitialized() { return _udp != NULL; } 73 | bool IsSynchronized() { return _current_state == Running; } 74 | bool IsRunning() { return _current_state == Running; } 75 | void SendInput(GameInput &input); 76 | void SendInputAck(); 77 | bool IsPendingFull(); 78 | bool HandlesMsg(sockaddr_in &from, UdpMsg *msg); 79 | void OnMsg(UdpMsg *msg, int len); 80 | void Disconnect(); 81 | 82 | void GetNetworkStats(struct FGGPONetworkStats *stats); 83 | bool GetEvent(UdpProtocol::Event &e); 84 | void SetLocalFrameNumber(int num); 85 | int RecommendFrameDelay(); 86 | 87 | void SetDisconnectTimeout(int timeout); 88 | void SetDisconnectNotifyStart(int timeout); 89 | 90 | protected: 91 | enum State { 92 | Syncing, 93 | Synchronzied, 94 | Running, 95 | Disconnected 96 | }; 97 | struct QueueEntry { 98 | int queue_time; 99 | sockaddr_in dest_addr; 100 | UdpMsg *msg; 101 | 102 | QueueEntry() {} 103 | QueueEntry(int time, sockaddr_in &dst, UdpMsg *m) : queue_time(time), dest_addr(dst), msg(m) { } 104 | }; 105 | 106 | bool CreateSocket(int retries); 107 | void UpdateNetworkStats(void); 108 | void QueueEvent(const UdpProtocol::Event &evt); 109 | void ClearSendQueue(void); 110 | void Log(const char *fmt, ...); 111 | void LogMsg(const char *prefix, UdpMsg *msg); 112 | void LogEvent(const char *prefix, const UdpProtocol::Event &evt); 113 | void SendSyncRequest(); 114 | void SendMsg(UdpMsg *msg); 115 | void PumpSendQueue(); 116 | void DispatchMsg(uint8 *buffer, int len); 117 | void SendPendingOutput(); 118 | bool OnInvalid(UdpMsg *msg, int len); 119 | bool OnSyncRequest(UdpMsg *msg, int len); 120 | bool OnSyncReply(UdpMsg *msg, int len); 121 | bool OnInput(UdpMsg *msg, int len); 122 | bool OnInputAck(UdpMsg *msg, int len); 123 | bool OnQualityReport(UdpMsg *msg, int len); 124 | bool OnQualityReply(UdpMsg *msg, int len); 125 | bool OnKeepAlive(UdpMsg *msg, int len); 126 | 127 | protected: 128 | /* 129 | * Network transmission information 130 | */ 131 | Udp *_udp; 132 | sockaddr_in _peer_addr; 133 | uint16 _magic_number; 134 | int _queue; 135 | uint16 _remote_magic_number; 136 | bool _connected; 137 | int _send_latency; 138 | int _oop_percent; 139 | struct { 140 | int send_time; 141 | sockaddr_in dest_addr; 142 | UdpMsg* msg; 143 | } _oo_packet; 144 | RingBuffer _send_queue; 145 | 146 | /* 147 | * Stats 148 | */ 149 | int _round_trip_time; 150 | int _packets_sent; 151 | int _bytes_sent; 152 | int _kbps_sent; 153 | int _stats_start_time; 154 | 155 | /* 156 | * The state machine 157 | */ 158 | UdpMsg::connect_status *_local_connect_status; 159 | UdpMsg::connect_status _peer_connect_status[UDP_MSG_MAX_PLAYERS]; 160 | 161 | State _current_state; 162 | union { 163 | struct { 164 | uint32 roundtrips_remaining; 165 | uint32 random; 166 | } sync; 167 | struct { 168 | uint32 last_quality_report_time; 169 | uint32 last_network_stats_interval; 170 | uint32 last_input_packet_recv_time; 171 | } running; 172 | } _state; 173 | 174 | /* 175 | * Fairness. 176 | */ 177 | int _local_frame_advantage; 178 | int _remote_frame_advantage; 179 | 180 | /* 181 | * Packet loss... 182 | */ 183 | RingBuffer _pending_output; 184 | GameInput _last_received_input; 185 | GameInput _last_sent_input; 186 | GameInput _last_acked_input; 187 | unsigned int _last_send_time; 188 | unsigned int _last_recv_time; 189 | unsigned int _shutdown_timeout; 190 | unsigned int _disconnect_event_sent; 191 | unsigned int _disconnect_timeout; 192 | unsigned int _disconnect_notify_start; 193 | bool _disconnect_notify_sent; 194 | 195 | uint16 _next_send_seq; 196 | uint16 _next_recv_seq; 197 | 198 | /* 199 | * Rift synchronization. 200 | */ 201 | TimeSync _timesync; 202 | 203 | /* 204 | * Event queue 205 | */ 206 | RingBuffer _event_queue; 207 | }; 208 | 209 | #endif 210 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/backends/synctest.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "synctest.h" 9 | 10 | SyncTestBackend::SyncTestBackend(GGPOSessionCallbacks *cb, 11 | char *gamename, 12 | int frames, 13 | int num_players) : 14 | _sync(NULL) 15 | { 16 | _callbacks = *cb; 17 | _num_players = num_players; 18 | _check_distance = frames; 19 | _last_verified = 0; 20 | _rollingback = false; 21 | _running = false; 22 | _logfp = NULL; 23 | _current_input.erase(); 24 | strcpy_s(_game, gamename); 25 | 26 | /* 27 | * Initialize the synchronziation layer 28 | */ 29 | Sync::Config config = { 0 }; 30 | config.callbacks = _callbacks; 31 | config.num_prediction_frames = MAX_PREDICTION_FRAMES; 32 | _sync.Init(config); 33 | 34 | /* 35 | * Preload the ROM 36 | */ 37 | _callbacks.begin_game(gamename); 38 | } 39 | 40 | SyncTestBackend::~SyncTestBackend() 41 | { 42 | } 43 | 44 | GGPOErrorCode 45 | SyncTestBackend::DoPoll(int timeout) 46 | { 47 | if (!_running) { 48 | GGPOEvent info; 49 | 50 | info.code = GGPO_EVENTCODE_RUNNING; 51 | _callbacks.on_event(&info); 52 | _running = true; 53 | } 54 | return GGPO_OK; 55 | } 56 | 57 | GGPOErrorCode 58 | SyncTestBackend::AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle) 59 | { 60 | if (player->player_num < 1 || player->player_num > _num_players) { 61 | return GGPO_ERRORCODE_PLAYER_OUT_OF_RANGE; 62 | } 63 | *handle = (GGPOPlayerHandle)(player->player_num - 1); 64 | return GGPO_OK; 65 | } 66 | 67 | GGPOErrorCode 68 | SyncTestBackend::AddLocalInput(GGPOPlayerHandle player, void *values, int size) 69 | { 70 | if (!_running) { 71 | return GGPO_ERRORCODE_NOT_SYNCHRONIZED; 72 | } 73 | 74 | int index = (int)player; 75 | for (int i = 0; i < size; i++) { 76 | _current_input.bits[(index * size) + i] |= ((char *)values)[i]; 77 | } 78 | return GGPO_OK; 79 | } 80 | 81 | GGPOErrorCode 82 | SyncTestBackend::SyncInput(void *values, 83 | int size, 84 | int *disconnect_flags) 85 | { 86 | BeginLog(false); 87 | if (_rollingback) { 88 | _last_input = _saved_frames.front().input; 89 | } else { 90 | if (_sync.GetFrameCount() == 0) { 91 | _sync.SaveCurrentFrame(); 92 | } 93 | _last_input = _current_input; 94 | } 95 | memcpy(values, _last_input.bits, size); 96 | if (disconnect_flags) { 97 | *disconnect_flags = 0; 98 | } 99 | return GGPO_OK; 100 | } 101 | 102 | GGPOErrorCode 103 | SyncTestBackend::IncrementFrame(void) 104 | { 105 | _sync.IncrementFrame(); 106 | _current_input.erase(); 107 | 108 | Log("End of frame(%d)...\n", _sync.GetFrameCount()); 109 | EndLog(); 110 | 111 | if (_rollingback) { 112 | return GGPO_OK; 113 | } 114 | 115 | int frame = _sync.GetFrameCount(); 116 | // Hold onto the current frame in our queue of saved states. We'll need 117 | // the checksum later to verify that our replay of the same frame got the 118 | // same results. 119 | SavedInfo info; 120 | info.frame = frame; 121 | info.input = _last_input; 122 | info.cbuf = _sync.GetLastSavedFrame().cbuf; 123 | info.buf = (char *)malloc(info.cbuf); 124 | memcpy(info.buf, _sync.GetLastSavedFrame().buf, info.cbuf); 125 | info.checksum = _sync.GetLastSavedFrame().checksum; 126 | _saved_frames.push(info); 127 | 128 | if (frame - _last_verified == _check_distance) { 129 | // We've gone far enough ahead and should now start replaying frames. 130 | // Load the last verified frame and set the rollback flag to true. 131 | _sync.LoadFrame(_last_verified); 132 | 133 | _rollingback = true; 134 | while(!_saved_frames.empty()) { 135 | _callbacks.advance_frame(0); 136 | 137 | // Verify that the checksumn of this frame is the same as the one in our 138 | // list. 139 | info = _saved_frames.front(); 140 | _saved_frames.pop(); 141 | 142 | if (info.frame != _sync.GetFrameCount()) { 143 | RaiseSyncError("Frame number %d does not match saved frame number %d", info.frame, frame); 144 | } 145 | int checksum = _sync.GetLastSavedFrame().checksum; 146 | if (info.checksum != checksum) { 147 | LogSaveStates(info); 148 | RaiseSyncError("Checksum for frame %d does not match saved (%d != %d)", frame, checksum, info.checksum); 149 | } 150 | printf("Checksum %08d for frame %d matches.\n", checksum, info.frame); 151 | free(info.buf); 152 | } 153 | _last_verified = frame; 154 | _rollingback = false; 155 | } 156 | 157 | return GGPO_OK; 158 | } 159 | 160 | void 161 | SyncTestBackend::RaiseSyncError(const char *fmt, ...) 162 | { 163 | char buf[1024]; 164 | va_list args; 165 | va_start(args, fmt); 166 | vsprintf_s(buf, ARRAY_SIZE(buf), fmt, args); 167 | va_end(args); 168 | 169 | puts(buf); 170 | OutputDebugStringA(buf); 171 | EndLog(); 172 | DebugBreak(); 173 | } 174 | 175 | GGPOErrorCode 176 | SyncTestBackend::Logv(EGGPOLogVerbosity Verbosity, char *fmt, va_list list) 177 | { 178 | if (_logfp) { 179 | vfprintf(_logfp, fmt, list); 180 | } 181 | return GGPO_OK; 182 | } 183 | 184 | void 185 | SyncTestBackend::BeginLog(int saving) 186 | { 187 | EndLog(); 188 | 189 | char filename[MAX_PATH]; 190 | CreateDirectoryA("synclogs", NULL); 191 | sprintf_s(filename, ARRAY_SIZE(filename), "synclogs\\%s-%04d-%s.log", 192 | saving ? "state" : "log", 193 | _sync.GetFrameCount(), 194 | _rollingback ? "replay" : "original"); 195 | 196 | fopen_s(&_logfp, filename, "w"); 197 | } 198 | 199 | void 200 | SyncTestBackend::EndLog() 201 | { 202 | if (_logfp) { 203 | fprintf(_logfp, "Closing log file.\n"); 204 | fclose(_logfp); 205 | _logfp = NULL; 206 | } 207 | } 208 | void 209 | SyncTestBackend::LogSaveStates(SavedInfo &info) 210 | { 211 | char filename[MAX_PATH]; 212 | sprintf_s(filename, ARRAY_SIZE(filename), "synclogs\\state-%04d-original.log", _sync.GetFrameCount()); 213 | _callbacks.log_game_state(filename, (unsigned char *)info.buf, info.cbuf); 214 | 215 | sprintf_s(filename, ARRAY_SIZE(filename), "synclogs\\state-%04d-replay.log", _sync.GetFrameCount()); 216 | _callbacks.log_game_state(filename, _sync.GetLastSavedFrame().buf, _sync.GetLastSavedFrame().cbuf); 217 | } 218 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/main.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "types.h" 9 | #include "backends/p2p.h" 10 | #include "backends/synctest.h" 11 | #include "backends/spectator.h" 12 | #include "include/ggponet.h" 13 | 14 | BOOL WINAPI 15 | DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) 16 | { 17 | srand(Platform::GetCurrentTimeMS() + Platform::GetProcessID()); 18 | return true; 19 | } 20 | 21 | void 22 | GGPONet::ggpo_log(GGPOSession *ggpo, const char *fmt, ...) 23 | { 24 | va_list args; 25 | va_start(args, fmt); 26 | ggpo_logv(ggpo, EGGPOLogVerbosity::Info, fmt, args); 27 | va_end(args); 28 | } 29 | 30 | void 31 | GGPONet::ggpo_log(GGPOSession *ggpo, EGGPOLogVerbosity Verbosity, const char *fmt, ...) 32 | { 33 | va_list args; 34 | va_start(args, fmt); 35 | ggpo_logv(ggpo, Verbosity, fmt, args); 36 | va_end(args); 37 | } 38 | 39 | void 40 | GGPONet::ggpo_logv(GGPOSession *ggpo, EGGPOLogVerbosity Verbosity, const char *fmt, va_list args) 41 | { 42 | if (ggpo) { 43 | ggpo->Logv(Verbosity, fmt, args); 44 | } 45 | } 46 | 47 | GGPOErrorCode 48 | GGPONet::ggpo_start_session(GGPOSession **session, 49 | GGPOSessionCallbacks *cb, 50 | const char *game, 51 | int num_players, 52 | int input_size, 53 | unsigned short localport) 54 | { 55 | *session= (GGPOSession *)new Peer2PeerBackend(cb, 56 | game, 57 | localport, 58 | num_players, 59 | input_size); 60 | return GGPO_OK; 61 | } 62 | 63 | GGPOErrorCode 64 | GGPONet::ggpo_add_player(GGPOSession *ggpo, 65 | GGPOPlayer *player, 66 | GGPOPlayerHandle *handle) 67 | { 68 | if (!ggpo) { 69 | return GGPO_ERRORCODE_INVALID_SESSION; 70 | } 71 | return ggpo->AddPlayer(player, handle); 72 | } 73 | 74 | 75 | 76 | GGPOErrorCode 77 | GGPONet::ggpo_start_synctest(GGPOSession **ggpo, 78 | GGPOSessionCallbacks *cb, 79 | char *game, 80 | int num_players, 81 | int input_size, 82 | int frames) 83 | { 84 | *ggpo = (GGPOSession *)new SyncTestBackend(cb, game, frames, num_players); 85 | return GGPO_OK; 86 | } 87 | 88 | GGPOErrorCode 89 | GGPONet::ggpo_set_frame_delay(GGPOSession *ggpo, 90 | GGPOPlayerHandle player, 91 | int frame_delay) 92 | { 93 | if (!ggpo) { 94 | return GGPO_ERRORCODE_INVALID_SESSION; 95 | } 96 | return ggpo->SetFrameDelay(player, frame_delay); 97 | } 98 | 99 | GGPOErrorCode 100 | GGPONet::ggpo_idle(GGPOSession *ggpo, int timeout) 101 | { 102 | if (!ggpo) { 103 | return GGPO_ERRORCODE_INVALID_SESSION; 104 | } 105 | return ggpo->DoPoll(timeout); 106 | } 107 | 108 | GGPOErrorCode 109 | GGPONet::ggpo_add_local_input(GGPOSession *ggpo, 110 | GGPOPlayerHandle player, 111 | void *values, 112 | int size) 113 | { 114 | if (!ggpo) { 115 | return GGPO_ERRORCODE_INVALID_SESSION; 116 | } 117 | return ggpo->AddLocalInput(player, values, size); 118 | } 119 | 120 | GGPOErrorCode 121 | GGPONet::ggpo_synchronize_input(GGPOSession *ggpo, 122 | void *values, 123 | int size, 124 | int *disconnect_flags) 125 | { 126 | if (!ggpo) { 127 | return GGPO_ERRORCODE_INVALID_SESSION; 128 | } 129 | return ggpo->SyncInput(values, size, disconnect_flags); 130 | } 131 | 132 | GGPOErrorCode GGPONet::ggpo_disconnect_player(GGPOSession *ggpo, 133 | GGPOPlayerHandle player) 134 | { 135 | if (!ggpo) { 136 | return GGPO_ERRORCODE_INVALID_SESSION; 137 | } 138 | return ggpo->DisconnectPlayer(player); 139 | } 140 | 141 | GGPOErrorCode 142 | GGPONet::ggpo_advance_frame(GGPOSession *ggpo) 143 | { 144 | if (!ggpo) { 145 | return GGPO_ERRORCODE_INVALID_SESSION; 146 | } 147 | return ggpo->IncrementFrame(); 148 | } 149 | 150 | GGPOErrorCode 151 | ggpo_client_chat(GGPOSession *ggpo, char *text) 152 | { 153 | if (!ggpo) { 154 | return GGPO_ERRORCODE_INVALID_SESSION; 155 | } 156 | return ggpo->Chat(text); 157 | } 158 | 159 | GGPOErrorCode 160 | GGPONet::ggpo_get_network_stats(GGPOSession *ggpo, 161 | GGPOPlayerHandle player, 162 | FGGPONetworkStats *stats) 163 | { 164 | if (!ggpo) { 165 | return GGPO_ERRORCODE_INVALID_SESSION; 166 | } 167 | return ggpo->GetNetworkStats(stats, player); 168 | } 169 | 170 | 171 | GGPOErrorCode 172 | GGPONet::ggpo_close_session(GGPOSession *ggpo) 173 | { 174 | if (!ggpo) { 175 | return GGPO_ERRORCODE_INVALID_SESSION; 176 | } 177 | delete ggpo; 178 | return GGPO_OK; 179 | } 180 | 181 | GGPOErrorCode 182 | GGPONet::ggpo_set_disconnect_timeout(GGPOSession *ggpo, int timeout) 183 | { 184 | if (!ggpo) { 185 | return GGPO_ERRORCODE_INVALID_SESSION; 186 | } 187 | return ggpo->SetDisconnectTimeout(timeout); 188 | } 189 | 190 | GGPOErrorCode 191 | GGPONet::ggpo_set_disconnect_notify_start(GGPOSession *ggpo, int timeout) 192 | { 193 | if (!ggpo) { 194 | return GGPO_ERRORCODE_INVALID_SESSION; 195 | } 196 | return ggpo->SetDisconnectNotifyStart(timeout); 197 | } 198 | 199 | GGPOErrorCode 200 | GGPONet::ggpo_try_synchronize_local(GGPOSession* ggpo) 201 | { 202 | if (!ggpo) { 203 | return GGPO_ERRORCODE_INVALID_SESSION; 204 | } 205 | return ggpo->TrySynchronizeLocal(); 206 | } 207 | 208 | GGPOErrorCode GGPONet::ggpo_start_spectating(GGPOSession **session, 209 | GGPOSessionCallbacks *cb, 210 | const char *game, 211 | int num_players, 212 | int input_size, 213 | unsigned short local_port, 214 | char *host_ip, 215 | unsigned short host_port) 216 | { 217 | *session= (GGPOSession *)new SpectatorBackend(cb, 218 | game, 219 | local_port, 220 | num_players, 221 | input_size, 222 | host_ip, 223 | host_port); 224 | return GGPO_OK; 225 | } 226 | 227 | -------------------------------------------------------------------------------- /Source/GGPOUE/Public/include/ggponet.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "ggponet.h" 9 | #include "UObject/UObjectGlobals.h" 10 | 11 | #define ARRAYSIZE(a) sizeof(a) / sizeof(a[0]) 12 | 13 | //UGGPONetworkAddress 14 | 15 | UGGPONetworkAddress* UGGPONetworkAddress::CreateNetworkAddress(UObject* Outer, const FName Name, const FString Address) 16 | { 17 | UGGPONetworkAddress* Result = NewObject(Outer, Name); 18 | // Same type, apparently? 19 | const wchar_t* address = *Address; 20 | 21 | wchar_t WideIpBuffer[128]; 22 | uint32 WideIpBufferSize = (uint32)ARRAYSIZE(WideIpBuffer); 23 | // Check and get port 24 | if (swscanf_s(address, L"%[^:]:%hd", WideIpBuffer, WideIpBufferSize, &Result->Port) != 2) { 25 | Result->bValidAddress = false; 26 | } 27 | else 28 | { 29 | // Get address 30 | wcstombs_s(nullptr, Result->IpAddress, ARRAYSIZE(Result->IpAddress), WideIpBuffer, _TRUNCATE); 31 | } 32 | 33 | return Result; 34 | } 35 | UGGPONetworkAddress* UGGPONetworkAddress::CreateLocalAddress(UObject* Outer, const FName Name, int32 LocalPort) 36 | { 37 | UGGPONetworkAddress* Result = NewObject(Outer, Name); 38 | 39 | Result->bValidAddress = true; 40 | Result->Port = (uint16)LocalPort; 41 | strcpy(Result->IpAddress, "127.0.0.1"); 42 | 43 | return Result; 44 | } 45 | 46 | void UGGPONetworkAddress::GetIpAddress(char OutAddress[32]) const 47 | { 48 | std::memcpy(OutAddress, IpAddress, sizeof(IpAddress)); 49 | } 50 | 51 | bool UGGPONetworkAddress::IsValidAddress() const 52 | { 53 | return bValidAddress; 54 | } 55 | FString UGGPONetworkAddress::GetIpAddressString() const 56 | { 57 | auto address = FString(ANSI_TO_TCHAR(IpAddress)); 58 | return address; 59 | } 60 | int32 UGGPONetworkAddress::GetPort() const 61 | { 62 | return Port; 63 | } 64 | 65 | bool UGGPONetworkAddress::IsSameAddress(const UGGPONetworkAddress* Other) const 66 | { 67 | if (bValidAddress != Other->bValidAddress) 68 | return false; 69 | if (!std::equal(std::begin(IpAddress), std::end(IpAddress), std::begin(Other->IpAddress))) 70 | return false; 71 | if (Port != Other->Port) 72 | return false; 73 | 74 | return true; 75 | } 76 | 77 | // UGGPONetwork 78 | 79 | UGGPONetwork* UGGPONetwork::CreateNetwork(UObject* Outer, const FName Name, int32 InNumPlayers, int32 PlayerIndex, int32 InLocalPort, TArray RemoteAddresses) 80 | { 81 | TObjectPtr Result = NewObject(Outer, Name); 82 | 83 | Result->LocalPlayerIndex = PlayerIndex - 1; 84 | Result->NumPlayers = InNumPlayers; 85 | 86 | if (Result->LocalPlayerIndex >= 0) 87 | { 88 | int32 remoteIndex = 0; 89 | // Add remote players 90 | for (int32 i = 0; i < InNumPlayers; i++) 91 | { 92 | // Only the port matters for local player 93 | if (i == Result->LocalPlayerIndex) 94 | { 95 | // Create a GGPO Network Address and add to the addresses 96 | UGGPONetworkAddress* address = UGGPONetworkAddress::CreateLocalAddress( 97 | Outer, 98 | FName(FString::Printf(TEXT("P%dIPAddress"), i + 1)), 99 | InLocalPort); 100 | Result->Addresses.Add(address); 101 | } 102 | else 103 | { 104 | // If we ran out of remote addresses, clear the addresses and break 105 | if (remoteIndex >= RemoteAddresses.Num()) 106 | { 107 | Result->Addresses.Empty(); 108 | break; 109 | } 110 | 111 | // Create a GGPO Network Address and add to the addresses 112 | UGGPONetworkAddress* address = UGGPONetworkAddress::CreateNetworkAddress( 113 | Outer, 114 | FName(FString::Printf(TEXT("P%dIPAddress"), i + 1)), 115 | RemoteAddresses[remoteIndex]); 116 | Result->Addresses.Add(address); 117 | remoteIndex++; 118 | } 119 | } 120 | 121 | // Add any spectators 122 | if (Result->Addresses.Num() > 0) 123 | { 124 | for (int32 i = remoteIndex; i < RemoteAddresses.Num(); i++) 125 | { 126 | // Create a GGPO Network Address and add to the spectators 127 | UGGPONetworkAddress* address = UGGPONetworkAddress::CreateNetworkAddress( 128 | Outer, 129 | FName(FString::Printf(TEXT("S%dIPAddress"), i + 1 - remoteIndex)), 130 | RemoteAddresses[i]); 131 | Result->Spectators.Add(address); 132 | } 133 | } 134 | } 135 | // Spectator 136 | else 137 | { 138 | if (RemoteAddresses.Num() >= 1) 139 | { 140 | // Create a GGPO Network Address and add to the addresses 141 | UGGPONetworkAddress* address = UGGPONetworkAddress::CreateNetworkAddress( 142 | Outer, 143 | FName(FString::Printf(TEXT("HostIPAddress"))), 144 | RemoteAddresses[0]); 145 | Result->Addresses.Add(address); 146 | } 147 | 148 | Result->LocalPort = InLocalPort; 149 | } 150 | 151 | return Result; 152 | } 153 | 154 | bool UGGPONetwork::AllValidAddresses() const 155 | { 156 | // If there are no players, this isn't valid 157 | if (Addresses.Num() == 0) 158 | return false; 159 | 160 | for (int32 i = 0; i < Addresses.Num(); i++) 161 | { 162 | // If an address is invalid, return false 163 | UGGPONetworkAddress* address = Addresses[i]; 164 | if (!address->IsValidAddress()) 165 | return false; 166 | } 167 | 168 | for (int32 i = 0; i < Spectators.Num(); i++) 169 | { 170 | // If an address is invalid, return false 171 | UGGPONetworkAddress* address = Spectators[i]; 172 | if (!address->IsValidAddress()) 173 | return false; 174 | } 175 | 176 | return AllUniqueAddresses(); 177 | } 178 | bool UGGPONetwork::AllUniqueAddresses() const 179 | { 180 | // Compare both remote players and spectators 181 | auto AllRemotes = Addresses; 182 | AllRemotes.Append(Spectators); 183 | 184 | for (int32 i = 0; i < AllRemotes.Num(); i++) 185 | { 186 | for (int32 j = i + 1; j < AllRemotes.Num(); j++) 187 | { 188 | // If the address is the same, return false 189 | if (AllRemotes[i]->IsSameAddress(AllRemotes[j])) 190 | return false; 191 | } 192 | } 193 | 194 | return true; 195 | } 196 | 197 | UGGPONetworkAddress* UGGPONetwork::GetAddress(int32 Index) const 198 | { 199 | if (Index < 0 || Index >= Addresses.Num()) 200 | return nullptr; 201 | 202 | return Addresses[Index]; 203 | } 204 | int32 UGGPONetwork::NumAddresses() const 205 | { 206 | return Addresses.Num(); 207 | } 208 | 209 | UGGPONetworkAddress* UGGPONetwork::GetSpectator(int32 Index) const 210 | { 211 | if (Index < 0 || Index >= Spectators.Num()) 212 | return nullptr; 213 | 214 | return Spectators[Index]; 215 | } 216 | int32 UGGPONetwork::NumSpectators() const 217 | { 218 | return Spectators.Num(); 219 | } 220 | 221 | int32 UGGPONetwork::GetLocalPort() const 222 | { 223 | // For spectators 224 | if (LocalPlayerIndex <= -1) 225 | return LocalPort; 226 | 227 | return Addresses[LocalPlayerIndex]->GetPort(); 228 | } 229 | 230 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/sync.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "sync.h" 9 | 10 | Sync::Sync(UdpMsg::connect_status *connect_status) : 11 | _local_connect_status(connect_status), 12 | _input_queues(NULL) 13 | { 14 | _framecount = 0; 15 | _last_confirmed_frame = -1; 16 | _max_prediction_frames = 0; 17 | memset(&_savedstate, 0, sizeof(_savedstate)); 18 | } 19 | 20 | Sync::~Sync() 21 | { 22 | /* 23 | * Delete frames manually here rather than in a destructor of the SavedFrame 24 | * structure so we can efficently copy frames via weak references. 25 | */ 26 | for (int i = 0; i < ARRAY_SIZE(_savedstate.frames); i++) { 27 | _callbacks.free_buffer(_savedstate.frames[i].buf); 28 | } 29 | delete [] _input_queues; 30 | _input_queues = NULL; 31 | } 32 | 33 | void 34 | Sync::Init(Sync::Config &config) 35 | { 36 | _config = config; 37 | _callbacks = config.callbacks; 38 | _framecount = 0; 39 | _rollingback = false; 40 | 41 | _max_prediction_frames = config.num_prediction_frames; 42 | 43 | CreateQueues(config); 44 | } 45 | 46 | void 47 | Sync::SetLastConfirmedFrame(int frame) 48 | { 49 | _last_confirmed_frame = frame; 50 | if (_last_confirmed_frame > 0) { 51 | for (int i = 0; i < _config.num_players; i++) { 52 | _input_queues[i].DiscardConfirmedFrames(frame - 1); 53 | } 54 | } 55 | } 56 | 57 | bool 58 | Sync::AddLocalInput(int queue, GameInput &input) 59 | { 60 | int frames_behind = _framecount - _last_confirmed_frame; 61 | if (_framecount >= _max_prediction_frames && frames_behind >= _max_prediction_frames) { 62 | Log("Rejecting local input: reached prediction barrier.\n"); 63 | return false; 64 | } 65 | 66 | if (_framecount == 0) { 67 | SaveCurrentFrame(); 68 | } 69 | 70 | Log("Sending undelayed local frame %d to queue %d.\n", _framecount, queue); 71 | input.frame = _framecount; 72 | _input_queues[queue].AddInput(input); 73 | 74 | return true; 75 | } 76 | 77 | void 78 | Sync::AddRemoteInput(int queue, GameInput &input) 79 | { 80 | _input_queues[queue].AddInput(input); 81 | } 82 | 83 | int 84 | Sync::GetConfirmedInputs(void *values, int size, int frame) 85 | { 86 | int disconnect_flags = 0; 87 | char *output = (char *)values; 88 | 89 | ASSERT(size >= _config.num_players * _config.input_size); 90 | 91 | memset(output, 0, size); 92 | for (int i = 0; i < _config.num_players; i++) { 93 | GameInput input; 94 | if (_local_connect_status[i].disconnected && frame > _local_connect_status[i].last_frame) { 95 | disconnect_flags |= (1 << i); 96 | input.erase(); 97 | } else { 98 | _input_queues[i].GetConfirmedInput(frame, &input); 99 | } 100 | memcpy(output + (i * _config.input_size), input.bits, _config.input_size); 101 | } 102 | return disconnect_flags; 103 | } 104 | 105 | int 106 | Sync::SynchronizeInputs(void *values, int size) 107 | { 108 | int disconnect_flags = 0; 109 | char *output = (char *)values; 110 | 111 | ASSERT(size >= _config.num_players * _config.input_size); 112 | 113 | memset(output, 0, size); 114 | for (int i = 0; i < _config.num_players; i++) { 115 | GameInput input; 116 | if (_local_connect_status[i].disconnected && _framecount > _local_connect_status[i].last_frame) { 117 | disconnect_flags |= (1 << i); 118 | input.erase(); 119 | } else { 120 | _input_queues[i].GetInput(_framecount, &input); 121 | } 122 | memcpy(output + (i * _config.input_size), input.bits, _config.input_size); 123 | } 124 | return disconnect_flags; 125 | } 126 | 127 | void 128 | Sync::CheckSimulation(int timeout) 129 | { 130 | int seek_to; 131 | // If the simulation is no longer synched 132 | if (!CheckSimulationConsistency(&seek_to)) { 133 | // Jump back to the most recent frame that was still in synch and re-simulate 134 | AdjustSimulation(seek_to); 135 | } 136 | } 137 | 138 | void 139 | Sync::IncrementFrame(void) 140 | { 141 | _framecount++; 142 | SaveCurrentFrame(); 143 | } 144 | 145 | void 146 | Sync::AdjustSimulation(int seek_to) 147 | { 148 | int framecount = _framecount; 149 | int count = _framecount - seek_to; 150 | 151 | Log("Catching up\n"); 152 | _rollingback = true; 153 | 154 | /* 155 | * Flush our input queue and load the last frame. 156 | */ 157 | LoadFrame(seek_to); 158 | ASSERT(_framecount == seek_to); 159 | 160 | /* 161 | * Advance frame by frame (stuffing notifications back to 162 | * the master). 163 | */ 164 | ResetPrediction(_framecount); 165 | for (int i = 0; i < count; i++) { 166 | _callbacks.advance_frame(0); 167 | } 168 | ASSERT(_framecount == framecount); 169 | 170 | _rollingback = false; 171 | 172 | Log("---\n"); 173 | } 174 | 175 | void 176 | Sync::LoadFrame(int frame) 177 | { 178 | // find the frame in question 179 | if (frame == _framecount) { 180 | Log("Skipping NOP.\n"); 181 | return; 182 | } 183 | 184 | // Move the head pointer back and load it up 185 | _savedstate.head = FindSavedFrameIndex(frame); 186 | SavedFrame *state = _savedstate.frames + _savedstate.head; 187 | 188 | Log("=== Loading frame info %d (size: %d checksum: %08x).\n", 189 | state->frame, state->cbuf, state->checksum); 190 | 191 | ASSERT(state->buf && state->cbuf); 192 | _callbacks.load_game_state(state->buf, state->cbuf); 193 | 194 | // Reset framecount and the head of the state ring-buffer to point in 195 | // advance of the current frame (as if we had just finished executing it). 196 | _framecount = state->frame; 197 | _savedstate.head = (_savedstate.head + 1) % ARRAY_SIZE(_savedstate.frames); 198 | } 199 | 200 | void 201 | Sync::SaveCurrentFrame() 202 | { 203 | /* 204 | * See StateCompress for the real save feature implemented by FinalBurn. 205 | * Write everything into the head, then advance the head pointer. 206 | */ 207 | SavedFrame *state = _savedstate.frames + _savedstate.head; 208 | if (state->buf) { 209 | _callbacks.free_buffer(state->buf); 210 | state->buf = NULL; 211 | } 212 | state->frame = _framecount; 213 | _callbacks.save_game_state(&state->buf, &state->cbuf, &state->checksum, state->frame); 214 | 215 | Log("=== Saved frame info %d (size: %d checksum: %08x).\n", state->frame, state->cbuf, state->checksum); 216 | _savedstate.head = (_savedstate.head + 1) % ARRAY_SIZE(_savedstate.frames); 217 | } 218 | 219 | Sync::SavedFrame& 220 | Sync::GetLastSavedFrame() 221 | { 222 | int i = _savedstate.head - 1; 223 | if (i < 0) { 224 | i = ARRAY_SIZE(_savedstate.frames) - 1; 225 | } 226 | return _savedstate.frames[i]; 227 | } 228 | 229 | 230 | int 231 | Sync::FindSavedFrameIndex(int frame) 232 | { 233 | int i, count = ARRAY_SIZE(_savedstate.frames); 234 | for (i = 0; i < count; i++) { 235 | if (_savedstate.frames[i].frame == frame) { 236 | break; 237 | } 238 | } 239 | if (i == count) { 240 | ASSERT(false); 241 | } 242 | return i; 243 | } 244 | 245 | 246 | bool 247 | Sync::CreateQueues(Config &config) 248 | { 249 | delete [] _input_queues; 250 | _input_queues = new InputQueue[_config.num_players]; 251 | 252 | for (int i = 0; i < _config.num_players; i++) { 253 | _input_queues[i].Init(i, _config.input_size); 254 | } 255 | return true; 256 | } 257 | 258 | bool 259 | Sync::CheckSimulationConsistency(int *seekTo) 260 | { 261 | int first_incorrect = GameInput::NullFrame; 262 | for (int i = 0; i < _config.num_players; i++) { 263 | int incorrect = _input_queues[i].GetFirstIncorrectFrame(); 264 | Log("considering incorrect frame %d reported by queue %d.\n", incorrect, i); 265 | 266 | if (incorrect != GameInput::NullFrame && (first_incorrect == GameInput::NullFrame || incorrect < first_incorrect)) { 267 | first_incorrect = incorrect; 268 | } 269 | } 270 | 271 | if (first_incorrect == GameInput::NullFrame) { 272 | Log("prediction ok. proceeding.\n"); 273 | return true; 274 | } 275 | *seekTo = first_incorrect; 276 | return false; 277 | } 278 | 279 | void 280 | Sync::SetFrameDelay(int queue, int delay) 281 | { 282 | _input_queues[queue].SetFrameDelay(delay); 283 | } 284 | 285 | 286 | void 287 | Sync::ResetPrediction(int frameNumber) 288 | { 289 | for (int i = 0; i < _config.num_players; i++) { 290 | _input_queues[i].ResetPrediction(frameNumber); 291 | } 292 | } 293 | 294 | 295 | bool 296 | Sync::GetEvent(Event &e) 297 | { 298 | if (_event_queue.size()) { 299 | e = _event_queue.front(); 300 | _event_queue.pop(); 301 | return true; 302 | } 303 | return false; 304 | } 305 | 306 | 307 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/zconf.h: -------------------------------------------------------------------------------- 1 | /* zconf.h -- configuration of the zlib compression library 2 | * Copyright (C) 1995-2002 Jean-loup Gailly. 3 | * For conditions of distribution and use, see copyright notice in zlib.h 4 | */ 5 | 6 | // Dave: 7 | #define ZEXPORT __fastcall 8 | #define ZEXPORTVA __cdecl 9 | 10 | /* @(#) $Id$ */ 11 | 12 | #ifndef _ZCONF_H 13 | #define _ZCONF_H 14 | 15 | /* 16 | * If you *really* need a unique prefix for all types and library functions, 17 | * compile with -DZ_PREFIX. The "standard" zlib should be compiled without it. 18 | */ 19 | #ifdef Z_PREFIX 20 | # define deflateInit_ z_deflateInit_ 21 | # define deflate z_deflate 22 | # define deflateEnd z_deflateEnd 23 | # define inflateInit_ z_inflateInit_ 24 | # define inflate z_inflate 25 | # define inflateEnd z_inflateEnd 26 | # define deflateInit2_ z_deflateInit2_ 27 | # define deflateSetDictionary z_deflateSetDictionary 28 | # define deflateCopy z_deflateCopy 29 | # define deflateReset z_deflateReset 30 | # define deflateParams z_deflateParams 31 | # define inflateInit2_ z_inflateInit2_ 32 | # define inflateSetDictionary z_inflateSetDictionary 33 | # define inflateSync z_inflateSync 34 | # define inflateSyncPoint z_inflateSyncPoint 35 | # define inflateReset z_inflateReset 36 | # define compress z_compress 37 | # define compress2 z_compress2 38 | # define uncompress z_uncompress 39 | # define adler32 z_adler32 40 | # define crc32 z_crc32 41 | # define get_crc_table z_get_crc_table 42 | 43 | # define Byte z_Byte 44 | # define uInt z_uInt 45 | # define uLong z_uLong 46 | # define Bytef z_Bytef 47 | # define charf z_charf 48 | # define intf z_intf 49 | # define uIntf z_uIntf 50 | # define uLongf z_uLongf 51 | # define voidpf z_voidpf 52 | # define voidp z_voidp 53 | #endif 54 | 55 | #if (defined(_WIN32) || defined(__WIN32__)) && !defined(WIN32) 56 | # define WIN32 57 | #endif 58 | #if defined(__GNUC__) || defined(WIN32) || defined(__386__) || defined(i386) 59 | # ifndef __32BIT__ 60 | # define __32BIT__ 61 | # endif 62 | #endif 63 | #if defined(__MSDOS__) && !defined(MSDOS) 64 | # define MSDOS 65 | #endif 66 | 67 | /* 68 | * Compile with -DMAXSEG_64K if the alloc function cannot allocate more 69 | * than 64k bytes at a time (needed on systems with 16-bit int). 70 | */ 71 | #if defined(MSDOS) && !defined(__32BIT__) 72 | # define MAXSEG_64K 73 | #endif 74 | #ifdef MSDOS 75 | # define UNALIGNED_OK 76 | #endif 77 | 78 | #if (defined(MSDOS) || defined(_WINDOWS) || defined(WIN32)) && !defined(STDC) 79 | # define STDC 80 | #endif 81 | #if defined(__STDC__) || defined(__cplusplus) || defined(__OS2__) 82 | # ifndef STDC 83 | # define STDC 84 | # endif 85 | #endif 86 | 87 | #ifndef STDC 88 | # ifndef const /* cannot use !defined(STDC) && !defined(const) on Mac */ 89 | # define const 90 | # endif 91 | #endif 92 | 93 | /* Some Mac compilers merge all .h files incorrectly: */ 94 | #if defined(__MWERKS__) || defined(applec) ||defined(THINK_C) ||defined(__SC__) 95 | # define NO_DUMMY_DECL 96 | #endif 97 | 98 | /* Old Borland C incorrectly complains about missing returns: */ 99 | #if defined(__BORLANDC__) && (__BORLANDC__ < 0x500) 100 | # define NEED_DUMMY_RETURN 101 | #endif 102 | 103 | 104 | /* Maximum value for memLevel in deflateInit2 */ 105 | #ifndef MAX_MEM_LEVEL 106 | # ifdef MAXSEG_64K 107 | # define MAX_MEM_LEVEL 8 108 | # else 109 | # define MAX_MEM_LEVEL 9 110 | # endif 111 | #endif 112 | 113 | /* Maximum value for windowBits in deflateInit2 and inflateInit2. 114 | * WARNING: reducing MAX_WBITS makes minigzip unable to extract .gz files 115 | * created by gzip. (Files created by minigzip can still be extracted by 116 | * gzip.) 117 | */ 118 | #ifndef MAX_WBITS 119 | # define MAX_WBITS 15 /* 32K LZ77 window */ 120 | #endif 121 | 122 | /* The memory requirements for deflate are (in bytes): 123 | (1 << (windowBits+2)) + (1 << (memLevel+9)) 124 | that is: 128K for windowBits=15 + 128K for memLevel = 8 (default values) 125 | plus a few kilobytes for small objects. For example, if you want to reduce 126 | the default memory requirements from 256K to 128K, compile with 127 | make CFLAGS="-O -DMAX_WBITS=14 -DMAX_MEM_LEVEL=7" 128 | Of course this will generally degrade compression (there's no free lunch). 129 | 130 | The memory requirements for inflate are (in bytes) 1 << windowBits 131 | that is, 32K for windowBits=15 (default value) plus a few kilobytes 132 | for small objects. 133 | */ 134 | 135 | /* Type declarations */ 136 | 137 | #ifndef OF /* function prototypes */ 138 | # ifdef STDC 139 | # define OF(args) args 140 | # else 141 | # define OF(args) () 142 | # endif 143 | #endif 144 | 145 | /* The following definitions for FAR are needed only for MSDOS mixed 146 | * model programming (small or medium model with some far allocations). 147 | * This was tsted only with MSC; for other MSDOS compilers you may have 148 | * to define NO_MEMCPY in zutil.h. If you don't need the mixed model, 149 | * just define FAR to be empty. 150 | */ 151 | #if (defined(M_I86SM) || defined(M_I86MM)) && !defined(__32BIT__) 152 | /* MSC small or medium model */ 153 | # define SMALL_MEDIUM 154 | # ifdef _MSC_VER 155 | # define FAR _far 156 | # else 157 | # define FAR far 158 | # endif 159 | #endif 160 | #if defined(__BORLANDC__) && (defined(__SMALL__) || defined(__MEDIUM__)) 161 | # ifndef __32BIT__ 162 | # define SMALL_MEDIUM 163 | # define FAR _far 164 | # endif 165 | #endif 166 | 167 | /* Compile with -DZLIB_DLL for Windows DLL support */ 168 | #if defined(ZLIB_DLL) 169 | # if defined(_WINDOWS) || defined(WINDOWS) 170 | # ifdef FAR 171 | # undef FAR 172 | # endif 173 | # include 174 | # define ZEXPORT WINAPI 175 | # ifdef WIN32 176 | # define ZEXPORTVA WINAPIV 177 | # else 178 | # define ZEXPORTVA FAR _cdecl _export 179 | # endif 180 | # endif 181 | # if defined (__BORLANDC__) 182 | # if (__BORLANDC__ >= 0x0500) && defined (WIN32) 183 | # include 184 | # define ZEXPORT __declspec(dllexport) WINAPI 185 | # define ZEXPORTRVA __declspec(dllexport) WINAPIV 186 | # else 187 | # if defined (_Windows) && defined (__DLL__) 188 | # define ZEXPORT _export 189 | # define ZEXPORTVA _export 190 | # endif 191 | # endif 192 | # endif 193 | #endif 194 | 195 | #if defined (__BEOS__) 196 | # if defined (ZLIB_DLL) 197 | # define ZEXTERN extern __declspec(dllexport) 198 | # else 199 | # define ZEXTERN extern __declspec(dllimport) 200 | # endif 201 | #endif 202 | 203 | #ifndef ZEXPORT 204 | # define ZEXPORT 205 | #endif 206 | #ifndef ZEXPORTVA 207 | # define ZEXPORTVA 208 | #endif 209 | #ifndef ZEXTERN 210 | # define ZEXTERN extern 211 | #endif 212 | 213 | #ifndef FAR 214 | # define FAR 215 | #endif 216 | 217 | #if !defined(MACOS) && !defined(TARGET_OS_MAC) 218 | typedef unsigned char Byte; /* 8 bits */ 219 | #endif 220 | typedef unsigned int uInt; /* 16 bits or more */ 221 | typedef unsigned long uLong; /* 32 bits or more */ 222 | 223 | #ifdef SMALL_MEDIUM 224 | /* Borland C/C++ and some old MSC versions ignore FAR inside typedef */ 225 | # define Bytef Byte FAR 226 | #else 227 | typedef Byte FAR Bytef; 228 | #endif 229 | typedef char FAR charf; 230 | typedef int FAR intf; 231 | typedef uInt FAR uIntf; 232 | typedef uLong FAR uLongf; 233 | 234 | #ifdef STDC 235 | typedef void FAR *voidpf; 236 | typedef void *voidp; 237 | #else 238 | typedef Byte FAR *voidpf; 239 | typedef Byte *voidp; 240 | #endif 241 | 242 | #ifdef HAVE_UNISTD_H 243 | # include /* for off_t */ 244 | # include /* for SEEK_* and off_t */ 245 | # define z_off_t off_t 246 | #endif 247 | #ifndef SEEK_SET 248 | # define SEEK_SET 0 /* Seek from beginning of file. */ 249 | # define SEEK_CUR 1 /* Seek from current position. */ 250 | # define SEEK_END 2 /* Set file pointer to EOF plus "offset" */ 251 | #endif 252 | #ifndef z_off_t 253 | # define z_off_t long 254 | #endif 255 | 256 | /* MVS linker does not support external names larger than 8 bytes */ 257 | #if defined(__MVS__) 258 | # pragma map(deflateInit_,"DEIN") 259 | # pragma map(deflateInit2_,"DEIN2") 260 | # pragma map(deflateEnd,"DEEND") 261 | # pragma map(inflateInit_,"ININ") 262 | # pragma map(inflateInit2_,"ININ2") 263 | # pragma map(inflateEnd,"INEND") 264 | # pragma map(inflateSync,"INSY") 265 | # pragma map(inflateSetDictionary,"INSEDI") 266 | # pragma map(inflate_blocks,"INBL") 267 | # pragma map(inflate_blocks_new,"INBLNE") 268 | # pragma map(inflate_blocks_free,"INBLFR") 269 | # pragma map(inflate_blocks_reset,"INBLRE") 270 | # pragma map(inflate_codes_free,"INCOFR") 271 | # pragma map(inflate_codes,"INCO") 272 | # pragma map(inflate_fast,"INFA") 273 | # pragma map(inflate_flush,"INFLU") 274 | # pragma map(inflate_mask,"INMA") 275 | # pragma map(inflate_set_dictionary,"INSEDI2") 276 | # pragma map(inflate_copyright,"INCOPY") 277 | # pragma map(inflate_trees_bits,"INTRBI") 278 | # pragma map(inflate_trees_dynamic,"INTRDY") 279 | # pragma map(inflate_trees_fixed,"INTRFI") 280 | # pragma map(inflate_trees_free,"INTRFR") 281 | #endif 282 | 283 | #endif /* _ZCONF_H */ 284 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # What's GGPO? 2 | 3 | Created in 2009, the GGPO networking SDK pioneered the use of rollback networking in peer-to-peer games. It's designed specifically to hide network latency in fast paced, twitch style games which require very precise inputs and frame perfect execution. 4 | 5 | Traditional techniques account for network transmission time by adding delay to a players input, resulting in a sluggish, laggy game-feel. Rollback networking uses input prediction and speculative execution to send player inputs to the game immediately, providing the illusion of a zero-latency network. Using rollback, the same timings, reactions, visual and audio queues, and muscle memory your players build up playing offline will translate directly online. The GGPO networking SDK is designed to make incorporating rollback networking into new and existing games as easy as possible. 6 | 7 | # How Does It Work? 8 | 9 | Rollback networking is designed to be integrated into a fully deterministic peer-to-peer engine. With full determinism, the game is guaranteed to play out the same way on all players computers if we simply feed them the same inputs. One way to achieve this is to exchange inputs for all players over the network, only executing a frame of gameplay logic when all players have received all the inputs from their peers. This often results in sluggish, unresponsive gameplay. The longer it takes to get inputs over the network, the slower the game becomes. 10 | 11 | ## Networking Using Input Delay 12 | 13 | ### In Theory... 14 | 15 | Take a look at the diagram below. It shows how 2 clients are kept synchronized in an ideal network with 0 milliseconds of latency. Player 1's inputs and game state are shown in blue, player 2's inputs are shown in red, and the network layer is shown in green. The black arrows indicate how inputs move through the system and transitions from one game state to the next. Each frame is separated by a horizontal, dashed line. Although the diagram only shows what happens from the perspective of player 1, the game on player 2's end goes through the exact same steps. 16 | 17 | ![](images/overview_image1.png) 18 | 19 | The inputs for player 1 are merged with the inputs from player 2 by the network layer before sending them to the game engine. The engine modifies the game state for the current frame using those inputs. Player 2 does the same thing: merging player 1's inputs with his own before sending the combined inputs to the game engine. The game proceeds in this manner every frame, modifying the previous frame's game state by applying logic according to the value of the the player inputs. Since player 1 and player 2 both began with the same game state and the inputs they send to their respective engines are the same, the game states of the two players will remain synchronized on every frame. 20 | 21 | ### In Practice.. 22 | 23 | The Ideal Network example assumes that packets are transmitted over the network instantaneously. Reality isn't quite so rosy. Typical broadband connections take anywhere between 5 and 150 milliseconds to transmit a packet, depending on the distance between the players and the quality of the infrastructure where the players live. That could be anywhere between 1 and 9 frames if your game runs at 60 frames per seconds. 24 | 25 | Since the game cannot process the frame until it has received the inputs from both players, it must apply 1 to 9 frames of delay, or "lag", on each player's inputs. Let's modify the previous diagram to take latency into account: 26 | 27 | ![](images/overview_image3.png) 28 | 29 | In this example it takes 3 frames to transmit a packet. This means the remote inputs sent by player 2 at frame 1 don't arrive at player 1's game console until 3 frames later. The game engine for player 1 cannot advance until it receives the input, so it's forced to delay the frame 1 for 3 frames. All subsequent frames are delayed by 3 frames as well. The network layer is generally forced to delay all merged inputs by the maximum one way transit time of the packets sent between the two players. This lag is enough to substantially affect the quality of the game play experience for many game types in all but the most ideal networking conditions. 30 | 31 | ## Removing Input Delay with Rollback Networking 32 | 33 | ### Speculative Execution 34 | 35 | GGPO prevents the input lag by hiding the latency required to send a packet using speculative execution. Let's see another diagram: 36 | 37 | ![](images/overview_image2.png) 38 | 39 | Instead of waiting for the input to arrive from the remote player, GGPO predicts what the other player is likely to do based on past inputs. It combines the predicted input with player 1's local input and immediately passes the merged inputs to your game engine so it can proceed executing the next frame, even though you have not yet received the packet containing the inputs from the other player. 40 | If GGPO's prediction were perfect, the user experience playing online would be identical to playing offline. Of course, no one can predict the future! GGPO will occasionally incorrectly predict player 2's inputs. Take another look at the diagram above. What happens if GGPO sent the wrong inputs for player 2 at frame 1? The inputs for player 2 would be different on player 1's game than in player 2's. The two games will lose synchronization and the players will be left interacting with different versions of reality. The synchronization loss cannot possibly be discovered until frame 4 when player 1 receives the correct inputs for player 2, but by then it's too late. 41 | This is why GGPO's method is called "speculative execution". What the current player sees at the current frame may be correct, but it may not be. When GGPO incorrectly predicts the inputs for the remote player, it needs to correct that error before proceeding on to the next frame. The next example explains how that happens. 42 | 43 | ### Correcting Speculative Execution Errors with Rollbacks 44 | 45 | GGPO uses rollbacks to resynchronize the clients whenever it incorrectly predicts what the remote player will do. The term "rollback" refers to the process of rewinding state and predicting new outcomes based on new, more correct information about a player's input. In the previous section we wondered what would happen if the predicted frame for remote input 1 was incorrect. Let's see how GGPO corrects the error: 46 | 47 | ![](images/overview_image5.png) 48 | 49 | GGPO checks the quality of its prediction for previous frames every time it receives a remote input. As mentioned earlier, GGPO doesn't receive the inputs for player 2's first frame until player 1's fourth. At frame 4, GGPO notices that the inputs received from the network do not match the predicted inputs sent earlier. To resynchronize the two games, GGPO needs to undo the damage caused by running the game with incorrect inputs for 3 frames. It does this by asking the game engine to go back in time to a frame before the erroneously speculated inputs were sent (i.e. to "rollback" to a previous state). Once the previous state has been restored, GGPO asks the engine to move forward one frame at a time with the corrected input stream. These frames are shown in light blue. Your game engine should advance through these frames as quickly as possible with no visible effect to the user. For example, your video renderer should not draw these frames to the screen. Your audio renderer should ideally continue to generate audio, but it should not be rendered until after the rollback, at which point samples should start playing n frames in, where n is the current frame minus the frame where the sample was generated. 50 | Once your engine reaches the frame it was on before GGPO discovered the error, GGPO drops out of rollback mode and allows the game to proceed as normal. Frames 5 and 6 in the diagram show what happens when GGPO predicts correctly. Since the game state is correct, there's no reason to rollback. 51 | 52 | # Code Structure 53 | 54 | The following diagram shows the major moving parts in the GGPO session object and their relationship to each other. Each component is described in detail below. 55 | 56 | ![](images/overview_image4.png) 57 | 58 | ## GGPO Interface 59 | 60 | The GGPO interface abstracts away the implementation details between the P2P and the Sync Test backends. The proper backend is created automatically when you call the ggpo_start_session or ggpo_start_synctest entry points. 61 | 62 | ## P2P Backend 63 | 64 | The P2P backend orchestrates a game between players. It is created by the ggpo_start_session API call. Most of the heavy lifting is done by the contained helper classes. 65 | 66 | ## Poll Object 67 | 68 | (not pictured). The poll object is a registration mechanism used by the other objects in the code. It delivers timers and notifications when waitable objects become ready. For example, the UDP backend uses the Poll object to receive notifications when new packets arrive. 69 | 70 | ## Sync Object 71 | 72 | The sync object is used to keep track of the last n-frames of game state. When its embedded prediction object notifies it of a prediction error, the Sync backend rewinds the game to the more-correct state and single-steps forward to correct the prediction error. 73 | 74 | ## Input Queue Object 75 | 76 | The InputQueue object keeps track of all the inputs received for a local or remote player. When asked for an input which it doesn't have, the input queue predicts the next input, and keeps track of this information for later so the sync object will know where to rollback to if the prediction was incorrect. The input queue also implements the frame-delay if requested. 77 | 78 | ## UDP Protocol Object 79 | 80 | The UDP protocol object handles the synchronization and input exchange protocols between any two players. It also implements the game input compression and reliable-UDP layer. Each UDP Protocol object has a contained TimeSync object which is uses to approximate the wall clock time skew between two players. 81 | 82 | ## UDP Object 83 | 84 | The UDP object is simply a dumb UDP packet sender/receiver. It's divorced from UDP protocol to ease ports to other platforms. 85 | 86 | ## Sync Test Backend 87 | 88 | (not pictured) The Sync Test backend uses the same Sync object as the P2P backend to verify your application's save state and stepping functionality execute deterministically. For more information on sync test uses, consult the Developer Guide. 89 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/input_queue.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "input_queue.h" 9 | #include "types.h" 10 | 11 | #define PREVIOUS_FRAME(offset) (((offset) == 0) ? (INPUT_QUEUE_LENGTH - 1) : ((offset) - 1)) 12 | 13 | InputQueue::InputQueue(int input_size) 14 | { 15 | Init(-1, input_size); 16 | } 17 | 18 | InputQueue::~InputQueue() 19 | { 20 | } 21 | 22 | void 23 | InputQueue::Init(int id, int input_size) 24 | { 25 | _id = id; 26 | _head = 0; 27 | _tail = 0; 28 | _length = 0; 29 | _frame_delay = 0; 30 | _first_frame = true; 31 | _last_user_added_frame = GameInput::NullFrame; 32 | _first_incorrect_frame = GameInput::NullFrame; 33 | _last_frame_requested = GameInput::NullFrame; 34 | _last_added_frame = GameInput::NullFrame; 35 | 36 | _prediction.init(GameInput::NullFrame, NULL, input_size); 37 | 38 | /* 39 | * This is safe because we know the GameInput is a proper structure (as in, 40 | * no virtual methods, no contained classes, etc.). 41 | */ 42 | memset(_inputs, 0, sizeof _inputs); 43 | for (int i = 0; i < ARRAY_SIZE(_inputs); i++) { 44 | _inputs[i].size = input_size; 45 | } 46 | } 47 | 48 | int 49 | InputQueue::GetLastConfirmedFrame() 50 | { 51 | Log("returning last confirmed frame %d.\n", _last_added_frame); 52 | return _last_added_frame; 53 | } 54 | 55 | int 56 | InputQueue::GetFirstIncorrectFrame() 57 | { 58 | return _first_incorrect_frame; 59 | } 60 | 61 | void 62 | InputQueue::DiscardConfirmedFrames(int frame) 63 | { 64 | ASSERT(frame >= 0); 65 | 66 | if (_last_frame_requested != GameInput::NullFrame) { 67 | frame = MIN(frame, _last_frame_requested); 68 | } 69 | 70 | Log("discarding confirmed frames up to %d (last_added:%d length:%d [head:%d tail:%d]).\n", 71 | frame, _last_added_frame, _length, _head, _tail); 72 | if (frame >= _last_added_frame) { 73 | _tail = _head; 74 | } else { 75 | int offset = frame - _inputs[_tail].frame + 1; 76 | 77 | Log("difference of %d frames.\n", offset); 78 | ASSERT(offset >= 0); 79 | 80 | _tail = (_tail + offset) % INPUT_QUEUE_LENGTH; 81 | _length -= offset; 82 | } 83 | 84 | Log("after discarding, new tail is %d (frame:%d).\n", _tail, _inputs[_tail].frame); 85 | ASSERT(_length >= 0); 86 | } 87 | 88 | void 89 | InputQueue::ResetPrediction(int frame) 90 | { 91 | ASSERT(_first_incorrect_frame == GameInput::NullFrame || frame <= _first_incorrect_frame); 92 | 93 | Log("resetting all prediction errors back to frame %d.\n", frame); 94 | 95 | /* 96 | * There's nothing really to do other than reset our prediction 97 | * state and the incorrect frame counter... 98 | */ 99 | _prediction.frame = GameInput::NullFrame; 100 | _first_incorrect_frame = GameInput::NullFrame; 101 | _last_frame_requested = GameInput::NullFrame; 102 | } 103 | 104 | bool 105 | InputQueue::GetConfirmedInput(int requested_frame, GameInput *input) 106 | { 107 | ASSERT(_first_incorrect_frame == GameInput::NullFrame || requested_frame < _first_incorrect_frame); 108 | int offset = requested_frame % INPUT_QUEUE_LENGTH; 109 | if (_inputs[offset].frame != requested_frame) { 110 | return false; 111 | } 112 | *input = _inputs[offset]; 113 | return true; 114 | } 115 | 116 | bool 117 | InputQueue::GetInput(int requested_frame, GameInput *input) 118 | { 119 | Log("requesting input frame %d.\n", requested_frame); 120 | 121 | /* 122 | * No one should ever try to grab any input when we have a prediction 123 | * error. Doing so means that we're just going further down the wrong 124 | * path. ASSERT this to verify that it's true. 125 | */ 126 | ASSERT(_first_incorrect_frame == GameInput::NullFrame); 127 | 128 | /* 129 | * Remember the last requested frame number for later. We'll need 130 | * this in AddInput() to drop out of prediction mode. 131 | */ 132 | _last_frame_requested = requested_frame; 133 | 134 | ASSERT(requested_frame >= _inputs[_tail].frame); 135 | 136 | if (_prediction.frame == GameInput::NullFrame) { 137 | /* 138 | * If the frame requested is in our range, fetch it out of the queue and 139 | * return it. 140 | */ 141 | int offset = requested_frame - _inputs[_tail].frame; 142 | 143 | if (offset < _length) { 144 | offset = (offset + _tail) % INPUT_QUEUE_LENGTH; 145 | ASSERT(_inputs[offset].frame == requested_frame); 146 | *input = _inputs[offset]; 147 | Log("returning confirmed frame number %d.\n", input->frame); 148 | return true; 149 | } 150 | 151 | /* 152 | * The requested frame isn't in the queue. Bummer. This means we need 153 | * to return a prediction frame. Predict that the user will do the 154 | * same thing they did last time. 155 | */ 156 | if (requested_frame == 0) { 157 | Log("basing new prediction frame from nothing, you're client wants frame 0.\n"); 158 | _prediction.erase(); 159 | } else if (_last_added_frame == GameInput::NullFrame) { 160 | Log("basing new prediction frame from nothing, since we have no frames yet.\n"); 161 | _prediction.erase(); 162 | } else { 163 | Log("basing new prediction frame from previously added frame (queue entry:%d, frame:%d).\n", 164 | PREVIOUS_FRAME(_head), _inputs[PREVIOUS_FRAME(_head)].frame); 165 | _prediction = _inputs[PREVIOUS_FRAME(_head)]; 166 | } 167 | _prediction.frame++; 168 | } 169 | 170 | ASSERT(_prediction.frame >= 0); 171 | 172 | /* 173 | * If we've made it this far, we must be predicting. Go ahead and 174 | * forward the prediction frame contents. Be sure to return the 175 | * frame number requested by the client, though. 176 | */ 177 | *input = _prediction; 178 | input->frame = requested_frame; 179 | Log("returning prediction frame number %d (%d).\n", input->frame, _prediction.frame); 180 | 181 | return false; 182 | } 183 | 184 | void 185 | InputQueue::AddInput(GameInput &input) 186 | { 187 | int new_frame; 188 | 189 | Log("adding input frame number %d to queue.\n", input.frame); 190 | 191 | /* 192 | * These next two lines simply verify that inputs are passed in 193 | * sequentially by the user, regardless of frame delay. 194 | */ 195 | ASSERT(_last_user_added_frame == GameInput::NullFrame || 196 | input.frame == _last_user_added_frame + 1); 197 | _last_user_added_frame = input.frame; 198 | 199 | /* 200 | * Move the queue head to the correct point in preparation to 201 | * input the frame into the queue. 202 | */ 203 | new_frame = AdvanceQueueHead(input.frame); 204 | if (new_frame != GameInput::NullFrame) { 205 | AddDelayedInputToQueue(input, new_frame); 206 | } 207 | 208 | /* 209 | * Update the frame number for the input. This will also set the 210 | * frame to GameInput::NullFrame for frames that get dropped (by 211 | * design). 212 | */ 213 | input.frame = new_frame; 214 | } 215 | 216 | void 217 | InputQueue::AddDelayedInputToQueue(GameInput &input, int frame_number) 218 | { 219 | Log("adding delayed input frame number %d to queue.\n", frame_number); 220 | 221 | ASSERT(input.size == _prediction.size); 222 | 223 | ASSERT(_last_added_frame == GameInput::NullFrame || frame_number == _last_added_frame + 1); 224 | 225 | ASSERT(frame_number == 0 || _inputs[PREVIOUS_FRAME(_head)].frame == frame_number - 1); 226 | 227 | /* 228 | * Add the frame to the back of the queue 229 | */ 230 | _inputs[_head] = input; 231 | _inputs[_head].frame = frame_number; 232 | _head = (_head + 1) % INPUT_QUEUE_LENGTH; 233 | _length++; 234 | _first_frame = false; 235 | 236 | _last_added_frame = frame_number; 237 | 238 | if (_prediction.frame != GameInput::NullFrame) { 239 | ASSERT(frame_number == _prediction.frame); 240 | 241 | /* 242 | * We've been predicting... See if the inputs we've gotten match 243 | * what we've been predicting. If so, don't worry about it. If not, 244 | * remember the first input which was incorrect so we can report it 245 | * in GetFirstIncorrectFrame() 246 | */ 247 | if (_first_incorrect_frame == GameInput::NullFrame && !_prediction.equal(input, true)) { 248 | Log("frame %d does not match prediction. marking error.\n", frame_number); 249 | _first_incorrect_frame = frame_number; 250 | } 251 | 252 | /* 253 | * If this input is the same frame as the last one requested and we 254 | * still haven't found any mis-predicted inputs, we can dump out 255 | * of predition mode entirely! Otherwise, advance the prediction frame 256 | * count up. 257 | */ 258 | if (_prediction.frame == _last_frame_requested && _first_incorrect_frame == GameInput::NullFrame) { 259 | Log("prediction is correct! dumping out of prediction mode.\n"); 260 | _prediction.frame = GameInput::NullFrame; 261 | } else { 262 | _prediction.frame++; 263 | } 264 | } 265 | ASSERT(_length <= INPUT_QUEUE_LENGTH); 266 | } 267 | 268 | int 269 | InputQueue::AdvanceQueueHead(int frame) 270 | { 271 | Log("advancing queue head to frame %d.\n", frame); 272 | 273 | int expected_frame = _first_frame ? 0 : _inputs[PREVIOUS_FRAME(_head)].frame + 1; 274 | 275 | frame += _frame_delay; 276 | 277 | if (expected_frame > frame) { 278 | /* 279 | * This can occur when the frame delay has dropped since the last 280 | * time we shoved a frame into the system. In this case, there's 281 | * no room on the queue. Toss it. 282 | */ 283 | Log("Dropping input frame %d (expected next frame to be %d).\n", 284 | frame, expected_frame); 285 | return GameInput::NullFrame; 286 | } 287 | 288 | while (expected_frame < frame) { 289 | /* 290 | * This can occur when the frame delay has been increased since the last 291 | * time we shoved a frame into the system. We need to replicate the 292 | * last frame in the queue several times in order to fill the space 293 | * left. 294 | */ 295 | Log("Adding padding frame %d to account for change in frame delay.\n", 296 | expected_frame); 297 | GameInput &last_frame = _inputs[PREVIOUS_FRAME(_head)]; 298 | AddDelayedInputToQueue(last_frame, expected_frame); 299 | expected_frame++; 300 | } 301 | 302 | ASSERT(frame == 0 || frame == _inputs[PREVIOUS_FRAME(_head)].frame + 1); 303 | return frame; 304 | } 305 | 306 | 307 | void 308 | InputQueue::Log(const char *fmt, ...) 309 | { 310 | char buf[1024]; 311 | size_t offset; 312 | va_list args; 313 | 314 | offset = sprintf_s(buf, ARRAY_SIZE(buf), "input q%d | ", _id); 315 | va_start(args, fmt); 316 | vsnprintf(buf + offset, ARRAY_SIZE(buf) - offset - 1, fmt, args); 317 | buf[ARRAY_SIZE(buf)-1] = '\0'; 318 | ::Log("%s", buf); 319 | va_end(args); 320 | } 321 | -------------------------------------------------------------------------------- /doc/DeveloperGuide.ja.md: -------------------------------------------------------------------------------- 1 | # GGPO開発者ガイド 2 | 3 | GGPOネットワークライブラリ開発者ガイドは、アプリケーションにGGPOネットワークライブラリを実装する開発者向けに用意されたドキュメントです。 4 | 5 | ## ゲームステートと入力 6 | 7 | ゲームには数多くの変化するパートがあるかと思います。GGPOは次の2つだけに依存します。 8 | 9 | - **ゲームステート**はゲームでの全ての状態を表します。シューティングゲームの場合、画面上にある自機と敵機の位置、ショットや敵弾の位置、敵機の体力、現在のスコアなどになります。 10 | 11 | - **ゲーム入力**はゲームステートを変更する一連のものを指します。言うまでもなく、プレイヤーが操作したジョイスティックやボタンの押下が含まれますが、入力以外のものも含みます。例えば、現在時刻を使って何かを計算した場合、フレームを開始した時の時刻も入力になります。 12 | 13 | ゲームエンジンにはゲームステートでも入力でもないものが他にもたくさんあります。例えばオーディオやビデオレンダラーはゲームの結果に影響を与えないため、ゲームステートではありません。ゲームに影響を与えない特殊効果を生成する特殊効果エンジンがあったとしたら、それもゲームステートから除外できます。 14 | 15 | ## 同期にステートと入力を使用する 16 | 17 | GGPOを使ったゲームで遊ぶ各プレイヤーは、プレイしているゲームの完全なコピーを持っています。両プレイヤーが同じゲーム内容で遊べるよう、保持しているゲームステートのコピーを同期し続ける必要があります。フレームが進む度にプレイヤー間でゲームステートの全コピーを送信するのは大きな負荷になります。代わりにGGPOはお互いの入力を送信し、各プレイヤーのゲームを進めます。これが機能するには、ゲームエンジンが3つの条件を満たしている必要があります。 18 | 19 | - ゲームのシミュレーションは完全に決定的でなければなりません。つまり、特定のゲームステートと入力があった時に、ゲームステートを1フレーム進めると全プレイヤーのゲームステートが同じにならなければいけません。 20 | - ゲームステートが完全にカプセル化され、シリアライズが可能であること。 21 | - ゲームエンジンはそのフレームのゲーム内容をレンダリングすることなく、復元、保存、フレームのシミュレーションができなくてはなりません。これはロールバックを実装するために使用されます。 22 | 23 | ## プログラミングガイド 24 | 25 | 次のセクションではあなたのアプリケーションをGGPO上で動作させるための一連の流れを紹介しています。GGPO APIの詳細な説明については、以下のGGPOリファレンスセクションを参照してください。 26 | 27 | ### GGPOとの繋ぎ込み 28 | 29 | GGPOは新規および既存のゲームエンジンと簡単に繋ぎ込みができるよう設計されています。`GGPOSessionCallbacks`フックを介してアプリケーションを呼び出すことにより、ほとんどのロールバックの実装を行います。 30 | 31 | ### GGPOSessionオブジェクトの生成 32 | 33 | `GGPOSession`オブジェクトはGGPOフレームワークへのインターフェースです。ローカルのポートと、対戦したいプレイヤーのIPアドレスとポートを`ggponet_start_session`関数を渡して作成します。またゲームステートを管理するコールバック関数で満たされた`GGPOSessionCallbacks`オブジェクトと、このセッションで遊ぶプレイヤーの数を渡す必要があります。全ての`GGPOSessionCallback`関数を実装しなければなりません。詳細は以下を参照してください。 34 | 例えば、ポート8001にバインドされた別のプレイヤーと同じホストで新しいセッションを開始する場合、次のようになります。 35 | 36 | ``` 37 | GGPOSession ggpo; 38 | GGPOErrorCode result; 39 | GGPOSessionCallbacks cb; 40 | 41 | /* fill in all callback functions */ 42 | cb.begin_game = vw_begin_game_callback; 43 | cb.advance_frame = vw_advance_frame_callback; 44 | cb.load_game_state = vw_load_game_state_callback; 45 | cb.save_game_state = vw_save_game_state_callback; 46 | cb.free_buffer = vw_free_buffer; 47 | cb.on_event = vw_on_event_callback; 48 | 49 | /* Start a new session */ 50 | result = ggpo_start_session(&ggpo, // the new session object 51 | &cb, // our callbacks 52 | "test_app", // application name 53 | 2, // 2 players 54 | sizeof(int), // size of an input packet 55 | 8001); // our local udp port 56 | ``` 57 | 58 | 59 | 60 | `GGPOSession`オブジェクトは単一のゲームセッションだけに使われるべきです。別の相手と接続する必要がある場合、`ggpo_close_session`を使用して既存のオブジェクトを閉じ、新しいオブジェクトを開始します。 61 | 62 | ``` 63 | /* Close the current session and start a new one */ 64 | ggpo_close_session(ggpo); 65 | ``` 66 | 67 | ### プレイヤーの場所を送信する 68 | 69 | GGPOSessionオブジェクトを作成した時、ゲームに参加しているプレイヤーの数を渡しましたが、実際にそれらを連携をする方法について説明していませんでした。これを行うには、各プレイヤーを表す`GGPOPlayer`オブジェクトを`ggpo_add_player`関数に渡して呼び出します。次の例は、2人用のゲームでの`ggpo_add_player`の使い方です。 70 | 71 | ``` 72 | GGPOPlayer p1, p2; 73 | GGPOPlayerHandle player_handles[2]; 74 | 75 | p1.size = p2.size = sizeof(GGPOPlayer); 76 | p1.type = GGPO_PLAYERTYPE_LOCAL; // local player 77 | p2.type = GGPO_PLAYERTYPE_REMOTE; // remote player 78 | strcpy(p2.remote.ip_address, "192.168.0.100"); // ip addess of the player 79 | p2.remote.ip_address.port = 8001; // port of that player 80 | 81 | result = ggpo_add_player(ggpo, &p1, &player_handles[0]); 82 | ... 83 | result = ggpo_add_player(ggpo, &p2, &player_handles[1]); 84 | ``` 85 | 86 | ### ローカルと遠隔プレイヤーによる入力の同期 87 | 88 | 入力の同期は各ゲームフレームの最初に行われます。各ローカルプレイヤーに対する`ggpo_add_local_input`の呼び出しと、遠隔プレイヤーの入力を取得する`ggpo_synchronize_input`の呼び出しによって行われます。 89 | `ggpo_synchronize_inputs`の戻り値は必ず確認するようにしてください。`GGPO_OK`以外の値が返ってきた場合、ゲームステートを進めないでください。これは通常、GGPOがしばらくの間、遠隔プレイヤーからパケットを受信せず、内部の予測制限に達したことで発生します。 90 | 91 | 例えば、ローカルゲームのコードが次のようになっている場合、 92 | 93 | ``` 94 | GameInputs &p1, &p2; 95 | GetControllerInputs(0, &p1); /* read p1's controller inputs */ 96 | GetControllerInputs(1, &p2); /* read p2's controller inputs */ 97 | AdvanceGameState(&p1, &p2, &gamestate); /* send p1 and p2 to the game */ 98 | ``` 99 | 100 | 次のように変更する必要があります。 101 | 102 | ``` 103 | GameInputs p[2]; 104 | GetControllerInputs(0, &p[0]); /* read the controller */ 105 | 106 | /* notify ggpo of the local player's inputs */ 107 | result = ggpo_add_local_input(ggpo, // the session object 108 | player_handles[0], // handle for p1 109 | &p[0], // p1's inputs 110 | sizeof(p[0])); // size of p1's inputs 111 | 112 | /* synchronize the local and remote inputs */ 113 | if (GGPO_SUCCEEDED(result)) { 114 | result = ggpo_synchronize_inputs(ggpo, // the session object 115 | p, // array of inputs 116 | sizeof(p)); // size of all inputs 117 | if (GGPO_SUCCEEDED(result)) { 118 | /* pass both inputs to our advance function */ 119 | AdvanceGameState(&p[0], &p[1], &gamestate); 120 | } 121 | } 122 | ``` 123 | 124 | ロールバック中に発生したものも含め、全てのフレームで`ggpo_synchronize_inputs`を呼び出す必要があります。ゲームステートを進めるためには、ローカルコントローラーから得られた値を読むのではなく、常に`ggpo_synchronize_inputs`から返された値を使用してください。ロールバック中に`ggpo_synchronize_inputs`は`ggpo_add_local_input`に渡された値を前のフレームに使われた値に置き換えます。また、ロールバックの影響を緩和するためにローカルプレイヤー向けの入力遅延を加えた場合、`ggpo_add_local_input`に渡された入力はフレーム遅延が終わるまで`ggpo_synchronize_inputs`に返されません。 125 | 126 | ### 保存、復元、解放コールバックの実装 127 | 128 | GGPOはゲームステートを定期的に保存または復元するために、`load_game_state`と`save_game_state`コールバックを使用します。`save_game_state`関数はゲームの現在のステートを復元し、それを`buffer`出力パラメーターで返すのに十分な情報を含むバッファーを作成する必要があります。`load_game_state`関数は以前に保存したバッファーからゲームステートを復元します。例えば、 129 | 130 | ``` 131 | struct GameState gamestate; // Suppose the authoritative value of our game's state is in here. 132 | 133 | bool __cdecl 134 | ggpo_save_game_state_callback(unsigned char **buffer, int *len, 135 | int *checksum, int frame) 136 | { 137 | *len = sizeof(gamestate); 138 | *buffer = (unsigned char *)malloc(*len); 139 | if (!*buffer) { 140 | return false; 141 | } 142 | memcpy(*buffer, &gamestate, *len); 143 | return true; 144 | } 145 | 146 | bool __cdecl 147 | ggpo_load_game_state_callback(unsigned char *buffer, int len) 148 | { 149 | memcpy(&gamestate, buffer, len); 150 | return true; 151 | } 152 | ``` 153 | 154 | 不要になったら、GGPOは`free_buffer`コールバックを呼び出して、`save_game_state`コールバックで割り当てたメモリを解放します。 155 | 156 | ``` 157 | void __cdecl 158 | ggpo_free_buffer(void *buffer) 159 | { 160 | free(buffer); 161 | } 162 | ``` 163 | 164 | ### 残っているコールバックの実装 165 | 166 | 前述のように、`GGPOSessionCallbacks`構造体にはオプション扱いのコールバックはありません。これらは少なくとも`return true`である必要がありますが、残りのコールバックは必ずしもすぐに実装する必要はありません。詳細については`ggponet.h`のコメントを参照してください。 167 | 168 | ### ggpo_advance_frameとggpo_idle関数の呼び出し 169 | 170 | いよいよ終わりに近づいてきました。大丈夫、お約束します。最後のステップはゲームステートを1フレーム進める度にGGPOへ通知することです。1フレームを終えた後、次のフレームを開始する前に`ggpo_advance_frame`を呼び出すだけです。 171 | 172 | GGPOは内部記録を行うパケットを送受信するために、一定の時間が必要になります。GGPOに許可したミリ秒単位で、最低でもフレームごとに1回は`ggpo_idle`関数を呼び出す必要があります。 173 | 174 | ## アプリケーションのチューニング: フレーム遅延 vs 投機的実行 175 | 176 | GGPOは遅延を感じさせないようにするために、フレーム遅延と投機的実行の両方を使用します。これは、アプリケーション開発者が入力を遅延させるフレーム数を選択できるようにすることで実現します。もしゲームのフレーム数よりパケットの送信に時間がかかった場合、GGPOは投機的実行を使って残りの遅延を隠します。この数値は、必要に応じてゲーム中でも調整することができます。フレーム遅延の適切な値はゲームに大きく依存します。役に立つヒントをいくつか紹介しましょう。 177 | 178 | まずはゲームを遊ぶ感覚に影響を与えない範囲で、フレーム遅延を出来るだけ大きく設定してみてください。例えば格闘ゲームではドット単位の精度、寸分違わぬタイミング、非常に正確なアーケードコントローラーの操作が必要となります。このタイプのゲームでは、ほとんどの中級プレイヤーは2フレームの遅延に気付き、上級プレイヤーであれば1フレームの遅延に気付くこともあります。一方、厳密な操作を必要としないボードゲームやパズルゲームであれば、4~5のフレーム遅延を設定すればユーザーが気付く前に上手くゲームを進められるかもしれません。 179 | 180 | フレーム遅延を大きく設定するもうひとつの理由は、ロールバック中に発生し得るグリッチ(不具合)を排除することにあります。ロールバックが長くなればなるほど、間違った予測フレームを一時的に実行したことによって生じた、本来存在しないシーンを継ぎ接ぎした様子が表示される可能性が高くなります。例えば、ユーザーがボタンを押した瞬間に2フレームの画面フラッシュが起きるゲームがあったとします。フレーム遅延を1に設定し、パケット送信に4フレームかかった場合、ロールバックは約3フレーム分(4 - 1 = 3)になります。フラッシュがロールバックの最初のフレームで発生した場合、2フレームのフラッシュはロールバックによって完全に消失してしまい、遠隔で遊ぶプレイヤーはフラッシュ演出を見ることができなくなります。この場合、さらに大きなフレーム遅延値を設定するか、ロールバック発生後までフラッシュを遅らせるようビデオレンダラーを再設計するのが良いでしょう。 181 | 182 | ## サンプルアプリケーション 183 | 184 | ソースディレクトリ内のVector Warには、GGPOを使った2つのクライアントを同期する単純なアプリケーションが含まれています。コマンドライン引数は以下の通りです。 185 | 186 | ``` 187 | vectorwar.exe ('local' | :) for each player 188 | ``` 189 | 190 | 2~4プレイヤーでのゲーム開始方法の例については、binディレクトリにある.cmdファイルを参照してください。 191 | 192 | ## ベストプラクティスとトラブルシューティング 193 | 194 | 以下はアプリケーションをGGPO上で動作させる際に検討したいベストプラクティスの一覧です。これら推奨事項は、まだゲームを作り初めていない段階でも簡単に理解できます。多くのアプリケーションは既にほとんどの推奨事項を満たしています。 195 | 196 | ### ゲームステートを非ゲームステートから分離する 197 | 198 | GGPOは定期的にゲームステート全体の保存と復元を要求します。ほとんどのゲームにおいて、保存が必要なステートはゲーム全体のごく一部です。通常、ビデオやオーディオレンダラー、テーブルの検索、テクスチャー、サウンドデータ、コードセグメントは、フレームごとに不変であるか、ゲームステートの計算には影響しません。これらを保存または復元する必要はありません。 199 | 200 | できるだけゲーム以外の状態をゲームステートから分離する必要があります。例えば、全ゲームステートをC言語の構造体にカプセル化することを考えるかもしれません。これは、ゲームステートであるものとそうでないものが明確に区別され、保存と復元のコールバック実装が簡単になります(詳細についてはリファレンスガイドを参照してください)。 201 | 202 | ### ゲームステートを進める際の固定時間を定義する 203 | 204 | GGPOは、フレームごとにアプリケーションのロールバックとシングルステップ実行を必要とすることがあります。もしゲームステートを可変ティックレートで進めている場合、実行は困難になります。レンダーループがそうでない場合でも、フレームごとに固定時間単位でゲームステートを進めるようにしてください。 205 | 206 | ### ゲームループ内にあるレンダリングからゲームステートの更新を分離する 207 | 208 | GGPOはロールバック中に、advance frameコールバックを何度も呼び出します。ロールバック中に発生するエフェクトやサウンドはロールバックが完了するまで先延ばしする必要があります。これはゲームステートとレンダーステートを分離することで最も簡単に実現できます。分離が出来たら、ゲームループは次のようになるでしょう。 209 | 210 | ``` 211 | Bool finished = FALSE; 212 | GameState state; 213 | Inputs inputs; 214 | 215 | do { 216 | GetControllerInputs(&inputs); 217 | finished = AdvanceGameState(&inputs, &state); 218 | if (!finished) { 219 | RenderCurrentFrame(&gamestate); 220 | } 221 | while (!finished); 222 | ``` 223 | 224 | 言い換えると、ゲームステートは入力のみで決定され、レンダリングは現在のゲームステートによって実行される必要があります。また、レンダリングせずに一連の入力を元にゲームステートを簡単に進める方法が必要です。 225 | 226 | ### ゲームステートの進行が決定的であることを確認する 227 | 228 | ゲームステートを特定したら、次のゲームステートが入力のみから計算されることを確認します。これは、全てのゲームステートと入力を正しく識別できていれば自然とそうなりますが、時には注意が必要です。見落とされがちなことをいくつか紹介します。 229 | 230 | #### 乱数ジェネレーターに気を付ける 231 | 232 | 次のゲームステートを計算するうえで、多くのゲームは乱数を使用します。もし乱数を使う場合、それらが完全に決定的であること、乱数ジェネレーターのシードが両プレイヤーの0フレーム目で同じであること、乱数ジェネレーターの状態がゲームステートに含まれていることを確認してください。これらのことが行われていれば、特定のフレームに対して生成される乱数は、GGPOがそのフレームをロールバックする回数に関係なく、常に同じ値になります。 233 | 234 | #### 外部の時刻情報(壁時計時間)に気を付ける 235 | 236 | ゲームステートの計算に現在時刻を使う場合は注意してください。ゲームに影響を与えたり、別のゲームステートに導く可能性があります(例: 乱数ジェネレーターのシードにタイマーを使う)。2台のコンピューターまたはゲームコンソールの時刻が同期することはほとんどないため、ゲームステートの計算に時刻を使用すると同期のトラブルに繋がります。ゲームステートに時刻を使うのを止めるか、プレイヤーの現在時刻をフレームへの入力の一部として含め、常にその時刻を使って計算を行う必要があります。 237 | 238 | ゲームステート以外の計算に外部の時刻情報を使う分には問題ありません(例: 画面上のエフェクト時間の計算やオーディオサンプルの減衰など)。 239 | 240 | ### ダングリングポインターに気を付ける 241 | 242 | ゲームステートに動的に割り当てられたメモリが含まれる場合、データの保存や復元の際に十分に気を付けながらポインターの再配置を行ってください。これを緩和するひとつの方法は、ポインターの代わりにベースとオフセットを使って割り当てられたメモリを参照することです。これにより再配置が必要なポインターの数を大幅に減らすことができます。 243 | 244 | ### 静的変数や隠れたステートに気を付ける 245 | 246 | ゲームが記述されている言語には、全てのステートの追跡を困難にさせる機能があるかもしれません。C言語の静的自動変数はこの動作の一例です。該当する全ての箇所を探し出し、保存可能な形式に変換する必要があります。例えば、以下を見比べてください。 247 | 248 | ``` 249 | // This will totally get you into trouble. 250 | int get_next_counter(void) { 251 | static int counter = 0; /* no way to roll this back... */ 252 | counter++; 253 | return counter; 254 | } 255 | ``` 256 | 257 | 次のように書き換えます。 258 | ``` 259 | // If you must, this is better 260 | static int global_counter = 0; /* move counter to a global */ 261 | 262 | int get_next_counter(void) { 263 | global_counter++; 264 | return global_counter; /* use the global value */ 265 | } 266 | 267 | bool __cdecl 268 | ggpo_load_game_state_callback(unsigned char *buffer, int len) 269 | { 270 | ... 271 | global_counter = *((int *)buffer) /* restore it in load callback */ 272 | ... 273 | return true; 274 | } 275 | ``` 276 | 277 | ### GGPOの同期テスト機能をたくさん使いましょう 278 | 279 | あなたのアプリケーションがGGPO上で動作するようになったら、`ggpo_start_synctest`関数を使ってゲームステートの漏れによる同期問題を追跡することができます。 280 | 281 | この同期テストセッションは、シミュレーション決定論におけるエラーを探すために設計された特別なシングルプレイヤーセッションです。同期テストセッションで実行すると、GGPOは全てのフレームに対して1フレームのロールバックを行います。フレームが最初に実行されたときのステートとロールバック中に実行されたステートを比較し、それらが異なっていた場合はエラーを発生させます。ゲーム実行中に`ggpo_log`関数を使用すると、初回フレームのログとロールバックフレームのログを比較してエラーを追跡することができます。 282 | 283 | ゲームコードを書いている時に開発システム上で同期テストを継続的に実行することで、同期ズレの原因となったバグをすぐに見つけることができます。 284 | 285 | ## さらに詳しく知りたい方は 286 | 287 | このドキュメントではGGPOの基本的な機能について紹介しました。さらに知りたい方は、`ggponet.h`ヘッダーにあるコメント、そしてコードを直接読むことをお勧めします。それではみなさん頑張ってください! 288 | -------------------------------------------------------------------------------- /doc/DeveloperGuide.md: -------------------------------------------------------------------------------- 1 | # GGPO Developer Guide 2 | 3 | The GGPO Network Library Developer Guide is for developers who are integrating the GGPO Network Library into their applications. 4 | 5 | ## Game State and Inputs 6 | 7 | Your game probably has many moving parts. GGPO only depends on these two: 8 | 9 | - **Game State** describes the current state of everything in your game. In a shooter, this would include the position of the ship and all the enemies on the screen, the location of all the bullets, how much health each opponent has, the current score, etc. etc. 10 | 11 | - **Game Inputs** are the set of things which modify the game state. These obviously include the joystick and button presses done by the player, but can include other non-obvious inputs as well. For example, if your game uses the current time of day to calculate something in the game, the current time of day at the beginning of a frame is also an input. 12 | 13 | There are many other things in your game engine that are neither game state nor inputs. For example, your audio and video renderers are not game state since they don't have an effect on the outcome of the game. If you have a special effects engine that's generating effects that do not have an impact on the game, they can be excluded from the game state as well. 14 | 15 | ## Using State and Inputs for Synchronization 16 | 17 | Each player in a GGPO networked game has a complete copy of your game running. GGPO needs to keep both copies of the game state in sync to ensure that both players are experiencing the same game. It would be much too expensive to send an entire copy of the game state between players every frame. Instead GGPO sends the players' inputs to each other and has each player step the game forward. In order for this to work, your game engine must meet three criteria: 18 | 19 | - The game simulation must be fully deterministic. That is, for any given game state and inputs, advancing the game state by exactly 1 frame must result in identical game states for all players. 20 | - The game state must be fully encapsulated and serializable. 21 | - Your game engine must be able to load, save, and execute a single simulation frame without rendering the result of that frame. This will be used to implement rollbacks. 22 | 23 | ## Programming Guide 24 | 25 | The following section contains a walk-through for porting your application to GGPO. For a detailed description of the GGPO API, please see the GGPO Reference section, below. 26 | 27 | ### Interfacing with GGPO 28 | 29 | GGPO is designed to be easy to interface with new and existing game engines. It handles most of the implementation of handling rollbacks by calling out to your application via the `GGPOSessionCallbacks` hooks. 30 | 31 | ### Creating the GGPOSession Object 32 | 33 | The `GGPOSession` object is your interface to the GGPO framework. Create one with the `ggponet_start_session` function passing the port to bind to locally and the IP address and port of the player you'd like to play against. You should also pass in a `GGPOSessionCallbacks` object filled in with your game's callback functions for managing game state and whether this session is for player 1 or player 2. All `GGPOSessionCallback` functions must be implemented. See the reference for more details. 34 | For example, to start a new session on the same host with another player bound to port 8001, you would do: 35 | 36 | ``` 37 | GGPOSession ggpo; 38 | GGPOErrorCode result; 39 | GGPOSessionCallbacks cb; 40 | 41 | /* fill in all callback functions */ 42 | cb.begin_game = vw_begin_game_callback; 43 | cb.advance_frame = vw_advance_frame_callback; 44 | cb.load_game_state = vw_load_game_state_callback; 45 | cb.save_game_state = vw_save_game_state_callback; 46 | cb.free_buffer = vw_free_buffer; 47 | cb.on_event = vw_on_event_callback; 48 | 49 | /* Start a new session */ 50 | result = ggpo_start_session(&ggpo, // the new session object 51 | &cb, // our callbacks 52 | "test_app", // application name 53 | 2, // 2 players 54 | sizeof(int), // size of an input packet 55 | 8001); // our local udp port 56 | ``` 57 | 58 | 59 | 60 | The `GGPOSession` object should only be used for a single game session. If you need to connect to another opponent, close your existing object using `ggpo_close_session` and start a new one: 61 | 62 | ``` 63 | /* Close the current session and start a new one */ 64 | ggpo_close_session(ggpo); 65 | ``` 66 | 67 | ### Sending Player Locations 68 | 69 | When you created the GGPOSession object passed in the number of players participating in the game, but didn't actually describe how to contact them. To do so, call the `ggpo_add_player` function with a `GGPOPlayer` object describing each player. The following example show how you might use ggpo_add_player in a 2 player game: 70 | 71 | ``` 72 | GGPOPlayer p1, p2; 73 | GGPOPlayerHandle player_handles[2]; 74 | 75 | p1.size = p2.size = sizeof(GGPOPlayer); 76 | p1.type = GGPO_PLAYERTYPE_LOCAL; // local player 77 | p2.type = GGPO_PLAYERTYPE_REMOTE; // remote player 78 | strcpy(p2.remote.ip_address, "192.168.0.100"); // ip addess of the player 79 | p2.remote.ip_address.port = 8001; // port of that player 80 | 81 | result = ggpo_add_player(ggpo, &p1, &player_handles[0]); 82 | ... 83 | result = ggpo_add_player(ggpo, &p2, &player_handles[1]); 84 | ``` 85 | 86 | ### Synchronizing Local and Remote Inputs 87 | 88 | Input synchronization happens at the top of each game frame. This is done by calling `ggpo_add_local_input` for each local player and `ggpo_synchronize_input` to fetch the inputs for remote players. 89 | Be sure to check the return value of `ggpo_synchronize_inputs`. If it returns a value other than `GGPO_OK`, you should not advance your game state. This usually happens because GGPO has not received packets from the remote player in a while and has reached its internal prediction limit. 90 | 91 | For example, if your code looks like this currently for a local game: 92 | 93 | ``` 94 | GameInputs &p1, &p2; 95 | GetControllerInputs(0, &p1); /* read p1's controller inputs */ 96 | GetControllerInputs(1, &p2); /* read p2's controller inputs */ 97 | AdvanceGameState(&p1, &p2, &gamestate); /* send p1 and p2 to the game */ 98 | ``` 99 | 100 | You should change it to read as follows: 101 | 102 | ``` 103 | GameInputs p[2]; 104 | GetControllerInputs(0, &p[0]); /* read the controller */ 105 | 106 | /* notify ggpo of the local player's inputs */ 107 | result = ggpo_add_local_input(ggpo, // the session object 108 | player_handles[0], // handle for p1 109 | &p[0], // p1's inputs 110 | sizeof(p[0])); // size of p1's inputs 111 | 112 | /* synchronize the local and remote inputs */ 113 | if (GGPO_SUCCEEDED(result)) { 114 | result = ggpo_synchronize_inputs(ggpo, // the session object 115 | p, // array of inputs 116 | sizeof(p)); // size of all inputs 117 | if (GGPO_SUCCEEDED(result)) { 118 | /* pass both inputs to our advance function */ 119 | AdvanceGameState(&p[0], &p[1], &gamestate); 120 | } 121 | } 122 | ``` 123 | 124 | You should call `ggpo_synchronize_inputs` every frame, even those that happen during a rollback. Make sure you always use the values returned from `ggpo_synchronize_inputs` rather than the values you've read from the local controllers to advance your game state. During a rollback `ggpo_synchronize_inputs` will replace the values passed into `ggpo_add_local_input` with the values used for previous frames. Also, if you've manually added input delay for the local player to smooth out the effect of rollbacks, the inputs you pass into `ggpo_add_local_input` won't actually be returned in `ggpo_synchronize_inputs` until after the frame delay. 125 | 126 | ### Implementing your save, load, and free Callbacks 127 | 128 | GGPO will use the `load_game_state` and `save_game_state` callbacks to periodically save and restore the state of your game. The `save_game_state` function should create a buffer containing enough information to restore the current state of the game and return it in the `buffer` out parameter. The `load_game_state` function should restore the game state from a previously saved buffer. For example: 129 | 130 | ``` 131 | struct GameState gamestate; // Suppose the authoritative value of our game's state is in here. 132 | 133 | bool __cdecl 134 | ggpo_save_game_state_callback(unsigned char **buffer, int *len, 135 | int *checksum, int frame) 136 | { 137 | *len = sizeof(gamestate); 138 | *buffer = (unsigned char *)malloc(*len); 139 | if (!*buffer) { 140 | return false; 141 | } 142 | memcpy(*buffer, &gamestate, *len); 143 | return true; 144 | } 145 | 146 | bool __cdecl 147 | ggpo_load_game_state_callback(unsigned char *buffer, int len) 148 | { 149 | memcpy(&gamestate, buffer, len); 150 | return true; 151 | } 152 | ``` 153 | 154 | GGPO will call your `free_buffer` callback to dispose of the memory you allocated in your `save_game_state` callback when it is no longer need. 155 | 156 | ``` 157 | void __cdecl 158 | ggpo_free_buffer(void *buffer) 159 | { 160 | free(buffer); 161 | } 162 | ``` 163 | 164 | ### Implementing Remaining Callbacks 165 | 166 | As mentioned previously, there are no optional callbacks in the `GGPOSessionCallbacks` structure. They all need to at least `return true`, but the remaining callbacks do not necessarily need to be implemented right away. See the comments in `ggponet.h` for more information. 167 | 168 | ### Calling the GGPO Advance and Idle Functions 169 | 170 | We're almost done. Promise. The last step is notify GGPO every time your gamestate finishes advancing by one frame. Just call `ggpo_advance_frame` after you've finished one frame but before you've started the next. 171 | 172 | GGPO also needs some amount of time to send and receive packets do its own internal bookkeeping. At least once per-frame you should call the `ggpo_idle` function with the number of milliseconds you're allowing GGPO to spend. 173 | 174 | ## Tuning Your Application: Frame Delay vs. Speculative Execution 175 | 176 | GGPO uses both frame delay and speculative execution to hide latency. It does so by allowing the application developer the choice of how many frames that they'd like to delay input by. If it takes more time to transmit a packet than the number of frames specified by the game, GGPO will use speculative execution to hide the remaining latency. This number can be tuned by the application mid-game if you so desire. Choosing a proper value for the frame delay depends very much on your game. Here are some helpful hints. 177 | 178 | In general you should try to make your frame delay as high as possible without affecting the qualitative experience of the game. For example, a fighting game requires pixel perfect accuracy, excellent timing, and extremely tightly controlled joystick motions. For this type of game, any frame delay larger than 1 can be noticed by most intermediate players, and expert players may even notice a single frame of delay. On the other hand, board games or puzzle games which do not have very strict timing requirements may get away with setting the frame latency as high as 4 or 5 before users begin to notice. 179 | 180 | Another reason to set the frame delay high is to eliminate the glitching that can occur during a rollback. The longer the rollback, the more likely the user is to notice the discontinuities caused by temporarily executing the incorrect prediction frames. For example, suppose your game has a feature where the entire screen will flash for exactly 2 frames immediately after the user presses a button. Suppose further that you've chosen a value of 1 for the frame latency and the time to transmit a packet is 4 frames. In this case, a rollback is likely to be around 3 frames (4 – 1 = 3). If the flash occurs on the first frame of the rollback, your 2-second flash will be entirely consumed by the rollback, and the remote player will never get to see it! In this case, you're better off either specifying a higher frame latency value or redesigning your video renderer to delay the flash until after the rollback occurs. 181 | 182 | ## Sample Application 183 | 184 | The Vector War application in the source directory contains a simple application which uses GGPO to synchronize the two clients. The command line arguments are: 185 | 186 | ``` 187 | vectorwar.exe ('local' | :) for each player 188 | ``` 189 | 190 | See the .cmd files in the bin directory for examples on how to start 2, 3, and 4 player games. 191 | 192 | ## Best Practices and Troubleshooting 193 | 194 | Below is a list of recommended best practices you should consider while porting your application to GGPO. Many of these recommendations are easy to follow even if you're not starting a game from scratch. Most applications will already conform to most of the recommendations below. 195 | 196 | ### Isolate Game State from Non-Game State 197 | 198 | GGPO will periodically request that you save and load the entire state of your game. For most games the state that needs to be saved is a tiny fraction of the entire game. Usually the video and audio renderers, look up tables, textures, sound data and your code segments are either constant from frame to frame or not involved in the calculation of game state. These do not need to be saved or restored. 199 | 200 | You should isolate non-game state from the game state as much as possible. For example, you may consider encapsulating all your game state into a single C structure. This both clearly delineates what is game state and was is not and makes it trivial to implement the save and load callbacks (see the Reference Guide for more information). 201 | 202 | ### Define a Fixed Time Quanta for Advancing Your Game State 203 | 204 | GGPO will occasionally need to rollback and single-step your application frame by frame. This is difficult to do if your game state advances by a variable tick rate. You should try to make your game state advanced by a fixed time quanta per frame, even if your render loop does not. 205 | 206 | ### Separate Updating Game State from Rendering in Your Game Loop 207 | 208 | GGPO will call your advance frame callback many times during a rollback. Any effects or sounds which are genearted during the rollback need to be deferred until after the rollback is finished. This is most easily accomplished by separating your game state from your render state. When you're finished, your game loop may look something like this: 209 | 210 | ``` 211 | Bool finished = FALSE; 212 | GameState state; 213 | Inputs inputs; 214 | 215 | do { 216 | GetControllerInputs(&inputs); 217 | finished = AdvanceGameState(&inputs, &state); 218 | if (!finished) { 219 | RenderCurrentFrame(&gamestate); 220 | } 221 | while (!finished); 222 | ``` 223 | 224 | In other words, your game state should be determined solely by the inputs, your rendering code should be driven by the current game state, and you should have a way to easily advance the game state forward using a set of inputs without rendering. 225 | 226 | ### Make Sure Your Game State Advances Deterministically 227 | 228 | Once you have your game state identified, make sure the next game state is computed solely from your game inputs. This should happen naturally if you have correctly identified all the game state and inputs, but it can be tricky sometimes. Here are some things which are easy to overlook: 229 | 230 | #### Beware of Random Number Generators 231 | 232 | Many games use random numbers in the computing of the next game state. If you use one, you must ensure that they are fully deterministic, that the seed for the random number generator is same at frame 0 for both players, and that the state of the random number generator is included in your game state. Doing both of these will ensure that the random numbers which get generated for a particular frame are always the same, regardless of how many times GGPO needs to rollback to that frame. 233 | 234 | #### Beware of External Time Sources (aka. Wall clock time) 235 | 236 | Be careful if you use the current time of day in your game state calculation. This may be used for an effect on the game or to derive other game state (e.g. using the timer as a seed to the random number generator). The time on two computers or game consoles is almost never in sync and using time in your game state calculations can lead to synchronization issues. You should either eliminate the use of time in your game state or include the current time for one of the players as part of the input to a frame and always use that time in your calculations. 237 | 238 | The use of external time sources in non-gamestate calculations is fine (e.g. computing the duration of effects on screen, or the attenuation of audio samples). 239 | 240 | ### Beware of Dangling References 241 | 242 | If your game state contains any dynamically allocated memory be very careful in your save and load functions to rebase your pointers as you save and load your data. One way to mitigate this is to use a base and offset to reference allocated memory instead of a pointer. This can greatly reduce the number of pointers you need to rebase. 243 | 244 | ### Beware of Static Variables or Other Hidden State 245 | 246 | The language your game is written in may have features which make it difficult to track down all your state. Static automatic variables in C are an example of this behavior. You need to track down all these locations and convert them to a form which can be saved. For example, compare: 247 | 248 | ``` 249 | // This will totally get you into trouble. 250 | int get_next_counter(void) { 251 | static int counter = 0; /* no way to roll this back... */ 252 | counter++; 253 | return counter; 254 | } 255 | ``` 256 | 257 | To: 258 | ``` 259 | // If you must, this is better 260 | static int global_counter = 0; /* move counter to a global */ 261 | 262 | int get_next_counter(void) { 263 | global_counter++; 264 | return global_counter; /* use the global value */ 265 | } 266 | 267 | bool __cdecl 268 | ggpo_load_game_state_callback(unsigned char *buffer, int len) 269 | { 270 | ... 271 | global_counter = *((int *)buffer) /* restore it in load callback */ 272 | ... 273 | return true; 274 | } 275 | ``` 276 | 277 | ### Use the GGPO SyncTest Feature. A Lot. 278 | 279 | Once you've ported your application to GGPO, you can use the `ggpo_start_synctest` function to help track down synchronization issues which may be the result of leaky game state. 280 | 281 | The sync test session is a special, single player session which is designed to find errors in your simulation's determinism. When running in a synctest session, GGPO will execute a 1 frame rollback for every frame of your game. It compares the state of the frame when it was executed the first time to the state executed during the rollback, and raises an error if they differ. If you used the `ggpo_log` function during your game's execution, you can diff the log of the initial frame vs the log of the rollback frame to track down errors. 282 | 283 | By running synctest on developer systems continuously when writing game code, you can identify desync causing bugs immediately after they're introduced. 284 | 285 | ## Where to Go from Here 286 | 287 | This document describes the most basic features of GGPO. To learn more, I recommend starting with reading the comments in the `ggponet.h` header and just diving into the code. Good luck! 288 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/backends/p2p.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "p2p.h" 9 | 10 | static const int RECOMMENDATION_INTERVAL = 240; 11 | static const int DEFAULT_DISCONNECT_TIMEOUT = 5000; 12 | static const int DEFAULT_DISCONNECT_NOTIFY_START = 750; 13 | 14 | Peer2PeerBackend::Peer2PeerBackend(GGPOSessionCallbacks *cb, 15 | const char *gamename, 16 | uint16 localport, 17 | int num_players, 18 | int input_size) : 19 | _num_players(num_players), 20 | _input_size(input_size), 21 | _sync(_local_connect_status), 22 | _disconnect_timeout(DEFAULT_DISCONNECT_TIMEOUT), 23 | _disconnect_notify_start(DEFAULT_DISCONNECT_NOTIFY_START), 24 | _num_spectators(0), 25 | _next_spectator_frame(0) 26 | { 27 | _callbacks = *cb; 28 | _synchronizing = true; 29 | _next_recommended_sleep = 0; 30 | 31 | /* 32 | * Initialize the synchronziation layer 33 | */ 34 | Sync::Config config = { 0 }; 35 | config.num_players = num_players; 36 | config.input_size = input_size; 37 | config.callbacks = _callbacks; 38 | config.num_prediction_frames = MAX_PREDICTION_FRAMES; 39 | _sync.Init(config); 40 | 41 | /* 42 | * Initialize the UDP port 43 | */ 44 | _udp.Init(localport, &_poll, this); 45 | 46 | _endpoints = new UdpProtocol[_num_players]; 47 | memset(_local_connect_status, 0, sizeof(_local_connect_status)); 48 | for (int i = 0; i < ARRAY_SIZE(_local_connect_status); i++) { 49 | _local_connect_status[i].last_frame = -1; 50 | } 51 | 52 | /* 53 | * Preload the ROM 54 | */ 55 | _callbacks.begin_game(gamename); 56 | } 57 | 58 | Peer2PeerBackend::~Peer2PeerBackend() 59 | { 60 | delete [] _endpoints; 61 | } 62 | 63 | void 64 | Peer2PeerBackend::AddRemotePlayer(char *ip, 65 | uint16 port, 66 | int queue) 67 | { 68 | /* 69 | * Start the state machine (xxx: no) 70 | */ 71 | _synchronizing = true; 72 | 73 | _endpoints[queue].Init(&_udp, _poll, queue, ip, port, _local_connect_status); 74 | _endpoints[queue].SetDisconnectTimeout(_disconnect_timeout); 75 | _endpoints[queue].SetDisconnectNotifyStart(_disconnect_notify_start); 76 | _endpoints[queue].Synchronize(); 77 | } 78 | 79 | GGPOErrorCode Peer2PeerBackend::AddSpectator(char *ip, 80 | uint16 port) 81 | { 82 | if (_num_spectators == GGPO_MAX_SPECTATORS) { 83 | return GGPO_ERRORCODE_TOO_MANY_SPECTATORS; 84 | } 85 | /* 86 | * Currently, we can only add spectators before the game starts. 87 | */ 88 | if (!_synchronizing) { 89 | return GGPO_ERRORCODE_INVALID_REQUEST; 90 | } 91 | int queue = _num_spectators++; 92 | 93 | _spectators[queue].Init(&_udp, _poll, queue + 1000, ip, port, _local_connect_status); 94 | _spectators[queue].SetDisconnectTimeout(_disconnect_timeout); 95 | _spectators[queue].SetDisconnectNotifyStart(_disconnect_notify_start); 96 | _spectators[queue].Synchronize(); 97 | 98 | return GGPO_OK; 99 | } 100 | 101 | GGPOErrorCode 102 | Peer2PeerBackend::DoPoll(int timeout) 103 | { 104 | if (!_sync.InRollback()) { 105 | _poll.Pump(0); 106 | 107 | PollUdpProtocolEvents(); 108 | 109 | if (!_synchronizing) { 110 | _sync.CheckSimulation(timeout); 111 | 112 | // notify all of our endpoints of their local frame number for their 113 | // next connection quality report 114 | int current_frame = _sync.GetFrameCount(); 115 | for (int i = 0; i < _num_players; i++) { 116 | _endpoints[i].SetLocalFrameNumber(current_frame); 117 | } 118 | 119 | int total_min_confirmed; 120 | if (_num_players <= 2) { 121 | total_min_confirmed = Poll2Players(current_frame); 122 | } else { 123 | total_min_confirmed = PollNPlayers(current_frame); 124 | } 125 | 126 | Log("last confirmed frame in p2p backend is %d.\n", total_min_confirmed); 127 | if (total_min_confirmed >= 0) { 128 | ASSERT(total_min_confirmed != INT_MAX); 129 | if (_num_spectators > 0) { 130 | while (_next_spectator_frame <= total_min_confirmed) { 131 | Log("pushing frame %d to spectators.\n", _next_spectator_frame); 132 | 133 | GameInput input; 134 | input.frame = _next_spectator_frame; 135 | input.size = _input_size * _num_players; 136 | _sync.GetConfirmedInputs(input.bits, _input_size * _num_players, _next_spectator_frame); 137 | for (int i = 0; i < _num_spectators; i++) { 138 | // If the spectator's queue of pending outputs is full, 139 | // the need to be disconnected because they can't 140 | // be caught up 141 | if (_spectators[i].IsPendingFull()) 142 | { 143 | Log(EGGPOLogVerbosity::Info, "disconnecting spectator %d because their pending output buffer is full.\n", i); 144 | DisconnectSpectatorQueue(i); 145 | } 146 | else 147 | _spectators[i].SendInput(input); 148 | } 149 | _next_spectator_frame++; 150 | } 151 | } 152 | Log("setting confirmed frame in sync to %d.\n", total_min_confirmed); 153 | _sync.SetLastConfirmedFrame(total_min_confirmed); 154 | } 155 | 156 | // send timesync notifications if now is the proper time 157 | if (current_frame > _next_recommended_sleep) { 158 | int interval = 0; 159 | for (int i = 0; i < _num_players; i++) { 160 | interval = MAX(interval, _endpoints[i].RecommendFrameDelay()); 161 | } 162 | 163 | if (interval > 0) { 164 | GGPOEvent info; 165 | info.code = GGPO_EVENTCODE_TIMESYNC; 166 | info.u.timesync.frames_ahead = interval; 167 | _callbacks.on_event(&info); 168 | _next_recommended_sleep = current_frame + RECOMMENDATION_INTERVAL; 169 | } 170 | } 171 | // XXX: this is obviously a farce... 172 | if (timeout) { 173 | Sleep(1); 174 | } 175 | } 176 | } 177 | return GGPO_OK; 178 | } 179 | 180 | int Peer2PeerBackend::Poll2Players(int current_frame) 181 | { 182 | int i; 183 | 184 | // discard confirmed frames as appropriate 185 | int total_min_confirmed = MAX_INT; 186 | for (i = 0; i < _num_players; i++) { 187 | bool queue_connected = true; 188 | if (_endpoints[i].IsRunning()) { 189 | int ignore; 190 | queue_connected = _endpoints[i].GetPeerConnectStatus(i, &ignore); 191 | } 192 | if (!_local_connect_status[i].disconnected) { 193 | total_min_confirmed = MIN(_local_connect_status[i].last_frame, total_min_confirmed); 194 | } 195 | Log(" local endp: connected = %d, last_received = %d, total_min_confirmed = %d.\n", !_local_connect_status[i].disconnected, _local_connect_status[i].last_frame, total_min_confirmed); 196 | if (!queue_connected && !_local_connect_status[i].disconnected) { 197 | Log(EGGPOLogVerbosity::Info, "disconnecting i %d by remote request.\n", i); 198 | DisconnectPlayerQueue(i, total_min_confirmed); 199 | } 200 | Log(" total_min_confirmed = %d.\n", total_min_confirmed); 201 | } 202 | return total_min_confirmed; 203 | } 204 | 205 | int Peer2PeerBackend::PollNPlayers(int current_frame) 206 | { 207 | int i, queue, last_received; 208 | 209 | // discard confirmed frames as appropriate 210 | int total_min_confirmed = MAX_INT; 211 | for (queue = 0; queue < _num_players; queue++) { 212 | bool queue_connected = true; 213 | int queue_min_confirmed = MAX_INT; 214 | Log("considering queue %d.\n", queue); 215 | for (i = 0; i < _num_players; i++) { 216 | // we're going to do a lot of logic here in consideration of endpoint i. 217 | // keep accumulating the minimum confirmed point for all n*n packets and 218 | // throw away the rest. 219 | if (_endpoints[i].IsRunning()) { 220 | bool connected = _endpoints[i].GetPeerConnectStatus(queue, &last_received); 221 | 222 | queue_connected = queue_connected && connected; 223 | queue_min_confirmed = MIN(last_received, queue_min_confirmed); 224 | Log(" endpoint %d: connected = %d, last_received = %d, queue_min_confirmed = %d.\n", i, connected, last_received, queue_min_confirmed); 225 | } else { 226 | Log(" endpoint %d: ignoring... not running.\n", i); 227 | } 228 | } 229 | // merge in our local status only if we're still connected! 230 | if (!_local_connect_status[queue].disconnected) { 231 | queue_min_confirmed = MIN(_local_connect_status[queue].last_frame, queue_min_confirmed); 232 | } 233 | Log(" local endp: connected = %d, last_received = %d, queue_min_confirmed = %d.\n", !_local_connect_status[queue].disconnected, _local_connect_status[queue].last_frame, queue_min_confirmed); 234 | 235 | if (queue_connected) { 236 | total_min_confirmed = MIN(queue_min_confirmed, total_min_confirmed); 237 | } else { 238 | // check to see if this disconnect notification is further back than we've been before. If 239 | // so, we need to re-adjust. This can happen when we detect our own disconnect at frame n 240 | // and later receive a disconnect notification for frame n-1. 241 | if (!_local_connect_status[queue].disconnected || _local_connect_status[queue].last_frame > queue_min_confirmed) { 242 | Log(EGGPOLogVerbosity::Info, "disconnecting queue %d by remote request.\n", queue); 243 | DisconnectPlayerQueue(queue, queue_min_confirmed); 244 | } 245 | } 246 | Log(" total_min_confirmed = %d.\n", total_min_confirmed); 247 | } 248 | return total_min_confirmed; 249 | } 250 | 251 | 252 | GGPOErrorCode 253 | Peer2PeerBackend::AddPlayer(GGPOPlayer *player, 254 | GGPOPlayerHandle *handle) 255 | { 256 | if (player->type == EGGPOPlayerType::SPECTATOR) { 257 | return AddSpectator(player->u.remote.ip_address, player->u.remote.port); 258 | } 259 | 260 | int queue = player->player_num - 1; 261 | if (player->player_num < 1 || player->player_num > _num_players) { 262 | return GGPO_ERRORCODE_PLAYER_OUT_OF_RANGE; 263 | } 264 | *handle = QueueToPlayerHandle(queue); 265 | 266 | if (player->type == EGGPOPlayerType::REMOTE) { 267 | AddRemotePlayer(player->u.remote.ip_address, player->u.remote.port, queue); 268 | } 269 | return GGPO_OK; 270 | } 271 | 272 | GGPOErrorCode 273 | Peer2PeerBackend::AddLocalInput(GGPOPlayerHandle player, 274 | void *values, 275 | int size) 276 | { 277 | int queue; 278 | GameInput input; 279 | GGPOErrorCode result; 280 | 281 | if (_sync.InRollback()) { 282 | return GGPO_ERRORCODE_IN_ROLLBACK; 283 | } 284 | if (_synchronizing) { 285 | return GGPO_ERRORCODE_NOT_SYNCHRONIZED; 286 | } 287 | 288 | result = PlayerHandleToQueue(player, &queue); 289 | if (!GGPO_SUCCEEDED(result)) { 290 | return result; 291 | } 292 | 293 | input.init(-1, (char *)values, size); 294 | 295 | // Feed the input for the current frame into the synchronzation layer. 296 | if (!_sync.AddLocalInput(queue, input)) { 297 | return GGPO_ERRORCODE_PREDICTION_THRESHOLD; 298 | } 299 | 300 | if (input.frame != GameInput::NullFrame) { // xxx: <- comment why this is the case 301 | // Update the local connect status state to indicate that we've got a 302 | // confirmed local frame for this player. this must come first so it 303 | // gets incorporated into the next packet we send. 304 | 305 | Log("setting local connect status for local queue %d to %d", queue, input.frame); 306 | _local_connect_status[queue].last_frame = input.frame; 307 | 308 | // Send the input to all the remote players. 309 | for (int i = 0; i < _num_players; i++) { 310 | if (_endpoints[i].IsInitialized()) { 311 | _endpoints[i].SendInput(input); 312 | } 313 | } 314 | } 315 | 316 | return GGPO_OK; 317 | } 318 | 319 | 320 | GGPOErrorCode 321 | Peer2PeerBackend::SyncInput(void *values, 322 | int size, 323 | int *disconnect_flags) 324 | { 325 | int flags; 326 | 327 | // Wait until we've started to return inputs. 328 | if (_synchronizing) { 329 | return GGPO_ERRORCODE_NOT_SYNCHRONIZED; 330 | } 331 | flags = _sync.SynchronizeInputs(values, size); 332 | if (disconnect_flags) { 333 | *disconnect_flags = flags; 334 | } 335 | return GGPO_OK; 336 | } 337 | 338 | GGPOErrorCode 339 | Peer2PeerBackend::IncrementFrame(void) 340 | { 341 | Log("End of frame (%d)...\n", _sync.GetFrameCount()); 342 | _sync.IncrementFrame(); 343 | DoPoll(0); 344 | PollSyncEvents(); 345 | 346 | return GGPO_OK; 347 | } 348 | 349 | 350 | void 351 | Peer2PeerBackend::PollSyncEvents(void) 352 | { 353 | Sync::Event e; 354 | while (_sync.GetEvent(e)) { 355 | OnSyncEvent(e); 356 | } 357 | return; 358 | } 359 | 360 | void 361 | Peer2PeerBackend::PollUdpProtocolEvents(void) 362 | { 363 | UdpProtocol::Event evt; 364 | for (int i = 0; i < _num_players; i++) { 365 | while (_endpoints[i].GetEvent(evt)) { 366 | OnUdpProtocolPeerEvent(evt, i); 367 | } 368 | } 369 | for (int i = 0; i < _num_spectators; i++) { 370 | while (_spectators[i].GetEvent(evt)) { 371 | OnUdpProtocolSpectatorEvent(evt, i); 372 | } 373 | } 374 | } 375 | 376 | void 377 | Peer2PeerBackend::OnUdpProtocolPeerEvent(UdpProtocol::Event &evt, int queue) 378 | { 379 | OnUdpProtocolEvent(evt, QueueToPlayerHandle(queue)); 380 | switch (evt.type) { 381 | case UdpProtocol::Event::Input: 382 | if (!_local_connect_status[queue].disconnected) { 383 | int current_remote_frame = _local_connect_status[queue].last_frame; 384 | int new_remote_frame = evt.u.input.input.frame; 385 | ASSERT(current_remote_frame == -1 || new_remote_frame == (current_remote_frame + 1)); 386 | 387 | _sync.AddRemoteInput(queue, evt.u.input.input); 388 | // Notify the other endpoints which frame we received from a peer 389 | Log("setting remote connect status for queue %d to %d\n", queue, evt.u.input.input.frame); 390 | _local_connect_status[queue].last_frame = evt.u.input.input.frame; 391 | } 392 | break; 393 | 394 | case UdpProtocol::Event::Disconnected: 395 | DisconnectPlayer(QueueToPlayerHandle(queue)); 396 | break; 397 | } 398 | } 399 | 400 | 401 | void 402 | Peer2PeerBackend::OnUdpProtocolSpectatorEvent(UdpProtocol::Event &evt, int queue) 403 | { 404 | GGPOPlayerHandle handle = QueueToSpectatorHandle(queue); 405 | OnUdpProtocolEvent(evt, handle); 406 | 407 | switch (evt.type) { 408 | case UdpProtocol::Event::Disconnected: 409 | DisconnectSpectatorQueue(queue); 410 | 411 | break; 412 | } 413 | } 414 | 415 | void 416 | Peer2PeerBackend::OnUdpProtocolEvent(UdpProtocol::Event &evt, GGPOPlayerHandle handle) 417 | { 418 | GGPOEvent info; 419 | 420 | switch (evt.type) { 421 | case UdpProtocol::Event::Connected: 422 | info.code = GGPO_EVENTCODE_CONNECTED_TO_PEER; 423 | info.u.connected.player = handle; 424 | _callbacks.on_event(&info); 425 | break; 426 | case UdpProtocol::Event::Synchronizing: 427 | info.code = GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER; 428 | info.u.synchronizing.player = handle; 429 | info.u.synchronizing.count = evt.u.synchronizing.count; 430 | info.u.synchronizing.total = evt.u.synchronizing.total; 431 | _callbacks.on_event(&info); 432 | break; 433 | case UdpProtocol::Event::Synchronzied: 434 | info.code = GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER; 435 | info.u.synchronized.player = handle; 436 | _callbacks.on_event(&info); 437 | 438 | CheckInitialSync(); 439 | break; 440 | 441 | case UdpProtocol::Event::NetworkInterrupted: 442 | info.code = GGPO_EVENTCODE_CONNECTION_INTERRUPTED; 443 | info.u.connection_interrupted.player = handle; 444 | info.u.connection_interrupted.disconnect_timeout = evt.u.network_interrupted.disconnect_timeout; 445 | _callbacks.on_event(&info); 446 | break; 447 | 448 | case UdpProtocol::Event::NetworkResumed: 449 | info.code = GGPO_EVENTCODE_CONNECTION_RESUMED; 450 | info.u.connection_resumed.player = handle; 451 | _callbacks.on_event(&info); 452 | break; 453 | } 454 | } 455 | 456 | /* 457 | * Called only as the result of a local decision to disconnect. The remote 458 | * decisions to disconnect are a result of us parsing the peer_connect_settings 459 | * blob in every endpoint periodically. 460 | */ 461 | GGPOErrorCode 462 | Peer2PeerBackend::DisconnectPlayer(GGPOPlayerHandle player) 463 | { 464 | int queue; 465 | GGPOErrorCode result; 466 | 467 | result = PlayerHandleToQueue(player, &queue); 468 | if (!GGPO_SUCCEEDED(result)) { 469 | return result; 470 | } 471 | 472 | if (_local_connect_status[queue].disconnected) { 473 | return GGPO_ERRORCODE_PLAYER_DISCONNECTED; 474 | } 475 | 476 | if (!_endpoints[queue].IsInitialized()) { 477 | int current_frame = _sync.GetFrameCount(); 478 | // xxx: we should be tracking who the local player is, but for now assume 479 | // that if the endpoint is not initalized, this must be the local player. 480 | Log(EGGPOLogVerbosity::Info, "Disconnecting local player %d at frame %d by user request.\n", queue, _local_connect_status[queue].last_frame); 481 | for (int i = 0; i < _num_players; i++) { 482 | if (_endpoints[i].IsInitialized()) { 483 | DisconnectPlayerQueue(i, current_frame); 484 | } 485 | } 486 | } else { 487 | Log(EGGPOLogVerbosity::Info, "Disconnecting queue %d at frame %d by user request.\n", queue, _local_connect_status[queue].last_frame); 488 | DisconnectPlayerQueue(queue, _local_connect_status[queue].last_frame); 489 | } 490 | return GGPO_OK; 491 | } 492 | 493 | void 494 | Peer2PeerBackend::DisconnectPlayerQueue(int queue, int syncto) 495 | { 496 | GGPOEvent info; 497 | int framecount = _sync.GetFrameCount(); 498 | 499 | _endpoints[queue].Disconnect(); 500 | 501 | Log(EGGPOLogVerbosity::Info, "Changing queue %d local connect status for last frame from %d to %d on disconnect request (current: %d).\n", 502 | queue, _local_connect_status[queue].last_frame, syncto, framecount); 503 | 504 | _local_connect_status[queue].disconnected = 1; 505 | _local_connect_status[queue].last_frame = syncto; 506 | 507 | if (syncto < framecount) { 508 | Log(EGGPOLogVerbosity::Verbose, "adjusting simulation to account for the fact that %d disconnected @ %d.\n", queue, syncto); 509 | _sync.AdjustSimulation(syncto); 510 | Log("finished adjusting simulation.\n"); 511 | } 512 | 513 | info.code = GGPO_EVENTCODE_DISCONNECTED_FROM_PEER; 514 | info.u.disconnected.player = QueueToPlayerHandle(queue); 515 | _callbacks.on_event(&info); 516 | 517 | CheckInitialSync(); 518 | } 519 | 520 | void 521 | Peer2PeerBackend::DisconnectSpectatorQueue(int queue) 522 | { 523 | GGPOEvent info; 524 | GGPOPlayerHandle handle = QueueToSpectatorHandle(queue); 525 | 526 | _spectators[queue].Disconnect(); 527 | 528 | info.code = GGPO_EVENTCODE_DISCONNECTED_FROM_PEER; 529 | info.u.disconnected.player = handle; 530 | _callbacks.on_event(&info); 531 | } 532 | 533 | 534 | GGPOErrorCode 535 | Peer2PeerBackend::GetNetworkStats(FGGPONetworkStats *stats, GGPOPlayerHandle player) 536 | { 537 | int queue; 538 | GGPOErrorCode result; 539 | 540 | result = PlayerHandleToQueue(player, &queue); 541 | if (!GGPO_SUCCEEDED(result)) { 542 | return result; 543 | } 544 | 545 | memset(stats, 0, sizeof *stats); 546 | _endpoints[queue].GetNetworkStats(stats); 547 | 548 | return GGPO_OK; 549 | } 550 | 551 | GGPOErrorCode 552 | Peer2PeerBackend::SetFrameDelay(GGPOPlayerHandle player, int delay) 553 | { 554 | int queue; 555 | GGPOErrorCode result; 556 | 557 | result = PlayerHandleToQueue(player, &queue); 558 | if (!GGPO_SUCCEEDED(result)) { 559 | return result; 560 | } 561 | _sync.SetFrameDelay(queue, delay); 562 | return GGPO_OK; 563 | } 564 | 565 | GGPOErrorCode 566 | Peer2PeerBackend::SetDisconnectTimeout(int timeout) 567 | { 568 | _disconnect_timeout = timeout; 569 | for (int i = 0; i < _num_players; i++) { 570 | if (_endpoints[i].IsInitialized()) { 571 | _endpoints[i].SetDisconnectTimeout(_disconnect_timeout); 572 | } 573 | } 574 | return GGPO_OK; 575 | } 576 | 577 | GGPOErrorCode 578 | Peer2PeerBackend::SetDisconnectNotifyStart(int timeout) 579 | { 580 | _disconnect_notify_start = timeout; 581 | for (int i = 0; i < _num_players; i++) { 582 | if (_endpoints[i].IsInitialized()) { 583 | _endpoints[i].SetDisconnectNotifyStart(_disconnect_notify_start); 584 | } 585 | } 586 | return GGPO_OK; 587 | } 588 | 589 | GGPOErrorCode 590 | Peer2PeerBackend::TrySynchronizeLocal() 591 | { 592 | if (_num_players <= 1 && _num_spectators == 0) { 593 | // xxx: Same as below in CheckInitialSync(), IsInitialized() is used 594 | // to test "represents the local player" 595 | if (_num_players == 0 || !_endpoints[0].IsInitialized()) { 596 | CheckInitialSync(); 597 | } 598 | } 599 | 600 | if (_synchronizing) { 601 | return GGPO_ERRORCODE_NOT_SYNCHRONIZED; 602 | } 603 | Log(EGGPOLogVerbosity::Info, "Synchronized local-only simulation.\n"); 604 | return GGPO_OK; 605 | } 606 | 607 | GGPOErrorCode 608 | Peer2PeerBackend::PlayerHandleToQueue(GGPOPlayerHandle player, int *queue) 609 | { 610 | int offset = ((int)player - 1); 611 | if (offset < 0 || offset >= _num_players) { 612 | return GGPO_ERRORCODE_INVALID_PLAYER_HANDLE; 613 | } 614 | *queue = offset; 615 | return GGPO_OK; 616 | } 617 | 618 | 619 | void 620 | Peer2PeerBackend::OnMsg(sockaddr_in &from, UdpMsg *msg, int len) 621 | { 622 | for (int i = 0; i < _num_players; i++) { 623 | if (_endpoints[i].HandlesMsg(from, msg)) { 624 | _endpoints[i].OnMsg(msg, len); 625 | return; 626 | } 627 | } 628 | for (int i = 0; i < _num_spectators; i++) { 629 | if (_spectators[i].HandlesMsg(from, msg)) { 630 | _spectators[i].OnMsg(msg, len); 631 | return; 632 | } 633 | } 634 | } 635 | 636 | void 637 | Peer2PeerBackend::CheckInitialSync() 638 | { 639 | int i; 640 | 641 | if (_synchronizing) { 642 | // Check to see if everyone is now synchronized. If so, 643 | // go ahead and tell the client that we're ok to accept input. 644 | for (i = 0; i < _num_players; i++) { 645 | // xxx: IsInitialized() must go... we're actually using it as a proxy for "represents the local player" 646 | if (_endpoints[i].IsInitialized() && !_endpoints[i].IsSynchronized() && !_local_connect_status[i].disconnected) { 647 | return; 648 | } 649 | } 650 | for (i = 0; i < _num_spectators; i++) { 651 | if (_spectators[i].IsInitialized() && !_spectators[i].IsSynchronized()) { 652 | return; 653 | } 654 | } 655 | 656 | GGPOEvent info; 657 | info.code = GGPO_EVENTCODE_RUNNING; 658 | _callbacks.on_event(&info); 659 | _synchronizing = false; 660 | } 661 | } 662 | -------------------------------------------------------------------------------- /Source/GGPOUE/Private/network/udp_proto.cpp: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. 3 | * 4 | * Use of this software is governed by the MIT license that can be found 5 | * in the LICENSE file. 6 | */ 7 | 8 | #include "udp_proto.h" 9 | #include "../types.h" 10 | #include "../bitvector.h" 11 | 12 | static const int UDP_HEADER_SIZE = 28; /* Size of IP + UDP headers */ 13 | static const int NUM_SYNC_PACKETS = 5; 14 | static const int SYNC_RETRY_INTERVAL = 2000; 15 | static const int SYNC_FIRST_RETRY_INTERVAL = 500; 16 | static const int RUNNING_RETRY_INTERVAL = 200; 17 | static const int KEEP_ALIVE_INTERVAL = 200; 18 | static const int QUALITY_REPORT_INTERVAL = 1000; 19 | static const int NETWORK_STATS_INTERVAL = 1000; 20 | static const int UDP_SHUTDOWN_TIMER = 5000; 21 | static const int MAX_SEQ_DISTANCE = (1 << 15); 22 | 23 | UdpProtocol::UdpProtocol() : 24 | _round_trip_time(0), 25 | _kbps_sent(0), 26 | _local_frame_advantage(0), 27 | _remote_frame_advantage(0), 28 | _queue(-1), 29 | _magic_number(0), 30 | _remote_magic_number(0), 31 | _packets_sent(0), 32 | _bytes_sent(0), 33 | _stats_start_time(0), 34 | _last_send_time(0), 35 | _shutdown_timeout(0), 36 | _disconnect_timeout(0), 37 | _disconnect_notify_start(0), 38 | _disconnect_notify_sent(false), 39 | _disconnect_event_sent(false), 40 | _connected(false), 41 | _next_send_seq(0), 42 | _next_recv_seq(0), 43 | _udp(NULL) 44 | { 45 | _last_sent_input.init(-1, NULL, 1); 46 | _last_received_input.init(-1, NULL, 1); 47 | _last_acked_input.init(-1, NULL, 1); 48 | 49 | memset(&_state, 0, sizeof _state); 50 | memset(_peer_connect_status, 0, sizeof(_peer_connect_status)); 51 | for (int i = 0; i < ARRAY_SIZE(_peer_connect_status); i++) { 52 | _peer_connect_status[i].last_frame = -1; 53 | } 54 | memset(&_peer_addr, 0, sizeof _peer_addr); 55 | _oo_packet.msg = NULL; 56 | 57 | _send_latency = Platform::GetConfigInt("ggpo.network.delay"); 58 | _oop_percent = Platform::GetConfigInt("ggpo.oop.percent"); 59 | } 60 | 61 | UdpProtocol::~UdpProtocol() 62 | { 63 | ClearSendQueue(); 64 | } 65 | 66 | void 67 | UdpProtocol::Init(Udp *udp, 68 | Poll &poll, 69 | int queue, 70 | char *ip, 71 | u_short port, 72 | UdpMsg::connect_status *status) 73 | { 74 | _udp = udp; 75 | _queue = queue; 76 | _local_connect_status = status; 77 | 78 | _peer_addr.sin_family = AF_INET; 79 | _peer_addr.sin_port = htons(port); 80 | inet_pton(AF_INET, ip, &_peer_addr.sin_addr.s_addr); 81 | 82 | do { 83 | _magic_number = (uint16)rand(); 84 | } while (_magic_number == 0); 85 | poll.RegisterLoop(this); 86 | } 87 | 88 | void 89 | UdpProtocol::SendInput(GameInput &input) 90 | { 91 | if (_udp) { 92 | if (_current_state == Running) { 93 | /* 94 | * Check to see if this is a good time to adjust for the rift... 95 | */ 96 | _timesync.advance_frame(input, _local_frame_advantage, _remote_frame_advantage); 97 | 98 | /* 99 | * Save this input packet 100 | * 101 | * XXX: This queue may fill up for spectators who do not ack input packets in a timely 102 | * manner. When this happens, we can either resize the queue (ug) or disconnect them 103 | * (better, but still ug). For the meantime, make this queue really big to decrease 104 | * the odds of this happening... 105 | */ 106 | _pending_output.push(input); 107 | } 108 | SendPendingOutput(); 109 | } 110 | } 111 | 112 | void 113 | UdpProtocol::SendPendingOutput() 114 | { 115 | UdpMsg *msg = new UdpMsg(UdpMsg::Input); 116 | int i, j, offset = 0; 117 | uint8 *bits; 118 | GameInput last; 119 | 120 | if (_pending_output.size()) { 121 | last = _last_acked_input; 122 | bits = msg->u.input.bits; 123 | 124 | msg->u.input.start_frame = _pending_output.front().frame; 125 | msg->u.input.input_size = (uint8)_pending_output.front().size; 126 | 127 | ASSERT(last.frame == -1 || last.frame + 1 == msg->u.input.start_frame); 128 | for (j = 0; j < _pending_output.size(); j++) { 129 | GameInput ¤t = _pending_output.item(j); 130 | if (memcmp(current.bits, last.bits, current.size) != 0) { 131 | ASSERT((GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS * 8) < (1 << BITVECTOR_NIBBLE_SIZE)); 132 | for (i = 0; i < current.size * 8; i++) { 133 | ASSERT(i < (1 << BITVECTOR_NIBBLE_SIZE)); 134 | if (current.value(i) != last.value(i)) { 135 | BitVector_SetBit(msg->u.input.bits, &offset); 136 | (current.value(i) ? BitVector_SetBit : BitVector_ClearBit)(bits, &offset); 137 | BitVector_WriteNibblet(bits, i, &offset); 138 | } 139 | } 140 | } 141 | BitVector_ClearBit(msg->u.input.bits, &offset); 142 | last = _last_sent_input = current; 143 | } 144 | } else { 145 | msg->u.input.start_frame = 0; 146 | msg->u.input.input_size = 0; 147 | } 148 | msg->u.input.ack_frame = _last_received_input.frame; 149 | msg->u.input.num_bits = (uint16)offset; 150 | 151 | msg->u.input.disconnect_requested = _current_state == Disconnected; 152 | if (_local_connect_status) { 153 | memcpy(msg->u.input.peer_connect_status, _local_connect_status, sizeof(UdpMsg::connect_status) * UDP_MSG_MAX_PLAYERS); 154 | } else { 155 | memset(msg->u.input.peer_connect_status, 0, sizeof(UdpMsg::connect_status) * UDP_MSG_MAX_PLAYERS); 156 | } 157 | 158 | ASSERT(offset < MAX_COMPRESSED_BITS); 159 | 160 | SendMsg(msg); 161 | } 162 | 163 | void 164 | UdpProtocol::SendInputAck() 165 | { 166 | UdpMsg *msg = new UdpMsg(UdpMsg::InputAck); 167 | msg->u.input_ack.ack_frame = _last_received_input.frame; 168 | SendMsg(msg); 169 | } 170 | 171 | bool 172 | UdpProtocol::IsPendingFull() 173 | { 174 | return _pending_output.full() && _current_state != Disconnected; 175 | } 176 | 177 | bool 178 | UdpProtocol::GetEvent(UdpProtocol::Event &e) 179 | { 180 | if (_event_queue.size() == 0) { 181 | return false; 182 | } 183 | e = _event_queue.front(); 184 | _event_queue.pop(); 185 | return true; 186 | } 187 | 188 | 189 | bool 190 | UdpProtocol::OnLoopPoll(void *cookie) 191 | { 192 | if (!_udp) { 193 | return true; 194 | } 195 | 196 | unsigned int now = Platform::GetCurrentTimeMS(); 197 | unsigned int next_interval; 198 | 199 | PumpSendQueue(); 200 | switch (_current_state) { 201 | case Syncing: 202 | next_interval = (_state.sync.roundtrips_remaining == NUM_SYNC_PACKETS) ? SYNC_FIRST_RETRY_INTERVAL : SYNC_RETRY_INTERVAL; 203 | if (_last_send_time && _last_send_time + next_interval < now) { 204 | Log("No luck syncing after %d ms... Re-queueing sync packet.\n", next_interval); 205 | SendSyncRequest(); 206 | } 207 | break; 208 | 209 | case Running: 210 | // xxx: rig all this up with a timer wrapper 211 | if (!_state.running.last_input_packet_recv_time || _state.running.last_input_packet_recv_time + RUNNING_RETRY_INTERVAL < now) { 212 | Log("Haven't exchanged packets in a while (last received:%d last sent:%d). Resending.\n", _last_received_input.frame, _last_sent_input.frame); 213 | SendPendingOutput(); 214 | _state.running.last_input_packet_recv_time = now; 215 | } 216 | 217 | if (!_state.running.last_quality_report_time || _state.running.last_quality_report_time + QUALITY_REPORT_INTERVAL < now) { 218 | UdpMsg *msg = new UdpMsg(UdpMsg::QualityReport); 219 | msg->u.quality_report.ping = Platform::GetCurrentTimeMS(); 220 | msg->u.quality_report.frame_advantage = (uint8)_local_frame_advantage; 221 | SendMsg(msg); 222 | _state.running.last_quality_report_time = now; 223 | } 224 | 225 | if (!_state.running.last_network_stats_interval || _state.running.last_network_stats_interval + NETWORK_STATS_INTERVAL < now) { 226 | UpdateNetworkStats(); 227 | _state.running.last_network_stats_interval = now; 228 | } 229 | 230 | if (_last_send_time && _last_send_time + KEEP_ALIVE_INTERVAL < now) { 231 | Log("Sending keep alive packet\n"); 232 | SendMsg(new UdpMsg(UdpMsg::KeepAlive)); 233 | } 234 | 235 | if (_disconnect_timeout && _disconnect_notify_start && 236 | !_disconnect_notify_sent && (_last_recv_time + _disconnect_notify_start < now)) { 237 | ::Log(EGGPOLogVerbosity::Verbose, "Endpoint has stopped receiving packets for %d ms. Sending notification.\n", _disconnect_notify_start); 238 | Event e(Event::NetworkInterrupted); 239 | e.u.network_interrupted.disconnect_timeout = _disconnect_timeout - _disconnect_notify_start; 240 | QueueEvent(e); 241 | _disconnect_notify_sent = true; 242 | } 243 | 244 | if (_disconnect_timeout && (_last_recv_time + _disconnect_timeout < now)) { 245 | if (!_disconnect_event_sent) { 246 | ::Log(EGGPOLogVerbosity::Info, "Endpoint has stopped receiving packets for %d ms. Disconnecting.\n", _disconnect_timeout); 247 | QueueEvent(Event(Event::Disconnected)); 248 | _disconnect_event_sent = true; 249 | } 250 | } 251 | break; 252 | 253 | case Disconnected: 254 | if (_shutdown_timeout < now) { 255 | ::Log(EGGPOLogVerbosity::Info, "Shutting down udp connection.\n"); 256 | _udp = NULL; 257 | _shutdown_timeout = 0; 258 | } 259 | 260 | } 261 | 262 | 263 | return true; 264 | } 265 | 266 | void 267 | UdpProtocol::Disconnect() 268 | { 269 | _current_state = Disconnected; 270 | _shutdown_timeout = Platform::GetCurrentTimeMS() + UDP_SHUTDOWN_TIMER; 271 | } 272 | 273 | void 274 | UdpProtocol::SendSyncRequest() 275 | { 276 | _state.sync.random = rand() & 0xFFFF; 277 | UdpMsg *msg = new UdpMsg(UdpMsg::SyncRequest); 278 | msg->u.sync_request.random_request = _state.sync.random; 279 | SendMsg(msg); 280 | } 281 | 282 | void 283 | UdpProtocol::SendMsg(UdpMsg *msg) 284 | { 285 | LogMsg("send", msg); 286 | 287 | _packets_sent++; 288 | _last_send_time = Platform::GetCurrentTimeMS(); 289 | _bytes_sent += msg->PacketSize(); 290 | 291 | msg->hdr.magic = _magic_number; 292 | msg->hdr.sequence_number = _next_send_seq++; 293 | 294 | _send_queue.push(QueueEntry(Platform::GetCurrentTimeMS(), _peer_addr, msg)); 295 | PumpSendQueue(); 296 | } 297 | 298 | bool 299 | UdpProtocol::HandlesMsg(sockaddr_in &from, 300 | UdpMsg *msg) 301 | { 302 | if (!_udp) { 303 | return false; 304 | } 305 | return _peer_addr.sin_addr.S_un.S_addr == from.sin_addr.S_un.S_addr && 306 | _peer_addr.sin_port == from.sin_port; 307 | } 308 | 309 | void 310 | UdpProtocol::OnMsg(UdpMsg *msg, int len) 311 | { 312 | bool handled = false; 313 | typedef bool (UdpProtocol::*DispatchFn)(UdpMsg *msg, int len); 314 | static const DispatchFn table[] = { 315 | &UdpProtocol::OnInvalid, /* Invalid */ 316 | &UdpProtocol::OnSyncRequest, /* SyncRequest */ 317 | &UdpProtocol::OnSyncReply, /* SyncReply */ 318 | &UdpProtocol::OnInput, /* Input */ 319 | &UdpProtocol::OnQualityReport, /* QualityReport */ 320 | &UdpProtocol::OnQualityReply, /* QualityReply */ 321 | &UdpProtocol::OnKeepAlive, /* KeepAlive */ 322 | &UdpProtocol::OnInputAck, /* InputAck */ 323 | }; 324 | 325 | // filter out messages that don't match what we expect 326 | uint16 seq = msg->hdr.sequence_number; 327 | if (msg->hdr.type != UdpMsg::SyncRequest && 328 | msg->hdr.type != UdpMsg::SyncReply) { 329 | if (msg->hdr.magic != _remote_magic_number) { 330 | LogMsg("recv rejecting", msg); 331 | return; 332 | } 333 | 334 | // filter out out-of-order packets 335 | uint16 skipped = (uint16)((int)seq - (int)_next_recv_seq); 336 | // Log("checking sequence number -> next - seq : %d - %d = %d\n", seq, _next_recv_seq, skipped); 337 | if (skipped > MAX_SEQ_DISTANCE) { 338 | Log("dropping out of order packet (seq: %d, last seq:%d)\n", seq, _next_recv_seq); 339 | return; 340 | } 341 | } 342 | 343 | _next_recv_seq = seq; 344 | LogMsg("recv", msg); 345 | if (msg->hdr.type >= ARRAY_SIZE(table)) { 346 | OnInvalid(msg, len); 347 | } else { 348 | handled = (this->*(table[msg->hdr.type]))(msg, len); 349 | } 350 | if (handled) { 351 | _last_recv_time = Platform::GetCurrentTimeMS(); 352 | if (_disconnect_notify_sent && _current_state == Running) { 353 | QueueEvent(Event(Event::NetworkResumed)); 354 | _disconnect_notify_sent = false; 355 | } 356 | } 357 | } 358 | 359 | void 360 | UdpProtocol::UpdateNetworkStats(void) 361 | { 362 | int now = Platform::GetCurrentTimeMS(); 363 | 364 | if (_stats_start_time == 0) { 365 | _stats_start_time = now; 366 | } 367 | 368 | int total_bytes_sent = _bytes_sent + (UDP_HEADER_SIZE * _packets_sent); 369 | float seconds = (float)((now - _stats_start_time) / 1000.0); 370 | float Bps = total_bytes_sent / seconds; 371 | float udp_overhead = (float)(100.0 * (UDP_HEADER_SIZE * _packets_sent) / _bytes_sent); 372 | 373 | _kbps_sent = int(Bps / 1024); 374 | 375 | Log("Network Stats -- Bandwidth: %.2f KBps Packets Sent: %5d (%.2f pps) " 376 | "KB Sent: %.2f UDP Overhead: %.2f %%.\n", 377 | _kbps_sent, 378 | _packets_sent, 379 | (float)_packets_sent * 1000 / (now - _stats_start_time), 380 | total_bytes_sent / 1024.0, 381 | udp_overhead); 382 | } 383 | 384 | 385 | void 386 | UdpProtocol::QueueEvent(const UdpProtocol::Event &evt) 387 | { 388 | LogEvent("Queuing event", evt); 389 | _event_queue.push(evt); 390 | } 391 | 392 | void 393 | UdpProtocol::Synchronize() 394 | { 395 | if (_udp) { 396 | _current_state = Syncing; 397 | _state.sync.roundtrips_remaining = NUM_SYNC_PACKETS; 398 | SendSyncRequest(); 399 | } 400 | } 401 | 402 | bool 403 | UdpProtocol::GetPeerConnectStatus(int id, int *frame) 404 | { 405 | *frame = _peer_connect_status[id].last_frame; 406 | return !_peer_connect_status[id].disconnected; 407 | } 408 | 409 | void 410 | UdpProtocol::Log(const char *fmt, ...) 411 | { 412 | char buf[1024]; 413 | size_t offset; 414 | va_list args; 415 | 416 | sprintf_s(buf, ARRAY_SIZE(buf), "udpproto%d | ", _queue); 417 | offset = strlen(buf); 418 | va_start(args, fmt); 419 | vsnprintf(buf + offset, ARRAY_SIZE(buf) - offset - 1, fmt, args); 420 | buf[ARRAY_SIZE(buf)-1] = '\0'; 421 | ::Log("%s", buf); 422 | va_end(args); 423 | } 424 | 425 | void 426 | UdpProtocol::LogMsg(const char *prefix, UdpMsg *msg) 427 | { 428 | switch (msg->hdr.type) { 429 | case UdpMsg::SyncRequest: 430 | Log("%s sync-request (%d).\n", prefix, 431 | msg->u.sync_request.random_request); 432 | break; 433 | case UdpMsg::SyncReply: 434 | Log("%s sync-reply (%d).\n", prefix, 435 | msg->u.sync_reply.random_reply); 436 | break; 437 | case UdpMsg::QualityReport: 438 | Log("%s quality report.\n", prefix); 439 | break; 440 | case UdpMsg::QualityReply: 441 | Log("%s quality reply.\n", prefix); 442 | break; 443 | case UdpMsg::KeepAlive: 444 | Log("%s keep alive.\n", prefix); 445 | break; 446 | case UdpMsg::Input: 447 | Log("%s game-compressed-input %d (+ %d bits).\n", prefix, msg->u.input.start_frame, msg->u.input.num_bits); 448 | break; 449 | case UdpMsg::InputAck: 450 | Log("%s input ack.\n", prefix); 451 | break; 452 | default: 453 | ASSERT(false && "Unknown UdpMsg type."); 454 | } 455 | } 456 | 457 | void 458 | UdpProtocol::LogEvent(const char *prefix, const UdpProtocol::Event &evt) 459 | { 460 | switch (evt.type) { 461 | case UdpProtocol::Event::Synchronzied: 462 | ::Log(EGGPOLogVerbosity::Verbose, "%s (event: Synchronzied).\n", prefix); 463 | break; 464 | } 465 | } 466 | 467 | bool 468 | UdpProtocol::OnInvalid(UdpMsg *msg, int len) 469 | { 470 | ASSERT(false && "Invalid msg in UdpProtocol"); 471 | return false; 472 | } 473 | 474 | bool 475 | UdpProtocol::OnSyncRequest(UdpMsg *msg, int len) 476 | { 477 | if (_remote_magic_number != 0 && msg->hdr.magic != _remote_magic_number) { 478 | ::Log(EGGPOLogVerbosity::Info, "Ignoring sync request from unknown endpoint (%d != %d).\n", 479 | msg->hdr.magic, _remote_magic_number); 480 | return false; 481 | } 482 | UdpMsg *reply = new UdpMsg(UdpMsg::SyncReply); 483 | reply->u.sync_reply.random_reply = msg->u.sync_request.random_request; 484 | SendMsg(reply); 485 | return true; 486 | } 487 | 488 | bool 489 | UdpProtocol::OnSyncReply(UdpMsg *msg, int len) 490 | { 491 | if (_current_state != Syncing) { 492 | ::Log(EGGPOLogVerbosity::Info, "Ignoring SyncReply while not synching.\n"); 493 | return msg->hdr.magic == _remote_magic_number; 494 | } 495 | 496 | if (msg->u.sync_reply.random_reply != _state.sync.random) { 497 | ::Log(EGGPOLogVerbosity::Info, "sync reply %d != %d. Keep looking...\n", 498 | msg->u.sync_reply.random_reply, _state.sync.random); 499 | return false; 500 | } 501 | 502 | if (!_connected) { 503 | QueueEvent(Event(Event::Connected)); 504 | _connected = true; 505 | } 506 | 507 | Log("Checking sync state (%d round trips remaining).\n", _state.sync.roundtrips_remaining); 508 | if (--_state.sync.roundtrips_remaining == 0) { 509 | ::Log(EGGPOLogVerbosity::Info, "Synchronized!\n"); 510 | QueueEvent(UdpProtocol::Event(UdpProtocol::Event::Synchronzied)); 511 | _current_state = Running; 512 | _last_received_input.frame = -1; 513 | _remote_magic_number = msg->hdr.magic; 514 | } else { 515 | UdpProtocol::Event evt(UdpProtocol::Event::Synchronizing); 516 | evt.u.synchronizing.total = NUM_SYNC_PACKETS; 517 | evt.u.synchronizing.count = NUM_SYNC_PACKETS - _state.sync.roundtrips_remaining; 518 | QueueEvent(evt); 519 | SendSyncRequest(); 520 | } 521 | return true; 522 | } 523 | 524 | bool 525 | UdpProtocol::OnInput(UdpMsg *msg, int len) 526 | { 527 | /* 528 | * If a disconnect is requested, go ahead and disconnect now. 529 | */ 530 | bool disconnect_requested = msg->u.input.disconnect_requested; 531 | if (disconnect_requested) { 532 | if (_current_state != Disconnected && !_disconnect_event_sent) { 533 | ::Log(EGGPOLogVerbosity::Info, "Disconnecting endpoint on remote request.\n"); 534 | QueueEvent(Event(Event::Disconnected)); 535 | _disconnect_event_sent = true; 536 | } 537 | } else { 538 | /* 539 | * Update the peer connection status if this peer is still considered to be part 540 | * of the network. 541 | */ 542 | UdpMsg::connect_status* remote_status = msg->u.input.peer_connect_status; 543 | for (int i = 0; i < ARRAY_SIZE(_peer_connect_status); i++) { 544 | ASSERT(remote_status[i].last_frame >= _peer_connect_status[i].last_frame); 545 | _peer_connect_status[i].disconnected = _peer_connect_status[i].disconnected || remote_status[i].disconnected; 546 | _peer_connect_status[i].last_frame = MAX(_peer_connect_status[i].last_frame, remote_status[i].last_frame); 547 | } 548 | } 549 | 550 | /* 551 | * Decompress the input. 552 | */ 553 | int last_received_frame_number = _last_received_input.frame; 554 | if (msg->u.input.num_bits) { 555 | int offset = 0; 556 | uint8 *bits = (uint8 *)msg->u.input.bits; 557 | int numBits = msg->u.input.num_bits; 558 | int currentFrame = msg->u.input.start_frame; 559 | 560 | _last_received_input.size = msg->u.input.input_size; 561 | if (_last_received_input.frame < 0) { 562 | _last_received_input.frame = msg->u.input.start_frame - 1; 563 | } 564 | while (offset < numBits) { 565 | /* 566 | * Keep walking through the frames (parsing bits) until we reach 567 | * the inputs for the frame right after the one we're on. 568 | */ 569 | ASSERT(currentFrame <= (_last_received_input.frame + 1)); 570 | bool useInputs = currentFrame == _last_received_input.frame + 1; 571 | 572 | while (BitVector_ReadBit(bits, &offset)) { 573 | int on = BitVector_ReadBit(bits, &offset); 574 | int button = BitVector_ReadNibblet(bits, &offset); 575 | if (useInputs) { 576 | if (on) { 577 | _last_received_input.set(button); 578 | } else { 579 | _last_received_input.clear(button); 580 | } 581 | } 582 | } 583 | ASSERT(offset <= numBits); 584 | 585 | /* 586 | * Now if we want to use these inputs, go ahead and send them to 587 | * the emulator. 588 | */ 589 | if (useInputs) { 590 | /* 591 | * Move forward 1 frame in the stream. 592 | */ 593 | char desc[1024]; 594 | ASSERT(currentFrame == _last_received_input.frame + 1); 595 | _last_received_input.frame = currentFrame; 596 | 597 | /* 598 | * Send the event to the emualtor 599 | */ 600 | UdpProtocol::Event evt(UdpProtocol::Event::Input); 601 | evt.u.input.input = _last_received_input; 602 | 603 | _last_received_input.desc(desc, ARRAY_SIZE(desc)); 604 | 605 | _state.running.last_input_packet_recv_time = Platform::GetCurrentTimeMS(); 606 | 607 | Log("Sending frame %d to emu queue %d (%s).\n", _last_received_input.frame, _queue, desc); 608 | QueueEvent(evt); 609 | 610 | } else { 611 | Log("Skipping past frame:(%d) current is %d.\n", currentFrame, _last_received_input.frame); 612 | } 613 | 614 | /* 615 | * Move forward 1 frame in the input stream. 616 | */ 617 | currentFrame++; 618 | } 619 | } 620 | ASSERT(_last_received_input.frame >= last_received_frame_number); 621 | 622 | /* 623 | * Get rid of our buffered input 624 | */ 625 | while (_pending_output.size() && _pending_output.front().frame < msg->u.input.ack_frame) { 626 | Log("Throwing away pending output frame %d\n", _pending_output.front().frame); 627 | _last_acked_input = _pending_output.front(); 628 | _pending_output.pop(); 629 | } 630 | return true; 631 | } 632 | 633 | 634 | bool 635 | UdpProtocol::OnInputAck(UdpMsg *msg, int len) 636 | { 637 | /* 638 | * Get rid of our buffered input 639 | */ 640 | while (_pending_output.size() && _pending_output.front().frame < msg->u.input_ack.ack_frame) { 641 | Log("Throwing away pending output frame %d\n", _pending_output.front().frame); 642 | _last_acked_input = _pending_output.front(); 643 | _pending_output.pop(); 644 | } 645 | return true; 646 | } 647 | 648 | bool 649 | UdpProtocol::OnQualityReport(UdpMsg *msg, int len) 650 | { 651 | // send a reply so the other side can compute the round trip transmit time. 652 | UdpMsg *reply = new UdpMsg(UdpMsg::QualityReply); 653 | reply->u.quality_reply.pong = msg->u.quality_report.ping; 654 | SendMsg(reply); 655 | 656 | _remote_frame_advantage = msg->u.quality_report.frame_advantage; 657 | return true; 658 | } 659 | 660 | bool 661 | UdpProtocol::OnQualityReply(UdpMsg *msg, int len) 662 | { 663 | _round_trip_time = Platform::GetCurrentTimeMS() - msg->u.quality_reply.pong; 664 | return true; 665 | } 666 | 667 | bool 668 | UdpProtocol::OnKeepAlive(UdpMsg *msg, int len) 669 | { 670 | return true; 671 | } 672 | 673 | void 674 | UdpProtocol::GetNetworkStats(struct FGGPONetworkStats *s) 675 | { 676 | s->network.ping = _round_trip_time; 677 | s->network.send_queue_len = _pending_output.size(); 678 | s->network.kbps_sent = _kbps_sent; 679 | s->timesync.remote_frames_behind = _remote_frame_advantage; 680 | s->timesync.local_frames_behind = _local_frame_advantage; 681 | } 682 | 683 | void 684 | UdpProtocol::SetLocalFrameNumber(int localFrame) 685 | { 686 | /* 687 | * Estimate which frame the other guy is one by looking at the 688 | * last frame they gave us plus some delta for the one-way packet 689 | * trip time. 690 | */ 691 | int remoteFrame = _last_received_input.frame + (_round_trip_time * 60 / 1000); 692 | 693 | /* 694 | * Our frame advantage is how many frames *behind* the other guy 695 | * we are. Counter-intuative, I know. It's an advantage because 696 | * it means they'll have to predict more often and our moves will 697 | * pop more frequenetly. 698 | */ 699 | _local_frame_advantage = remoteFrame - localFrame; 700 | } 701 | 702 | int 703 | UdpProtocol::RecommendFrameDelay() 704 | { 705 | // XXX: require idle input should be a configuration parameter 706 | return _timesync.recommend_frame_wait_duration(false); 707 | } 708 | 709 | 710 | void 711 | UdpProtocol::SetDisconnectTimeout(int timeout) 712 | { 713 | _disconnect_timeout = timeout; 714 | } 715 | 716 | void 717 | UdpProtocol::SetDisconnectNotifyStart(int timeout) 718 | { 719 | _disconnect_notify_start = timeout; 720 | } 721 | 722 | void 723 | UdpProtocol::PumpSendQueue() 724 | { 725 | while (!_send_queue.empty()) { 726 | QueueEntry &entry = _send_queue.front(); 727 | 728 | if (_send_latency) { 729 | // should really come up with a gaussian distributation based on the configured 730 | // value, but this will do for now. 731 | int jitter = (_send_latency * 2 / 3) + ((rand() % _send_latency) / 3); 732 | if (Platform::GetCurrentTimeMS() < _send_queue.front().queue_time + jitter) { 733 | break; 734 | } 735 | } 736 | if (_oop_percent && !_oo_packet.msg && ((rand() % 100) < _oop_percent)) { 737 | int delay = rand() % (_send_latency * 10 + 1000); 738 | Log("creating rogue oop (seq: %d delay: %d)\n", entry.msg->hdr.sequence_number, delay); 739 | _oo_packet.send_time = Platform::GetCurrentTimeMS() + delay; 740 | _oo_packet.msg = entry.msg; 741 | _oo_packet.dest_addr = entry.dest_addr; 742 | } else { 743 | ASSERT(entry.dest_addr.sin_addr.s_addr); 744 | 745 | _udp->SendTo((char *)entry.msg, entry.msg->PacketSize(), 0, 746 | (struct sockaddr *)&entry.dest_addr, sizeof entry.dest_addr); 747 | 748 | delete entry.msg; 749 | } 750 | _send_queue.pop(); 751 | } 752 | if (_oo_packet.msg && _oo_packet.send_time < Platform::GetCurrentTimeMS()) { 753 | Log("sending rogue oop!"); 754 | _udp->SendTo((char *)_oo_packet.msg, _oo_packet.msg->PacketSize(), 0, 755 | (struct sockaddr *)&_oo_packet.dest_addr, sizeof _oo_packet.dest_addr); 756 | 757 | delete _oo_packet.msg; 758 | _oo_packet.msg = NULL; 759 | } 760 | } 761 | 762 | void 763 | UdpProtocol::ClearSendQueue() 764 | { 765 | while (!_send_queue.empty()) { 766 | delete _send_queue.front().msg; 767 | _send_queue.pop(); 768 | } 769 | } 770 | --------------------------------------------------------------------------------