dep: Add ggpo-x

This commit is contained in:
Stenzek 2023-08-30 17:31:43 +10:00
parent fcd82df9d6
commit 5f99fda9d7
No known key found for this signature in database
37 changed files with 4845 additions and 0 deletions

View File

@ -35,6 +35,7 @@ disable_compiler_warnings_for_target(rcheevos)
if(NOT ANDROID)
add_subdirectory(enet)
add_subdirectory(ggpo-x)
endif()
if(ENABLE_CUBEB)

41
dep/ggpo-x/CMakeLists.txt Normal file
View File

@ -0,0 +1,41 @@
add_library(ggpo-x
include/ggponet.h
src/backends/backend.h
src/backends/p2p.cpp
src/backends/p2p.h
src/backends/spectator.cpp
src/backends/spectator.h
src/backends/synctest.h
src/backends/synctest.cpp
src/bitvector.h
src/bitvector.cpp
src/game_input.h
src/game_input.cpp
src/input_queue.h
src/input_queue.cpp
src/log.cpp
src/log.h
src/main.cpp
src/network/udp_msg.h
src/network/udp_proto.cpp
src/network/udp_proto.h
src/platform.cpp
src/platform.h
src/ring_buffer.h
src/static_buffer.h
src/sync.cpp
src/sync.h
src/timesync.cpp
src/timesync.h
src/types.h
)
target_include_directories(ggpo-x PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/include"
"${CMAKE_CURRENT_SOURCE_DIR}/src"
)
target_include_directories(ggpo-x INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
target_link_libraries(ggpo-x PRIVATE enet)

21
dep/ggpo-x/LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2009-2019 GroundStorm Studios, LLC. (http://ggpo.net)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

55
dep/ggpo-x/ggpo-x.vcxproj Normal file
View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\msvc\vsprops\Configurations.props" />
<PropertyGroup Label="Globals">
<ProjectGuid>{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}</ProjectGuid>
</PropertyGroup>
<ItemGroup>
<ClInclude Include="include\ggponet.h" />
<ClInclude Include="src\backends\backend.h" />
<ClInclude Include="src\backends\p2p.h" />
<ClInclude Include="src\backends\spectator.h" />
<ClInclude Include="src\backends\synctest.h" />
<ClInclude Include="src\bitvector.h" />
<ClInclude Include="src\game_input.h" />
<ClInclude Include="src\input_queue.h" />
<ClInclude Include="src\log.h" />
<ClInclude Include="src\network\udp_msg.h" />
<ClInclude Include="src\network\udp_proto.h" />
<ClInclude Include="src\platform.h" />
<ClInclude Include="src\ring_buffer.h" />
<ClInclude Include="src\static_buffer.h" />
<ClInclude Include="src\sync.h" />
<ClInclude Include="src\timesync.h" />
<ClInclude Include="src\types.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\backends\p2p.cpp" />
<ClCompile Include="src\backends\spectator.cpp" />
<ClCompile Include="src\backends\synctest.cpp" />
<ClCompile Include="src\bitvector.cpp" />
<ClCompile Include="src\game_input.cpp" />
<ClCompile Include="src\input_queue.cpp" />
<ClCompile Include="src\log.cpp" />
<ClCompile Include="src\main.cpp" />
<ClCompile Include="src\network\udp_proto.cpp" />
<ClCompile Include="src\platform.cpp" />
<ClCompile Include="src\sync.cpp" />
<ClCompile Include="src\timesync.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\enet\enet.vcxproj">
<Project>{460a096b-fcc7-465c-8e4b-434af490a9ea}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="..\msvc\vsprops\StaticLibrary.props" />
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>TurnOffAllWarnings</WarningLevel>
<PreprocessorDefinitions>_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(ProjectDir)src;$(ProjectDir)include</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)dep\enet\include</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<Import Project="..\msvc\vsprops\Targets.props" />
</Project>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClInclude Include="include\ggponet.h" />
<ClInclude Include="src\bitvector.h" />
<ClInclude Include="src\game_input.h" />
<ClInclude Include="src\input_queue.h" />
<ClInclude Include="src\log.h" />
<ClInclude Include="src\platform.h" />
<ClInclude Include="src\ring_buffer.h" />
<ClInclude Include="src\static_buffer.h" />
<ClInclude Include="src\sync.h" />
<ClInclude Include="src\timesync.h" />
<ClInclude Include="src\types.h" />
<ClInclude Include="src\backends\spectator.h">
<Filter>backends</Filter>
</ClInclude>
<ClInclude Include="src\backends\synctest.h">
<Filter>backends</Filter>
</ClInclude>
<ClInclude Include="src\backends\backend.h">
<Filter>backends</Filter>
</ClInclude>
<ClInclude Include="src\backends\p2p.h">
<Filter>backends</Filter>
</ClInclude>
<ClInclude Include="src\network\udp_msg.h">
<Filter>network</Filter>
</ClInclude>
<ClInclude Include="src\network\udp_proto.h">
<Filter>network</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\bitvector.cpp" />
<ClCompile Include="src\game_input.cpp" />
<ClCompile Include="src\input_queue.cpp" />
<ClCompile Include="src\log.cpp" />
<ClCompile Include="src\main.cpp" />
<ClCompile Include="src\platform.cpp" />
<ClCompile Include="src\sync.cpp" />
<ClCompile Include="src\timesync.cpp" />
<ClCompile Include="src\backends\spectator.cpp">
<Filter>backends</Filter>
</ClCompile>
<ClCompile Include="src\backends\synctest.cpp">
<Filter>backends</Filter>
</ClCompile>
<ClCompile Include="src\backends\p2p.cpp">
<Filter>backends</Filter>
</ClCompile>
<ClCompile Include="src\network\udp_proto.cpp">
<Filter>network</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<Filter Include="backends">
<UniqueIdentifier>{9734509d-391e-44d7-bbce-e3bc73dfcea3}</UniqueIdentifier>
</Filter>
<Filter Include="network">
<UniqueIdentifier>{1618c8a6-e50c-419c-8476-cb1c09fd8dce}</UniqueIdentifier>
</Filter>
</ItemGroup>
</Project>

View File

@ -0,0 +1,583 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _GGPONET_H_
#define _GGPONET_H_
#include "enet/enet.h"
#ifdef __cplusplus
extern "C" {
#endif
#include <stdarg.h>
#include <cstdint>
// On windows, export at build time and import at runtime.
// ELF systems don't need an explicit export/import.
#ifdef _WIN32
# if defined(GGPO_SHARED_LIB)
# ifdef GGPO_SDK_EXPORT
# define GGPO_API __declspec(dllexport)
# else
# define GGPO_API __declspec(dllimport)
# endif
# else
# define GGPO_API
# endif
#else
# define GGPO_API
#endif
#define GGPO_MAX_PLAYERS 4
//#define GGPO_MAX_PREDICTION_FRAMES 8
#define GGPO_MAX_SPECTATORS 32
#define GGPO_SPECTATOR_INPUT_INTERVAL 4
typedef class GGPOSession GGPOSession;
typedef int GGPOPlayerHandle;
typedef enum {
GGPO_PLAYERTYPE_LOCAL,
GGPO_PLAYERTYPE_REMOTE,
GGPO_PLAYERTYPE_SPECTATOR,
} GGPOPlayerType;
/*
* The GGPOPlayer structure used to describe players in ggpo_add_player
*
* size: Should be set to the sizeof(GGPOPlayer)
*
* type: One of the GGPOPlayerType values describing how inputs should be handled
* Local players must have their inputs updated every frame via
* ggpo_add_local_inputs. Remote players values will come over the
* network.
*
* player_num: The player number. Should be between 1 and the number of players
* In the game (e.g. in a 2 player game, either 1 or 2).
*
* If type == GGPO_PLAYERTYPE_REMOTE:
*
* u.remote.ip_address: The ip address of the ggpo session which will host this
* player.
*
* u.remote.port: The port where udp packets should be sent to reach this player.
* All the local inputs for this session will be sent to this player at
* ip_address:port.
*
*/
typedef struct GGPOPlayer {
int size;
GGPOPlayerType type;
int player_num;
union {
struct {
} local;
struct {
ENetPeer* peer;
} remote;
} u;
} GGPOPlayer;
typedef struct GGPOLocalEndpoint {
int player_num;
} GGPOLocalEndpoint;
#define GGPO_ERRORLIST \
GGPO_ERRORLIST_ENTRY(GGPO_OK, 0) \
GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_SUCCESS, 0) \
GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_GENERAL_FAILURE, -1) \
GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_INVALID_SESSION, 1) \
GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_INVALID_PLAYER_HANDLE, 2) \
GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_PLAYER_OUT_OF_RANGE, 3) \
GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_PREDICTION_THRESHOLD, 4) \
GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_UNSUPPORTED, 5) \
GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_NOT_SYNCHRONIZED, 6) \
GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_IN_ROLLBACK, 7) \
GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_INPUT_DROPPED, 8) \
GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_PLAYER_DISCONNECTED, 9) \
GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_TOO_MANY_SPECTATORS, 10) \
GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_INVALID_REQUEST, 11) \
GGPO_ERRORLIST_ENTRY(GGPO_CHAT_MESSAGE_TOO_LONG, 12)
#define GGPO_ERRORLIST_ENTRY(name, value) name = value,
typedef enum {
GGPO_ERRORLIST
} GGPOErrorCode;
#undef GGPO_ERRORLIST_ENTRY
#define GGPO_SUCCEEDED(result) ((result) == GGPO_ERRORCODE_SUCCESS)
#define GGPO_INVALID_HANDLE (-1)
/*
* The GGPOEventCode enumeration describes what type of event just happened.
*
* GGPO_EVENTCODE_CONNECTED_TO_PEER - Handshake with the game running on the
* other side of the network has been completed.
*
* GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER - Beginning the synchronization
* process with the client on the other end of the networking. The count
* and total fields in the u.synchronizing struct of the GGPOEvent
* object indicate progress.
*
* GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER - The synchronziation with this
* peer has finished.
*
* GGPO_EVENTCODE_RUNNING - All the clients have synchronized. You may begin
* sending inputs with ggpo_synchronize_inputs.
*
* GGPO_EVENTCODE_DISCONNECTED_FROM_PEER - The network connection on
* the other end of the network has closed.
*
* GGPO_EVENTCODE_TIMESYNC - The time synchronziation code has determined
* that this client is too far ahead of the other one and should slow
* down to ensure fairness. The u.timesync.frames_ahead parameter in
* the GGPOEvent object indicates how many frames the client is.
*
*/
typedef enum {
GGPO_EVENTCODE_CONNECTED_TO_PEER = 1000,
GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER = 1001,
GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER = 1002,
GGPO_EVENTCODE_RUNNING = 1003,
GGPO_EVENTCODE_TIMESYNC = 1004,
GGPO_EVENTCODE_DESYNC = 1006
} GGPOEventCode;
/*
* The GGPOEvent structure contains an asynchronous event notification sent
* by the on_event callback. See GGPOEventCode, above, for a detailed
* explanation of each event.
*/
typedef struct {
GGPOEventCode code;
union {
struct {
GGPOPlayerHandle player;
} connected;
struct {
GGPOPlayerHandle player;
int count;
int total;
} synchronizing;
struct {
GGPOPlayerHandle player;
} synchronized;
struct {
GGPOPlayerHandle player;
} disconnected;
struct {
float frames_ahead;
int timeSyncPeriodInFrames;
} timesync;
struct {
GGPOPlayerHandle player;
int disconnect_timeout;
} connection_interrupted;
struct {
GGPOPlayerHandle player;
} connection_resumed;
struct {
int nFrameOfDesync;
uint32_t ourCheckSum;
uint32_t remoteChecksum;
} desync;
} u;
} GGPOEvent;
/*
* The GGPOSessionCallbacks structure contains the callback functions that
* your application must implement. GGPO.net will periodically call these
* functions during the game. All callback functions must be implemented.
*/
typedef struct {
/*
* save_game_state - The client should allocate a buffer, copy the
* entire contents of the current game state into it, and copy the
* length into the *len parameter. Optionally, the client can compute
* a checksum of the data and store it in the *checksum argument.
*/
bool (__cdecl *save_game_state)(void* context, unsigned char **buffer, int *len, int *checksum, int frame);
/*
* load_game_state - GGPO.net will call this function at the beginning
* of a rollback. The buffer and len parameters contain a previously
* saved state returned from the save_game_state function. The client
* should make the current game state match the state contained in the
* buffer.
*/
bool (__cdecl *load_game_state)(void* context, unsigned char* buffer, int len, int framesToRollback, int frameToLoad);
/*
* copy_game_state - creates a copy of an existing save state.
*/
bool(__cdecl* copy_game_state)(void* context, unsigned char** out_buffer, int* out_len, int* out_checksum,
const unsigned char* in_buffer, const int in_len, const int in_checksum);
/*
* log_game_state - Used in diagnostic testing. The client should use
* the ggpo_log function to write the contents of the specified save
* state in a human readible form.
*/
bool (__cdecl *log_game_state)(void* context, char *filename, unsigned char *buffer, int len);
/*
* free_buffer - Frees a game state allocated in save_game_state. You
* should deallocate the memory contained in the buffer.
*/
void (__cdecl *free_buffer)(void* context, void *buffer, int frame);
/*
* advance_frame - Called during a rollback. You should advance your game
* state by exactly one frame. Before each frame, call ggpo_synchronize_input
* to retrieve the inputs you should use for that frame. After each frame,
* you should call ggpo_advance_frame to notify GGPO.net that you're
* finished.
*
* The flags parameter is reserved. It can safely be ignored at this time.
*/
bool (__cdecl *advance_frame)(void* context, int flags);
/*
* on_event - Notification that something has happened. See the GGPOEventCode
* structure above for more information.
*/
bool (__cdecl *on_event)(void* context, GGPOEvent *info);
/*
* Calling context
*/
void* context;
} GGPOSessionCallbacks;
/*
* The GGPONetworkStats function contains some statistics about the current
* session.
*
* network.send_queue_len - The length of the queue containing UDP packets
* which have not yet been acknowledged by the end client. The length of
* the send queue is a rough indication of the quality of the connection.
* The longer the send queue, the higher the round-trip time between the
* clients. The send queue will also be longer than usual during high
* packet loss situations.
*
* network.recv_queue_len - The number of inputs currently buffered by the
* GGPO.net network layer which have yet to be validated. The length of
* the prediction queue is roughly equal to the current frame number
* minus the frame number of the last packet in the remote queue.
*
* network.ping - The roundtrip packet transmission time as calcuated
* by GGPO.net. This will be roughly equal to the actual round trip
* packet transmission time + 2 the interval at which you call ggpo_idle
* or ggpo_advance_frame.
*
* network.kbps_sent - The estimated bandwidth used between the two
* clients, in kilobits per second.
*
* timesync.local_frames_behind - The number of frames GGPO.net calculates
* that the local client is behind the remote client at this instant in
* time. For example, if at this instant the current game client is running
* frame 1002 and the remote game client is running frame 1009, this value
* will mostly likely roughly equal 7.
*
* timesync.remote_frames_behind - The same as local_frames_behind, but
* calculated from the perspective of the remote player.
*
*/
typedef struct GGPONetworkStats {
struct {
int send_queue_len;
int recv_queue_len;
int ping;
int kbps_sent;
} network;
struct {
float local_frames_behind;
float remote_frames_behind;
float avg_local_frames_behind;
float avg_remote_frames_behind;
} timesync;
} GGPONetworkStats;
/*
* ggpo_start_session --
*
* Used to being a new GGPO.net session. The ggpo object returned by ggpo_start_session
* uniquely identifies the state for this session and should be passed to all other
* functions.
*
* session - An out parameter to the new ggpo session object.
*
* cb - A GGPOSessionCallbacks structure which contains the callbacks you implement
* to help GGPO.net synchronize the two games. You must implement all functions in
* cb, even if they do nothing but 'return true';
*
* game - The name of the game. This is used internally for GGPO for logging purposes only.
*
* num_players - The number of players which will be in this game. The number of players
* per session is fixed. If you need to change the number of players or any player
* disconnects, you must start a new session.
*
* input_size - The size of the game inputs which will be passsed to ggpo_add_local_input.
*
* local_port - The port GGPO should bind to for UDP traffic.
*/
GGPO_API GGPOErrorCode __cdecl ggpo_start_session(GGPOSession **session,
GGPOSessionCallbacks *cb,
int num_players,
int input_size,
int maxPrediction);
/*
* ggpo_add_player --
*
* Must be called for each player in the session (e.g. in a 3 player session, must
* be called 3 times).
*
* player - A GGPOPlayer struct used to describe the player.
*
* handle - An out parameter to a handle used to identify this player in the future.
* (e.g. in the on_event callbacks).
*/
GGPO_API GGPOErrorCode __cdecl ggpo_add_player(GGPOSession *session,
GGPOPlayer *player,
GGPOPlayerHandle *handle);
/*
* ggpo_start_synctest --
*
* Used to being a new GGPO.net sync test session. During a sync test, every
* frame of execution is run twice: once in prediction mode and once again to
* verify the result of the prediction. If the checksums of your save states
* do not match, the test is aborted.
*
* cb - A GGPOSessionCallbacks structure which contains the callbacks you implement
* to help GGPO.net synchronize the two games. You must implement all functions in
* cb, even if they do nothing but 'return true';
*
* game - The name of the game. This is used internally for GGPO for logging purposes only.
*
* num_players - The number of players which will be in this game. The number of players
* per session is fixed. If you need to change the number of players or any player
* disconnects, you must start a new session.
*
* input_size - The size of the game inputs which will be passsed to ggpo_add_local_input.
*
* frames - The number of frames to run before verifying the prediction. The
* recommended value is 1.
*
*/
GGPO_API GGPOErrorCode __cdecl ggpo_start_synctest(GGPOSession **session,
GGPOSessionCallbacks *cb,
int num_players,
int input_size,
int frames);
/*
* ggpo_start_spectating --
*
* Start a spectator session.
*
* cb - A GGPOSessionCallbacks structure which contains the callbacks you implement
* to help GGPO.net synchronize the two games. You must implement all functions in
* cb, even if they do nothing but 'return true';
*
* game - The name of the game. This is used internally for GGPO for logging purposes only.
*
* num_players - The number of players which will be in this game. The number of players
* per session is fixed. If you need to change the number of players or any player
* disconnects, you must start a new session.
*
* input_size - The size of the game inputs which will be passsed to ggpo_add_local_input.
*
* local_port - The port GGPO should bind to for UDP traffic.
*
* host_ip - The IP address of the host who will serve you the inputs for the game. Any
* player partcipating in the session can serve as a host.
*
* host_port - The port of the session on the host
*/
GGPO_API GGPOErrorCode __cdecl ggpo_start_spectating(GGPOSession **session,
GGPOSessionCallbacks *cb,
int num_players,
int input_size,
ENetPeer* host);
/*
* ggpo_close_session --
* Used to close a session. You must call ggpo_close_session to
* free the resources allocated in ggpo_start_session.
*/
GGPO_API GGPOErrorCode __cdecl ggpo_close_session(GGPOSession *);
/*
* ggpo_set_frame_delay --
*
* Change the amount of frames ggpo will delay local input. Must be called
* before the first call to ggpo_synchronize_input.
*/
GGPO_API GGPOErrorCode __cdecl ggpo_set_frame_delay(GGPOSession *,
GGPOPlayerHandle player,
int frame_delay);
/*
* ggpo_idle --
* Should be called periodically by your application to give GGPO.net
* a chance to do some work. Most packet transmissions and rollbacks occur
* in ggpo_idle.
*/
GGPO_API GGPOErrorCode __cdecl ggpo_idle(GGPOSession *);
GGPO_API GGPOErrorCode __cdecl ggpo_network_idle(GGPOSession *);
/*
* ggpo_add_local_input --
*
* Used to notify GGPO.net of inputs that should be trasmitted to remote
* players. ggpo_add_local_input must be called once every frame for
* all player of type GGPO_PLAYERTYPE_LOCAL.
*
* player - The player handle returned for this player when you called
* ggpo_add_local_player.
*
* values - The controller inputs for this player.
*
* size - The size of the controller inputs. This must be exactly equal to the
* size passed into ggpo_start_session.
*/
GGPO_API GGPOErrorCode __cdecl ggpo_add_local_input(GGPOSession *,
GGPOPlayerHandle player,
void *values,
int size);
/*
* ggpo_synchronize_input --
*
* You should call ggpo_synchronize_input before every frame of execution,
* including those frames which happen during rollback.
*
* values - When the function returns, the values parameter will contain
* inputs for this frame for all players. The values array must be at
* least (size * players) large.
*
* size - The size of the values array.
*
* disconnect_flags - Indicated whether the input in slot (1 << flag) is
* valid. If a player has disconnected, the input in the values array for
* that player will be zeroed and the i-th flag will be set. For example,
* if only player 3 has disconnected, disconnect flags will be 8 (i.e. 1 << 3).
*/
GGPO_API GGPOErrorCode __cdecl ggpo_synchronize_input(GGPOSession *,
void *values,
int size,
int *disconnect_flags);
/*
* ggpo_disconnect_player --
*
* Disconnects a remote player from a game. Will return GGPO_ERRORCODE_PLAYER_DISCONNECTED
* if you try to disconnect a player who has already been disconnected.
*/
GGPO_API GGPOErrorCode __cdecl ggpo_disconnect_player(GGPOSession *,
GGPOPlayerHandle player);
/*
* ggpo_advance_frame --
*
* You should call ggpo_advance_frame to notify GGPO.net that you have
* advanced your gamestate by a single frame. You should call this everytime
* you advance the gamestate by a frame, even during rollbacks. GGPO.net
* may call your save_state callback before this function returns.
*/
GGPO_API GGPOErrorCode __cdecl ggpo_advance_frame(GGPOSession *, uint16_t checksum);
/*
* ggpo_get_current_frame --
*
* current frame GGPO is dealing with
*/
GGPO_API GGPOErrorCode __cdecl ggpo_get_current_frame(GGPOSession* ggpo, int& nFrame);
/*
* ggpo_get_network_stats --
*
* Used to fetch some statistics about the quality of the network connection.
*
* player - The player handle returned from the ggpo_add_player function you used
* to add the remote player.
*
* stats - Out parameter to the network statistics.
*/
GGPO_API GGPOErrorCode __cdecl ggpo_get_network_stats(GGPOSession *,
GGPOPlayerHandle player,
GGPONetworkStats *stats);
/*
* ggpo_set_disconnect_timeout --
*
* Sets the disconnect timeout. The session will automatically disconnect
* from a remote peer if it has not received a packet in the timeout window.
* You will be notified of the disconnect via a GGPO_EVENTCODE_DISCONNECTED_FROM_PEER
* event.
*
* Setting a timeout value of 0 will disable automatic disconnects.
*
* timeout - The time in milliseconds to wait before disconnecting a peer.
*/
GGPO_API GGPOErrorCode __cdecl ggpo_set_disconnect_timeout(GGPOSession *,
int timeout);
/*
* ggpo_set_disconnect_notify_start --
*
* The time to wait before the first GGPO_EVENTCODE_NETWORK_INTERRUPTED timeout
* will be sent.
*
* timeout - The amount of time which needs to elapse without receiving a packet
* before the GGPO_EVENTCODE_NETWORK_INTERRUPTED event is sent.
*/
GGPO_API GGPOErrorCode __cdecl ggpo_set_disconnect_notify_start(GGPOSession *,
int timeout);
/*
* ENet packet processing
*/
GGPO_API GGPOErrorCode __cdecl ggpo_handle_packet(GGPOSession*,
ENetPeer* peer, const ENetPacket* pkt);
/*
* ggpo_log --
*
* Used to write to the ggpo.net log. In the current versions of the
* SDK, a log file is only generated if the "quark.log" environment
* variable is set to 1. This will change in future versions of the
* SDK.
*/
GGPO_API void __cdecl ggpo_log(GGPOSession *,
const char *fmt, ...);
/*
* ggpo_logv --
*
* A varargs compatible version of ggpo_log. See ggpo_log for
* more details.
*/
GGPO_API void __cdecl ggpo_logv(GGPOSession *,
const char *fmt,
va_list args);
#ifdef __cplusplus
};
#endif
#endif

View File

@ -0,0 +1,36 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _BACKEND_H
#define _BACKEND_H
#include "ggponet.h"
#include "types.h"
class GGPOSession {
public:
virtual ~GGPOSession() { }
virtual GGPOErrorCode DoPoll() = 0;
virtual GGPOErrorCode NetworkIdle() = 0;
virtual GGPOErrorCode AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle) = 0;
virtual GGPOErrorCode AddLocalInput(GGPOPlayerHandle player, void *values, int size) = 0;
virtual GGPOErrorCode SyncInput(void *values, int size, int *disconnect_flags) = 0;
virtual GGPOErrorCode IncrementFrame(uint16_t checksum) = 0;
virtual GGPOErrorCode CurrentFrame(int& current) =0;
virtual GGPOErrorCode DisconnectPlayer(GGPOPlayerHandle handle) = 0;// { return GGPO_OK; }
virtual GGPOErrorCode GetNetworkStats(GGPONetworkStats *stats, GGPOPlayerHandle handle) { return GGPO_OK; }
virtual GGPOErrorCode Logv(const char *fmt, va_list list) { ::Logv(fmt, list); return GGPO_OK; }
virtual GGPOErrorCode SetFrameDelay(GGPOPlayerHandle player, int delay) { return GGPO_ERRORCODE_UNSUPPORTED; }
virtual GGPOErrorCode SetDisconnectTimeout(int timeout) { return GGPO_ERRORCODE_UNSUPPORTED; }
virtual GGPOErrorCode SetDisconnectNotifyStart(int timeout) { return GGPO_ERRORCODE_UNSUPPORTED; }
virtual GGPOErrorCode OnPacket(ENetPeer* peer, const ENetPacket* pkt) = 0;
};
#endif

View File

@ -0,0 +1,728 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#include "p2p.h"
#include "log.h"
#include <cstdio>
#include <cstdlib>
#include <climits>
static const int RECOMMENDATION_INTERVAL = 120;
Peer2PeerBackend::Peer2PeerBackend(GGPOSessionCallbacks *cb,
int num_players,
int input_size, int nframes) :
_num_players(num_players),
_input_size(input_size),
_sync(_local_connect_status, nframes),
_num_spectators(0),
_next_spectator_frame(0)
{
_callbacks = *cb;
_synchronizing = true;
_next_recommended_sleep = 0;
/*
* Initialize the synchronziation layer
*/
Sync::Config config = { 0 };
config.num_players = num_players;
config.input_size = input_size;
config.callbacks = _callbacks;
config.num_prediction_frames = nframes;
_sync.Init(config);
_endpoints.resize(_num_players);
memset(_local_connect_status, 0, sizeof(_local_connect_status));
for (int i = 0; i < ARRAY_SIZE(_local_connect_status); i++) {
_local_connect_status[i].last_frame = -1;
}
}
Peer2PeerBackend::~Peer2PeerBackend()
{
}
void
Peer2PeerBackend::AddRemotePlayer(ENetPeer* peer, int queue)
{
/*
* Start the state machine (xxx: no)
*/
_synchronizing = true;
_endpoints[queue].Init(peer, queue, _local_connect_status);
_endpoints[queue].Synchronize();
}
GGPOErrorCode Peer2PeerBackend::AddSpectator(ENetPeer* peer)
{
if (_num_spectators == GGPO_MAX_SPECTATORS) {
return GGPO_ERRORCODE_TOO_MANY_SPECTATORS;
}
_synchronizing = true;
int queue = _num_spectators++;
_spectators[queue].Init(peer, queue + 1000, _local_connect_status);
_spectators[queue].Synchronize();
return GGPO_OK;
}
void Peer2PeerBackend::CheckDesync()
{
std::vector<int> keysToRemove;
for (auto& ep : _endpoints)
{
for (const auto& pair : ep._remoteCheckSums)
{
auto checkSumFrame = pair.first;
auto remoteChecksum = pair.second;
if (_confirmedCheckSums.count(checkSumFrame))
{
keysToRemove.push_back(checkSumFrame);
auto localChecksum = _confirmedCheckSums.at(checkSumFrame);
if (remoteChecksum != localChecksum)
{
GGPOEvent info;
info.code = GGPO_EVENTCODE_DESYNC;
info.u.desync.nFrameOfDesync = checkSumFrame;
info.u.desync.ourCheckSum = localChecksum;
info.u.desync.remoteChecksum = remoteChecksum;
_callbacks.on_event(_callbacks.context, &info);
Log("DESYNC Checksum frame %d, local: %d, remote %d, size of checksum maps: %d,%d", checkSumFrame, localChecksum, remoteChecksum, (int)_confirmedCheckSums.size(), (int)ep._remoteCheckSums.size());
}
if (checkSumFrame % 100 == 0)
{
Log("Checksum frame %d, local: %d, remote %d, size of checksum maps: %d,%d\n", checkSumFrame, localChecksum, remoteChecksum, (int)_confirmedCheckSums.size(), (int)ep._remoteCheckSums.size());
}
}
}
for (auto k : keysToRemove)
{
ep._remoteCheckSums.erase(k);
}
}
for (auto k : keysToRemove)
{
Log("Erase checksums for frame %d\n",k);
for (auto itr = _confirmedCheckSums.cbegin(); itr != _confirmedCheckSums.cend(); )
itr = (itr->first <=k) ? _confirmedCheckSums.erase(itr) : std::next(itr);
// ep._remoteCheckSums.erase(k);
}
}
GGPOErrorCode
Peer2PeerBackend::DoPoll()
{
if (!_sync.InRollback())
{
PollUdpProtocolEvents();
CheckDesync();
if (!_synchronizing) {
_sync.CheckSimulation();
// notify all of our endpoints of their local frame number for their
// next connection quality report
int current_frame = _sync.GetFrameCount();
for (int i = 0; i < _num_players; i++) {
_endpoints[i].SetLocalFrameNumber(current_frame);
}
int total_min_confirmed;
if (_num_players <= 2) {
total_min_confirmed = Poll2Players(current_frame);
} else {
total_min_confirmed = PollNPlayers(current_frame);
}
Log("last confirmed frame in p2p backend is %d.\n", total_min_confirmed);
if (total_min_confirmed >= 0) {
ASSERT(total_min_confirmed != INT_MAX);
if (_num_spectators > 0) {
while (_next_spectator_frame <= total_min_confirmed) {
Log("pushing frame %d to spectators.\n", _next_spectator_frame);
GameInput input;
input.frame = _next_spectator_frame;
input.size = _input_size * _num_players;
_sync.GetConfirmedInputs(input.bits, _input_size * _num_players, _next_spectator_frame);
for (int i = 0; i < _num_spectators; i++) {
_spectators[i].SendInput(input);
}
_next_spectator_frame++;
}
}
Log("setting confirmed frame in sync to %d.\n", total_min_confirmed);
_sync.SetLastConfirmedFrame(total_min_confirmed);
}
// send timesync notifications if now is the proper time
if (current_frame > _next_recommended_sleep) {
float interval = 0;
for (int i = 0; i < _num_players; i++) {
interval = BIGGEST(interval, _endpoints[i].RecommendFrameDelay());
}
//if (interval > 0)
{
GGPOEvent info;
info.code = GGPO_EVENTCODE_TIMESYNC;
info.u.timesync.frames_ahead = interval;
info.u.timesync.timeSyncPeriodInFrames = RECOMMENDATION_INTERVAL;
_callbacks.on_event(_callbacks.context, &info);
_next_recommended_sleep = current_frame + RECOMMENDATION_INTERVAL;// RECOMMENDATION_INTERVAL;// RECOMMENDATION_INTERVAL;
}
}
}
}
return GGPO_OK;
}
GGPOErrorCode Peer2PeerBackend::NetworkIdle()
{
for (UdpProtocol& udp : _endpoints)
udp.NetworkIdle();
for (UdpProtocol& udp : _spectators)
udp.NetworkIdle();
return GGPO_OK;
}
int Peer2PeerBackend::Poll2Players(int current_frame)
{
int i;
// discard confirmed frames as appropriate
int total_min_confirmed = MAX_INT;
for (i = 0; i < _num_players; i++) {
bool queue_connected = true;
if (_endpoints[i].IsRunning()) {
int ignore;
queue_connected = _endpoints[i].GetPeerConnectStatus(i, &ignore);
}
if (!_local_connect_status[i].disconnected) {
total_min_confirmed = MIN(_local_connect_status[i].last_frame, total_min_confirmed);
}
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);
if (!queue_connected && !_local_connect_status[i].disconnected) {
Log("disconnecting i %d by remote request.\n", i);
DisconnectPlayerQueue(i, total_min_confirmed);
}
Log(" total_min_confirmed = %d.\n", total_min_confirmed);
}
return total_min_confirmed;
}
int Peer2PeerBackend::PollNPlayers(int current_frame)
{
int i, queue, last_received;
// discard confirmed frames as appropriate
int total_min_confirmed = MAX_INT;
for (queue = 0; queue < _num_players; queue++) {
bool queue_connected = true;
int queue_min_confirmed = MAX_INT;
Log("considering queue %d.\n", queue);
for (i = 0; i < _num_players; i++) {
// we're going to do a lot of logic here in consideration of endpoint i.
// keep accumulating the minimum confirmed point for all n*n packets and
// throw away the rest.
if (_endpoints[i].IsRunning()) {
bool connected = _endpoints[i].GetPeerConnectStatus(queue, &last_received);
queue_connected = queue_connected && connected;
queue_min_confirmed = MIN(last_received, queue_min_confirmed);
Log(" endpoint %d: connected = %d, last_received = %d, queue_min_confirmed = %d.\n", i, connected, last_received, queue_min_confirmed);
} else {
Log(" endpoint %d: ignoring... not running.\n", i);
}
}
// merge in our local status only if we're still connected!
if (!_local_connect_status[queue].disconnected) {
queue_min_confirmed = MIN(_local_connect_status[queue].last_frame, queue_min_confirmed);
}
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);
if (queue_connected) {
total_min_confirmed = MIN(queue_min_confirmed, total_min_confirmed);
} else {
// check to see if this disconnect notification is further back than we've been before. If
// so, we need to re-adjust. This can happen when we detect our own disconnect at frame n
// and later receive a disconnect notification for frame n-1.
if (!_local_connect_status[queue].disconnected || _local_connect_status[queue].last_frame > queue_min_confirmed) {
Log("disconnecting queue %d by remote request.\n", queue);
DisconnectPlayerQueue(queue, queue_min_confirmed);
}
}
Log(" total_min_confirmed = %d.\n", total_min_confirmed);
}
return total_min_confirmed;
}
GGPOErrorCode
Peer2PeerBackend::AddPlayer(GGPOPlayer *player,
GGPOPlayerHandle *handle)
{
if (player->type == GGPO_PLAYERTYPE_SPECTATOR) {
return AddSpectator(player->u.remote.peer);
}
int queue = player->player_num - 1;
if (player->player_num < 1 || player->player_num > _num_players) {
return GGPO_ERRORCODE_PLAYER_OUT_OF_RANGE;
}
*handle = QueueToPlayerHandle(queue);
if (player->type == GGPO_PLAYERTYPE_REMOTE) {
AddRemotePlayer(player->u.remote.peer, queue);
}
// no other players in this session?
if (player->type == GGPO_PLAYERTYPE_LOCAL && _num_players == 1 && _num_spectators == 0)
_synchronizing = false;
return GGPO_OK;
}
GGPOErrorCode
Peer2PeerBackend::AddLocalInput(GGPOPlayerHandle player,
void *values,
int size)
{
int queue;
GameInput input;
GGPOErrorCode result;
if (_sync.InRollback()) {
return GGPO_ERRORCODE_IN_ROLLBACK;
}
if (_synchronizing) {
return GGPO_ERRORCODE_NOT_SYNCHRONIZED;
}
result = PlayerHandleToQueue(player, &queue);
if (!GGPO_SUCCEEDED(result)) {
return result;
}
input.init(-1, (char *)values, size);
// Feed the input for the current frame into the synchronzation layer.
if (!_sync.AddLocalInput(queue, input)) {
return GGPO_ERRORCODE_PREDICTION_THRESHOLD;
}
if (input.frame != GameInput::NullFrame) { // xxx: <- comment why this is the case
// Update the local connect status state to indicate that we've got a
// confirmed local frame for this player. this must come first so it
// gets incorporated into the next packet we send.
// Send checksum for frames old enough to be confirmed (ie older then current - MaxPredictionFrames())
_confirmedCheckSumFrame = input.frame - HowFarBackForChecksums();
input.checksum = 0;
if (_confirmedCheckSumFrame >= 0) {
input.checksum = _pendingCheckSums.at(_confirmedCheckSumFrame);
_confirmedCheckSums[_confirmedCheckSumFrame] = input.checksum;
_pendingCheckSums.erase(_confirmedCheckSumFrame);
Log("Frame %d: Send checksum for frame %d, val %d\n", input.frame, _confirmedCheckSumFrame, input.checksum);
}
Log("setting local connect status for local queue %d to %d", queue, input.frame);
_local_connect_status[queue].last_frame = input.frame;
// Send the input to all the remote players.
for (int i = 0; i < _num_players; i++) {
if (_endpoints[i].IsInitialized()) {
_endpoints[i].SendInput(input);
}
}
}
return GGPO_OK;
}
GGPOErrorCode
Peer2PeerBackend::SyncInput(void *values,
int size,
int *disconnect_flags)
{
int flags;
// Wait until we've started to return inputs.
if (_synchronizing) {
return GGPO_ERRORCODE_NOT_SYNCHRONIZED;
}
flags = _sync.SynchronizeInputs(values, size);
if (disconnect_flags) {
*disconnect_flags = flags;
}
return GGPO_OK;
}
GGPOErrorCode
Peer2PeerBackend::CurrentFrame(int& current)
{
current = _sync.GetFrameCount();
return GGPO_OK;
}
GGPOErrorCode
Peer2PeerBackend::IncrementFrame(uint16_t checksum1)
{
auto currentFrame = _sync.GetFrameCount();
_sync.IncrementFrame();
uint32 cSum = _sync.GetLastSavedFrame().checksum;
char buf[256];
Log("End of frame (%d)...\n", currentFrame);
static int maxDif = 0;
if (_pendingCheckSums.count(currentFrame))
{
auto max = _pendingCheckSums.rbegin()->first;
auto diff = max - currentFrame;
maxDif = MAX(maxDif, diff);
int oldChecksum = _pendingCheckSums[currentFrame];
_pendingCheckSums[currentFrame] = cSum;
std::snprintf(buf, sizeof(buf), "Replace local checksum for frame %d: %d with %d, newest frame is %d, max diff %d\n",
currentFrame, oldChecksum, _pendingCheckSums[currentFrame], max, maxDif);
if (currentFrame <= _confirmedCheckSumFrame)
{
Log("Changing frame %d in a rollback, but we've already sent frame %d\n", currentFrame, _confirmedCheckSumFrame);
abort();
}
if (diff >= (_sync.MaxPredictionFrames())) {
Log("diff is bigger than max prediction\n");
abort();
}
}
else
{
Log("Added local checksum for frame %d: %d\n", currentFrame, cSum);
}
_pendingCheckSums[currentFrame] = cSum;
DoPoll();
PollSyncEvents();
return GGPO_OK;
}
void
Peer2PeerBackend::PollSyncEvents(void)
{
Sync::Event e;
while (_sync.GetEvent(e)) {
OnSyncEvent(e);
}
return;
}
void
Peer2PeerBackend::PollUdpProtocolEvents(void)
{
UdpProtocol::Event evt;
for (int i = 0; i < _num_players; i++) {
_endpoints[i].StartPollLoop();
while (_endpoints[i].GetEvent(evt)) {
OnUdpProtocolPeerEvent(evt, i);
}
_endpoints[i].EndPollLoop();
}
for (int i = 0; i < _num_spectators; i++) {
while (_spectators[i].GetEvent(evt)) {
OnUdpProtocolSpectatorEvent(evt, i);
}
}
//for (int i = 0; i < _num_players; i++) {
// _endpoints[i].ApplyToEvents([&](UdpProtocol::Event& e) {
// OnUdpProtocolPeerEvent(evt, i);
// });
//}
}
void Peer2PeerBackend::CheckRemoteChecksum(int framenumber, uint32 cs)
{
if (framenumber <= _sync.MaxPredictionFrames())
return;
framenumber; cs;
//auto frameOfChecksumToSend = framenumber - (_sync.MaxPredictionFrames() + 1);
}
int Peer2PeerBackend::HowFarBackForChecksums()const
{
return _sync.MaxPredictionFrames() + 2;
}/*
uint16 Peer2PeerBackend::GetChecksumForConfirmedFrame(int frameNumber) const
{
auto frameOfChecksumToSend = frameNumber - HowFarBackForChecksums();
if (frameOfChecksumToSend < 0)
return 0;
if (_checkSums.count(frameOfChecksumToSend) == 0)
{
char s[128];
sprintf_s<128>(s, "No local checksum found, remote frame is %d, adjusted is %d, most recent we have is %d\n", frameNumber, frameOfChecksumToSend, _checkSums.rbegin()->first);
OutputDebugStringA(s);
throw std::exception("s");
}
return _checkSums.at(frameOfChecksumToSend);
}*/
void
Peer2PeerBackend::OnUdpProtocolPeerEvent(UdpProtocol::Event &evt, int queue)
{
OnUdpProtocolEvent(evt, QueueToPlayerHandle(queue));
switch (evt.type) {
case UdpProtocol::Event::Input:
if (!_local_connect_status[queue].disconnected) {
int current_remote_frame = _local_connect_status[queue].last_frame;
int new_remote_frame = evt.u.input.input.frame;
ASSERT(current_remote_frame == -1 || new_remote_frame == (current_remote_frame + 1));
_sync.AddRemoteInput(queue, evt.u.input.input);
// Notify the other endpoints which frame we received from a peer
Log("setting remote connect status for queue %d to %d\n", queue, evt.u.input.input.frame);
_local_connect_status[queue].last_frame = evt.u.input.input.frame;
auto remoteChecksum = evt.u.input.input.checksum;
int checkSumFrame = new_remote_frame - HowFarBackForChecksums();
if (checkSumFrame >= _endpoints[queue].RemoteFrameDelay()-1)
_endpoints[queue]._remoteCheckSumsThisFrame[checkSumFrame] = remoteChecksum;
// auto localChecksum = GetChecksumForConfirmedFrame(new_remote_frame);
//
if (checkSumFrame %120==0)
{
Log("Received checksum for frame %d, remote cs is %d\n", checkSumFrame, remoteChecksum);
}
}
break;
}
}
void
Peer2PeerBackend::OnUdpProtocolSpectatorEvent(UdpProtocol::Event &evt, int queue)
{
GGPOPlayerHandle handle = QueueToSpectatorHandle(queue);
OnUdpProtocolEvent(evt, handle);
}
void
Peer2PeerBackend::OnUdpProtocolEvent(UdpProtocol::Event &evt, GGPOPlayerHandle handle)
{
GGPOEvent info;
switch (evt.type) {
case UdpProtocol::Event::Connected:
info.code = GGPO_EVENTCODE_CONNECTED_TO_PEER;
info.u.connected.player = handle;
_callbacks.on_event(_callbacks.context, &info);
break;
case UdpProtocol::Event::Synchronizing:
info.code = GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER;
info.u.synchronizing.player = handle;
info.u.synchronizing.count = evt.u.synchronizing.count;
info.u.synchronizing.total = evt.u.synchronizing.total;
_callbacks.on_event(_callbacks.context, &info);
break;
case UdpProtocol::Event::Synchronzied:
info.code = GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER;
info.u.synchronized.player = handle;
_callbacks.on_event(_callbacks.context, &info);
CheckInitialSync();
break;
}
}
/*
* Called only as the result of a local decision to disconnect. The remote
* decisions to disconnect are a result of us parsing the peer_connect_settings
* blob in every endpoint periodically.
*/
GGPOErrorCode
Peer2PeerBackend::DisconnectPlayer(GGPOPlayerHandle player)
{
int queue;
GGPOErrorCode result;
result = PlayerHandleToQueue(player, &queue);
if (!GGPO_SUCCEEDED(result)) {
return result;
}
if (_local_connect_status[queue].disconnected) {
return GGPO_ERRORCODE_PLAYER_DISCONNECTED;
}
if (!_endpoints[queue].IsInitialized()) {
int current_frame = _sync.GetFrameCount();
// xxx: we should be tracking who the local player is, but for now assume
// that if the endpoint is not initalized, this must be the local player.
Log("Disconnecting local player %d at frame %d by user request.\n", queue, _local_connect_status[queue].last_frame);
for (int i = 0; i < _num_players; i++) {
if (_endpoints[i].IsInitialized()) {
DisconnectPlayerQueue(i, current_frame);
}
}
} else {
Log("Disconnecting queue %d at frame %d by user request.\n", queue, _local_connect_status[queue].last_frame);
DisconnectPlayerQueue(queue, _local_connect_status[queue].last_frame);
}
return GGPO_OK;
}
void
Peer2PeerBackend::DisconnectPlayerQueue(int queue, int syncto)
{
GGPOEvent info;
int framecount = _sync.GetFrameCount();
// TODO: Where does the endpoint actually get removed? I can't see it anywhere...
_endpoints[queue].Disconnect();
Log("Changing queue %d local connect status for last frame from %d to %d on disconnect request (current: %d).\n",
queue, _local_connect_status[queue].last_frame, syncto, framecount);
_local_connect_status[queue].disconnected = 1;
_local_connect_status[queue].last_frame = syncto;
/*if (syncto < framecount) {
Log("adjusting simulation to account for the fact that %d disconnected @ %d.\n", queue, syncto);
_sync.AdjustSimulation(syncto);
Log("finished adjusting simulation.\n");
}*/
CheckInitialSync();
}
GGPOErrorCode
Peer2PeerBackend::GetNetworkStats(GGPONetworkStats *stats, GGPOPlayerHandle player)
{
int queue;
GGPOErrorCode result;
result = PlayerHandleToQueue(player, &queue);
if (!GGPO_SUCCEEDED(result)) {
return result;
}
memset(stats, 0, sizeof *stats);
_endpoints[queue].GetNetworkStats(stats);
return GGPO_OK;
}
GGPOErrorCode
Peer2PeerBackend::SetFrameDelay(GGPOPlayerHandle player, int delay)
{
int queue;
GGPOErrorCode result;
result = PlayerHandleToQueue(player, &queue);
if (!GGPO_SUCCEEDED(result)) {
return result;
}
_sync.SetFrameDelay(queue, delay);
for (int i = 0; i < _num_players; i++) {
if (_endpoints[i].IsInitialized()) {
_endpoints[i].SetFrameDelay(delay);
}
}
return GGPO_OK;
}
GGPOErrorCode
Peer2PeerBackend::PlayerHandleToQueue(GGPOPlayerHandle player, int *queue)
{
int offset = ((int)player - 1);
if (offset < 0 || offset >= _num_players) {
return GGPO_ERRORCODE_INVALID_PLAYER_HANDLE;
}
*queue = offset;
return GGPO_OK;
}
GGPOErrorCode
Peer2PeerBackend::OnPacket(ENetPeer* peer, const ENetPacket* pkt)
{
// ugh why is const so hard for some people...
UdpMsg* msg = const_cast<UdpMsg*>(reinterpret_cast<const UdpMsg*>(pkt->data));
const int len = static_cast<int>(pkt->dataLength);
for (int i = 0; i < _num_players; i++) {
if (_endpoints[i].GetENetPeer() == peer) {
_endpoints[i].OnMsg(msg, len);
return GGPO_OK;
}
}
for (int i = 0; i < _num_spectators; i++) {
if (_spectators[i].GetENetPeer() == peer) {
_spectators[i].OnMsg(msg, len);
return GGPO_OK;
}
}
return GGPO_ERRORCODE_INVALID_PLAYER_HANDLE;
}
void
Peer2PeerBackend::CheckInitialSync()
{
int i;
if (_synchronizing) {
// Check to see if everyone is now synchronized. If so,
// go ahead and tell the client that we're ok to accept input.
for (i = 0; i < _num_players; i++) {
// xxx: IsInitialized() must go... we're actually using it as a proxy for "represents the local player"
if (_endpoints[i].IsInitialized() && !_endpoints[i].IsSynchronized() && !_local_connect_status[i].disconnected) {
return;
}
}
for (i = 0; i < _num_spectators; i++) {
if (_spectators[i].IsInitialized() && !_spectators[i].IsSynchronized()) {
return;
}
}
GGPOEvent info;
info.code = GGPO_EVENTCODE_RUNNING;
_callbacks.on_event(_callbacks.context, &info);
_synchronizing = false;
}
}

View File

@ -0,0 +1,83 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _P2P_H
#define _P2P_H
#include "types.h"
#include "sync.h"
#include "backend.h"
#include "timesync.h"
#include "network/udp_proto.h"
#include <map>
class Peer2PeerBackend final : public GGPOSession
{
public:
Peer2PeerBackend(GGPOSessionCallbacks *cb, int num_players, int input_size, int nframes);
virtual ~Peer2PeerBackend();
public:
virtual GGPOErrorCode DoPoll() override;
virtual GGPOErrorCode NetworkIdle() override;
virtual GGPOErrorCode AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle) override;
virtual GGPOErrorCode AddLocalInput(GGPOPlayerHandle player, void *values, int size) override;
virtual GGPOErrorCode SyncInput(void *values, int size, int *disconnect_flags) override;
virtual GGPOErrorCode IncrementFrame(uint16_t checksum) override;
virtual GGPOErrorCode DisconnectPlayer(GGPOPlayerHandle handle) override;
virtual GGPOErrorCode GetNetworkStats(GGPONetworkStats *stats, GGPOPlayerHandle handle) override;
virtual GGPOErrorCode SetFrameDelay(GGPOPlayerHandle player, int delay) override;
virtual GGPOErrorCode CurrentFrame(int& current) override;
virtual GGPOErrorCode OnPacket(ENetPeer* peer, const ENetPacket* pkt) override;
protected:
GGPOErrorCode PlayerHandleToQueue(GGPOPlayerHandle player, int *queue);
GGPOPlayerHandle QueueToPlayerHandle(int queue) { return (GGPOPlayerHandle)(queue + 1); }
GGPOPlayerHandle QueueToSpectatorHandle(int queue) { return (GGPOPlayerHandle)(queue + 1000); } /* out of range of the player array, basically */
void DisconnectPlayerQueue(int queue, int syncto);
void PollSyncEvents(void);
void PollUdpProtocolEvents(void);
void CheckInitialSync(void);
int Poll2Players(int current_frame);
int PollNPlayers(int current_frame);
void AddRemotePlayer(ENetPeer* peer, int queue);
GGPOErrorCode AddSpectator(ENetPeer* peer);
virtual void OnSyncEvent(Sync::Event &e) { }
virtual void OnUdpProtocolEvent(UdpProtocol::Event &e, GGPOPlayerHandle handle);
virtual void OnUdpProtocolPeerEvent(UdpProtocol::Event &e, int queue);
virtual void OnUdpProtocolSpectatorEvent(UdpProtocol::Event &e, int queue);
protected:
GGPOSessionCallbacks _callbacks;
Sync _sync;
std::vector<UdpProtocol> _endpoints;
UdpProtocol _spectators[GGPO_MAX_SPECTATORS];
int _num_spectators;
int _input_size;
bool _synchronizing;
int _num_players;
int _next_recommended_sleep;
int _next_spectator_frame;
UdpMsg::connect_status _local_connect_status[UDP_MSG_MAX_PLAYERS];
struct ChecksumEntry {
int nFrame;
int checkSum;;
};
std::map<int, uint32> _pendingCheckSums;
std::map<int, uint32> _confirmedCheckSums;
// uint16 GetChecksumForConfirmedFrame(int frameNumber) const;
void CheckRemoteChecksum(int framenumber, uint32 cs);
int HowFarBackForChecksums()const;
int _confirmedCheckSumFrame = -500;
void CheckDesync();
};
#endif

View File

@ -0,0 +1,152 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#include "spectator.h"
SpectatorBackend::SpectatorBackend(GGPOSessionCallbacks* cb, int num_players, int input_size, ENetPeer* peer)
: _num_players(num_players), _input_size(input_size), _next_input_to_send(0)
{
_callbacks = *cb;
_synchronizing = true;
for (int i = 0; i < ARRAY_SIZE(_inputs); i++) {
_inputs[i].frame = -1;
}
/*
* Init the host endpoint
*/
_host.Init(peer, 0, NULL);
_host.Synchronize();
}
SpectatorBackend::~SpectatorBackend()
{
}
GGPOErrorCode
SpectatorBackend::DoPoll()
{
PollUdpProtocolEvents();
return GGPO_OK;
}
GGPOErrorCode SpectatorBackend::NetworkIdle()
{
_host.NetworkIdle();
return GGPO_OK;
}
GGPOErrorCode
SpectatorBackend::SyncInput(void *values,
int size,
int *disconnect_flags)
{
// Wait until we've started to return inputs.
if (_synchronizing) {
return GGPO_ERRORCODE_NOT_SYNCHRONIZED;
}
GameInput &input = _inputs[_next_input_to_send % SPECTATOR_FRAME_BUFFER_SIZE];
if (input.frame < _next_input_to_send) {
// Haven't received the input from the host yet. Wait
return GGPO_ERRORCODE_PREDICTION_THRESHOLD;
}
if (input.frame > _next_input_to_send) {
// The host is way way way far ahead of the spectator. How'd this
// happen? Anyway, the input we need is gone forever.
return GGPO_ERRORCODE_GENERAL_FAILURE;
}
ASSERT(size >= _input_size * _num_players);
memcpy(values, input.bits, _input_size * _num_players);
if (disconnect_flags) {
*disconnect_flags = 0; // xxx: should get them from the host!
}
_next_input_to_send++;
return GGPO_OK;
}
GGPOErrorCode
SpectatorBackend::CurrentFrame(int& current)
{
current= _next_input_to_send;
return GGPO_OK;
}
GGPOErrorCode
SpectatorBackend::IncrementFrame(uint16_t checksum)
{
checksum;
Log("End of frame (%d)...\n", _next_input_to_send - 1);
DoPoll();
PollUdpProtocolEvents();
return GGPO_OK;
}
void
SpectatorBackend::PollUdpProtocolEvents(void)
{
UdpProtocol::Event evt;
while (_host.GetEvent(evt)) {
OnUdpProtocolEvent(evt);
}
}
void
SpectatorBackend::OnUdpProtocolEvent(UdpProtocol::Event &evt)
{
GGPOEvent info;
switch (evt.type) {
case UdpProtocol::Event::Connected:
info.code = GGPO_EVENTCODE_CONNECTED_TO_PEER;
info.u.connected.player = 0;
_callbacks.on_event(_callbacks.context, &info);
break;
case UdpProtocol::Event::Synchronizing:
info.code = GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER;
info.u.synchronizing.player = 0;
info.u.synchronizing.count = evt.u.synchronizing.count;
info.u.synchronizing.total = evt.u.synchronizing.total;
_callbacks.on_event(_callbacks.context, &info);
break;
case UdpProtocol::Event::Synchronzied:
if (_synchronizing) {
info.code = GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER;
info.u.synchronized.player = 0;
_callbacks.on_event(_callbacks.context, &info);
info.code = GGPO_EVENTCODE_RUNNING;
_callbacks.on_event(_callbacks.context, &info);
_synchronizing = false;
}
break;
case UdpProtocol::Event::Input:
GameInput& input = evt.u.input.input;
_host.SetLocalFrameNumber(input.frame);
_host.SendInputAck();
_inputs[input.frame % SPECTATOR_FRAME_BUFFER_SIZE] = input;
break;
}
}
GGPOErrorCode SpectatorBackend::OnPacket(ENetPeer* peer, const ENetPacket* pkt)
{
if (_host.GetENetPeer() != peer)
return GGPO_ERRORCODE_INVALID_PLAYER_HANDLE;
UdpMsg* msg = const_cast<UdpMsg*>(reinterpret_cast<const UdpMsg*>(pkt->data));
const int len = static_cast<int>(pkt->dataLength);
_host.OnMsg(msg, len);
return GGPO_OK;
}

View File

@ -0,0 +1,56 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _SPECTATOR_H
#define _SPECTATOR_H
#include "types.h"
#include "sync.h"
#include "backend.h"
#include "timesync.h"
#include "network/udp_proto.h"
#define SPECTATOR_FRAME_BUFFER_SIZE 64
class SpectatorBackend final : public GGPOSession {
public:
SpectatorBackend(GGPOSessionCallbacks *cb, int num_players, int input_size, ENetPeer* peer);
virtual ~SpectatorBackend();
public:
virtual GGPOErrorCode DoPoll();
virtual GGPOErrorCode NetworkIdle();
virtual GGPOErrorCode AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle) { return GGPO_ERRORCODE_UNSUPPORTED; }
virtual GGPOErrorCode AddLocalInput(GGPOPlayerHandle player, void *values, int size) { return GGPO_OK; }
virtual GGPOErrorCode SyncInput(void *values, int size, int *disconnect_flags);
virtual GGPOErrorCode IncrementFrame(uint16_t);
virtual GGPOErrorCode DisconnectPlayer(GGPOPlayerHandle handle) { return GGPO_ERRORCODE_UNSUPPORTED; }
virtual GGPOErrorCode GetNetworkStats(GGPONetworkStats *stats, GGPOPlayerHandle handle) { return GGPO_ERRORCODE_UNSUPPORTED; }
virtual GGPOErrorCode SetFrameDelay(GGPOPlayerHandle player, int delay) { return GGPO_ERRORCODE_UNSUPPORTED; }
virtual GGPOErrorCode SetDisconnectTimeout(int timeout) { return GGPO_ERRORCODE_UNSUPPORTED; }
virtual GGPOErrorCode SetDisconnectNotifyStart(int timeout) { return GGPO_ERRORCODE_UNSUPPORTED; }
virtual GGPOErrorCode CurrentFrame(int& current) override;
virtual GGPOErrorCode OnPacket(ENetPeer* peer, const ENetPacket* pkt) override;
protected:
void PollUdpProtocolEvents(void);
void CheckInitialSync(void);
void OnUdpProtocolEvent(UdpProtocol::Event &e);
protected:
GGPOSessionCallbacks _callbacks;
UdpProtocol _host;
bool _synchronizing;
int _input_size;
int _num_players;
int _next_input_to_send;
GameInput _inputs[SPECTATOR_FRAME_BUFFER_SIZE];
};
#endif

View File

@ -0,0 +1,224 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#include "synctest.h"
#include <cstdio>
SyncTestBackend::SyncTestBackend(GGPOSessionCallbacks *cb,
int frames,
int num_players) :
_sync(NULL, MAX_PREDICTION_FRAMES)
{
_callbacks = *cb;
_num_players = num_players;
_check_distance = frames;
_last_verified = 0;
_rollingback = false;
_running = false;
_logfp = NULL;
_current_input.erase();
/*
* Initialize the synchronziation layer
*/
Sync::Config config = { 0 };
config.callbacks = _callbacks;
config.num_prediction_frames = MAX_PREDICTION_FRAMES;
_sync.Init(config);
}
SyncTestBackend::~SyncTestBackend()
{
}
GGPOErrorCode
SyncTestBackend::DoPoll()
{
if (!_running) {
GGPOEvent info;
info.code = GGPO_EVENTCODE_RUNNING;
_callbacks.on_event(_callbacks.context, &info);
_running = true;
}
return GGPO_OK;
}
GGPOErrorCode
SyncTestBackend::NetworkIdle()
{
return GGPO_OK;
}
GGPOErrorCode
SyncTestBackend::AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle)
{
if (player->player_num < 1 || player->player_num > _num_players) {
return GGPO_ERRORCODE_PLAYER_OUT_OF_RANGE;
}
*handle = (GGPOPlayerHandle)(player->player_num - 1);
return GGPO_OK;
}
GGPOErrorCode
SyncTestBackend::AddLocalInput(GGPOPlayerHandle player, void *values, int size)
{
if (!_running) {
return GGPO_ERRORCODE_NOT_SYNCHRONIZED;
}
int index = (int)player;
for (int i = 0; i < size; i++) {
_current_input.bits[(index * size) + i] |= ((char *)values)[i];
}
return GGPO_OK;
}
GGPOErrorCode
SyncTestBackend::SyncInput(void *values,
int size,
int *disconnect_flags)
{
BeginLog(false);
if (_rollingback) {
_last_input = _saved_frames.front().input;
} else {
if (_sync.GetFrameCount() == 0) {
_sync.SaveCurrentFrame();
}
_last_input = _current_input;
}
memcpy(values, _last_input.bits, size);
if (disconnect_flags) {
*disconnect_flags = 0;
}
return GGPO_OK;
}
GGPOErrorCode SyncTestBackend::CurrentFrame(int& current)
{
current = _sync.GetFrameCount();
return GGPO_OK;
}
GGPOErrorCode
SyncTestBackend::IncrementFrame(uint16_t cs)
{
cs;
_sync.IncrementFrame();
_current_input.erase();
Log("End of frame(%d)...\n", _sync.GetFrameCount());
EndLog();
if (_rollingback) {
return GGPO_OK;
}
int frame = _sync.GetFrameCount();
// Hold onto the current frame in our queue of saved states. We'll need
// the checksum later to verify that our replay of the same frame got the
// same results.
SavedInfo info;
info.frame = frame;
info.input = _last_input;
const Sync::SavedFrame& lsf = _sync.GetLastSavedFrame();
info.cbuf = 0;
info.buf = nullptr;
info.checksum = 0;
_callbacks.copy_game_state(_callbacks.context, &info.buf, &info.cbuf, &info.checksum, lsf.buf, lsf.cbuf, lsf.checksum);
info.checksum = _sync.GetLastSavedFrame().checksum;
_saved_frames.push(info);
if (frame - _last_verified == _check_distance) {
// We've gone far enough ahead and should now start replaying frames.
// Load the last verified frame and set the rollback flag to true.
_sync.LoadFrame(_last_verified,(int)_saved_frames.size());
_rollingback = true;
while(!_saved_frames.empty()) {
_callbacks.advance_frame(_callbacks.context, 0);
// Verify that the checksumn of this frame is the same as the one in our
// list.
info = _saved_frames.front();
_saved_frames.pop();
if (info.frame != _sync.GetFrameCount()) {
RaiseSyncError("Frame number %d does not match saved frame number %d", info.frame, frame);
}
int checksum = _sync.GetLastSavedFrame().checksum;
if (info.checksum != checksum) {
LogSaveStates(info);
RaiseSyncError("Checksum for frame %d does not match saved (%d != %d)", frame, checksum, info.checksum);
}
printf("Checksum %08d for frame %d matches.\n", checksum, info.frame);
_callbacks.free_buffer(_callbacks.context, info.buf, info.frame);
}
_last_verified = frame;
_rollingback = false;
}
return GGPO_OK;
}
void
SyncTestBackend::RaiseSyncError(const char *fmt, ...)
{
char buf[1024];
va_list args;
va_start(args, fmt);
std::fprintf(stderr, buf, ARRAY_SIZE(buf), fmt, args);
std::fputc('\n', stderr);
va_end(args);
}
GGPOErrorCode
SyncTestBackend::Logv(char *fmt, va_list list)
{
if (_logfp) {
vfprintf(_logfp, fmt, list);
}
return GGPO_OK;
}
void
SyncTestBackend::BeginLog(int saving)
{
EndLog();
char filename[256];
//CreateDirectoryA("synclogs", NULL);
std::snprintf(filename, ARRAY_SIZE(filename), "synclogs\\%s-%04d-%s.log",
saving ? "state" : "log",
_sync.GetFrameCount(),
_rollingback ? "replay" : "original");
_logfp = std::fopen(filename, "w");
}
void
SyncTestBackend::EndLog()
{
if (_logfp) {
fprintf(_logfp, "Closing log file.\n");
fclose(_logfp);
_logfp = NULL;
}
}
void
SyncTestBackend::LogSaveStates(SavedInfo &info)
{
char filename[256];
std::snprintf(filename, ARRAY_SIZE(filename), "synclogs\\state-%04d-original.log", _sync.GetFrameCount());
_callbacks.log_game_state(_callbacks.context, filename, (unsigned char *)info.buf, info.cbuf);
std::snprintf(filename, ARRAY_SIZE(filename), "synclogs\\state-%04d-replay.log", _sync.GetFrameCount());
_callbacks.log_game_state(_callbacks.context, filename, _sync.GetLastSavedFrame().buf, _sync.GetLastSavedFrame().cbuf);
}

View File

@ -0,0 +1,62 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _SYNCTEST_H
#define _SYNCTEST_H
#include "types.h"
#include "backend.h"
#include "sync.h"
#include "ring_buffer.h"
class SyncTestBackend final : public GGPOSession {
public:
SyncTestBackend(GGPOSessionCallbacks *cb, int frames, int num_players);
virtual ~SyncTestBackend();
virtual GGPOErrorCode DoPoll();
virtual GGPOErrorCode NetworkIdle();
virtual GGPOErrorCode AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle);
virtual GGPOErrorCode AddLocalInput(GGPOPlayerHandle player, void *values, int size);
virtual GGPOErrorCode SyncInput(void *values, int size, int *disconnect_flags);
virtual GGPOErrorCode IncrementFrame(uint16_t checksum);
virtual GGPOErrorCode Logv(char *fmt, va_list list);
virtual GGPOErrorCode DisconnectPlayer(GGPOPlayerHandle handle) { return GGPO_OK; }
virtual GGPOErrorCode CurrentFrame(int& current) override;
virtual GGPOErrorCode OnPacket(ENetPeer* peer, const ENetPacket* pkt) override { return GGPO_ERRORCODE_UNSUPPORTED; }
protected:
struct SavedInfo {
int frame;
int checksum;
byte *buf;
int cbuf;
GameInput input;
};
void RaiseSyncError(const char *fmt, ...);
void BeginLog(int saving);
void EndLog();
void LogSaveStates(SavedInfo &info);
protected:
GGPOSessionCallbacks _callbacks;
Sync _sync;
int _num_players;
int _check_distance;
int _last_verified;
bool _rollingback;
bool _running;
FILE *_logfp;
GameInput _current_input;
GameInput _last_input;
RingBuffer<SavedInfo, 32> _saved_frames;
};
#endif

View File

@ -0,0 +1,56 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#include "types.h"
#include "bitvector.h"
void
BitVector_SetBit(uint8 *vector, int *offset)
{
vector[(*offset) / 8] |= (1 << ((*offset) % 8));
*offset += 1;
}
void
BitVector_ClearBit(uint8 *vector, int *offset)
{
vector[(*offset) / 8] &= ~(1 << ((*offset) % 8));
*offset += 1;
}
void
BitVector_WriteNibblet(uint8 *vector, int nibble, int *offset)
{
ASSERT(nibble < (1 << BITVECTOR_NIBBLE_SIZE));
for (int i = 0; i < BITVECTOR_NIBBLE_SIZE; i++) {
if (nibble & (1 << i)) {
BitVector_SetBit(vector, offset);
} else {
BitVector_ClearBit(vector, offset);
}
}
}
int
BitVector_ReadBit(uint8 *vector, int *offset)
{
int retval = !!(vector[(*offset) / 8] & (1 << ((*offset) % 8)));
*offset += 1;
return retval;
}
int
BitVector_ReadNibblet(uint8 *vector, int *offset)
{
int nibblet = 0;
for (int i = 0; i < BITVECTOR_NIBBLE_SIZE; i++) {
nibblet |= (BitVector_ReadBit(vector, offset) << i);
}
return nibblet;
}

View File

@ -0,0 +1,19 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _BITVECTOR_H
#define _BITVECTOR_H
#define BITVECTOR_NIBBLE_SIZE 8
void BitVector_SetBit(uint8 *vector, int *offset);
void BitVector_ClearBit(uint8 *vector, int *offset);
void BitVector_WriteNibblet(uint8 *vector, int nibble, int *offset);
int BitVector_ReadBit(uint8 *vector, int *offset);
int BitVector_ReadNibblet(uint8 *vector, int *offset);
#endif // _BITVECTOR_H

View File

@ -0,0 +1,96 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#include "types.h"
#include "game_input.h"
#include "log.h"
#include <algorithm>
#include <cstdio>
#include <cstring>
void
GameInput::init(int iframe, char *ibits, int isize, int offset)
{
ASSERT(isize);
ASSERT(isize <= GAMEINPUT_MAX_BYTES);
frame = iframe;
size = isize;
memset(bits, 0, sizeof(bits));
if (ibits) {
memcpy(bits + (offset * isize), ibits, isize);
}
}
void
GameInput::init(int iframe, char *ibits, int isize)
{
ASSERT(isize);
ASSERT(isize <= GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS);
frame = iframe;
size = isize;
memset(bits, 0, sizeof(bits));
if (ibits) {
memcpy(bits, ibits, isize);
}
}
void
GameInput::desc(char *buf, size_t buf_size, bool show_frame) const
{
ASSERT(size);
int remaining = static_cast<int>(buf_size);
if (show_frame) {
remaining -= std::snprintf(buf, buf_size, "(frame:%d size:%d ", frame, size);
} else {
remaining -= std::snprintf(buf, buf_size, "(size:%d ", size);
}
for (int i = 0; i < size * 8 && remaining > 0; i++) {
char buf2[16];
if (value(i)) {
int c = std::snprintf(buf2, ARRAY_SIZE(buf2), "%2d ", i);
std::strncat(buf, buf2, std::min(c, remaining));
remaining -= c;
}
}
if (remaining > 1)
{
std::strncat(buf, ")", 1);
remaining--;
}
buf[buf_size - remaining - 1] = '\0';
}
void
GameInput::log(char *prefix, bool show_frame) const
{
char buf[1024];
size_t c = std::strlen(prefix);
std::strncpy(buf, prefix, c);
desc(buf + c, ARRAY_SIZE(buf) - c, show_frame);
Log("%s\n", buf);
}
bool
GameInput::equal(GameInput &other, bool bitsonly)
{
if (!bitsonly && frame != other.frame) {
Log("frames don't match: %d, %d\n", frame, other.frame);
}
if (size != other.size) {
Log("sizes don't match: %d, %d\n", size, other.size);
}
if (memcmp(bits, other.bits, size)) {
Log("bits don't match\n");
}
ASSERT(size && other.size);
return (bitsonly || frame == other.frame) &&
size == other.size &&
memcmp(bits, other.bits, size) == 0;
}

View File

@ -0,0 +1,40 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _GAMEINPUT_H
#define _GAMEINPUT_H
#include <stdio.h>
#include <memory.h>
// GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS * 8 must be less than
// 2^BITVECTOR_NIBBLE_SIZE (see bitvector.h)
#define GAMEINPUT_MAX_BYTES 4
#define GAMEINPUT_MAX_PLAYERS 6
struct GameInput {
enum Constants {
NullFrame = -1
};
int frame;
int size; /* size in bytes of the entire input for all players */
char bits[GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS];
uint32 checksum;
bool is_null() { return frame == NullFrame; }
void init(int frame, char *bits, int size, int offset);
void init(int frame, char *bits, int size);
bool value(int i) const { return (bits[i/8] & (1 << (i%8))) != 0; }
void set(int i) { bits[i/8] |= (1 << (i%8)); }
void clear(int i) { bits[i/8] &= ~(1 << (i%8)); }
void erase() { memset(bits, 0, sizeof(bits)); }
void desc(char *buf, size_t buf_size, bool show_frame = true) const;
void log(char *prefix, bool show_frame = true) const;
bool equal(GameInput &input, bool bitsonly = false);
};
#endif

View File

@ -0,0 +1,323 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#include "types.h"
#include "input_queue.h"
#include <cstdarg>
#include <cstdio>
#define PREVIOUS_FRAME(offset) (((offset) == 0) ? (INPUT_QUEUE_LENGTH - 1) : ((offset) - 1))
InputQueue::InputQueue(int input_size)
{
Init(-1, input_size);
}
InputQueue::~InputQueue()
{
}
void
InputQueue::Init(int id, int input_size)
{
_id = id;
_head = 0;
_tail = 0;
_length = 0;
_frame_delay = 0;
_first_frame = true;
_last_user_added_frame = GameInput::NullFrame;
_first_incorrect_frame = GameInput::NullFrame;
_last_frame_requested = GameInput::NullFrame;
_last_added_frame = GameInput::NullFrame;
_prediction.init(GameInput::NullFrame, NULL, input_size);
/*
* This is safe because we know the GameInput is a proper structure (as in,
* no virtual methods, no contained classes, etc.).
*/
memset(_inputs, 0, sizeof _inputs);
for (int i = 0; i < ARRAY_SIZE(_inputs); i++) {
_inputs[i].size = input_size;
}
}
int
InputQueue::GetLastConfirmedFrame()
{
Log("returning last confirmed frame %d.\n", _last_added_frame);
return _last_added_frame;
}
int
InputQueue::GetFirstIncorrectFrame()
{
return _first_incorrect_frame;
}
void
InputQueue::DiscardConfirmedFrames(int frame)
{
ASSERT(frame >= 0);
if (_last_frame_requested != GameInput::NullFrame) {
frame = MIN(frame, _last_frame_requested);
}
Log("discarding confirmed frames up to %d (last_added:%d length:%d [head:%d tail:%d]).\n",
frame, _last_added_frame, _length, _head, _tail);
if (frame >= _last_added_frame) {
_tail = _head;
} else {
int offset = frame - _inputs[_tail].frame + 1;
Log("difference of %d frames.\n", offset);
ASSERT(offset >= 0);
_tail = (_tail + offset) % INPUT_QUEUE_LENGTH;
_length -= offset;
}
Log("after discarding, new tail is %d (frame:%d).\n", _tail, _inputs[_tail].frame);
ASSERT(_length >= 0);
}
void
InputQueue::ResetPrediction(int frame)
{
ASSERT(_first_incorrect_frame == GameInput::NullFrame || frame <= _first_incorrect_frame);
Log("resetting all prediction errors back to frame %d.\n", frame);
/*
* There's nothing really to do other than reset our prediction
* state and the incorrect frame counter...
*/
_prediction.frame = GameInput::NullFrame;
_first_incorrect_frame = GameInput::NullFrame;
_last_frame_requested = GameInput::NullFrame;
}
bool
InputQueue::GetConfirmedInput(int requested_frame, GameInput *input)
{
ASSERT(_first_incorrect_frame == GameInput::NullFrame || requested_frame < _first_incorrect_frame);
int offset = requested_frame % INPUT_QUEUE_LENGTH;
if (_inputs[offset].frame != requested_frame) {
return false;
}
*input = _inputs[offset];
return true;
}
bool
InputQueue::GetInput(int requested_frame, GameInput *input)
{
Log("requesting input frame %d.\n", requested_frame);
/*
* No one should ever try to grab any input when we have a prediction
* error. Doing so means that we're just going further down the wrong
* path. ASSERT this to verify that it's true.
*/
ASSERT(_first_incorrect_frame == GameInput::NullFrame);
/*
* Remember the last requested frame number for later. We'll need
* this in AddInput() to drop out of prediction mode.
*/
_last_frame_requested = requested_frame;
ASSERT(requested_frame >= _inputs[_tail].frame);
if (_prediction.frame == GameInput::NullFrame) {
/*
* If the frame requested is in our range, fetch it out of the queue and
* return it.
*/
int offset = requested_frame - _inputs[_tail].frame;
if (offset < _length) {
offset = (offset + _tail) % INPUT_QUEUE_LENGTH;
ASSERT(_inputs[offset].frame == requested_frame);
*input = _inputs[offset];
Log("returning confirmed frame number %d.\n", input->frame);
return true;
}
/*
* The requested frame isn't in the queue. Bummer. This means we need
* to return a prediction frame. Predict that the user will do the
* same thing they did last time.
*/
if (requested_frame == 0) {
Log("basing new prediction frame from nothing, you're client wants frame 0.\n");
_prediction.erase();
} else if (_last_added_frame == GameInput::NullFrame) {
Log("basing new prediction frame from nothing, since we have no frames yet.\n");
_prediction.erase();
} else {
Log("basing new prediction frame from previously added frame (queue entry:%d, frame:%d).\n",
PREVIOUS_FRAME(_head), _inputs[PREVIOUS_FRAME(_head)].frame);
_prediction = _inputs[PREVIOUS_FRAME(_head)];
}
_prediction.frame++;
}
ASSERT(_prediction.frame >= 0);
/*
* If we've made it this far, we must be predicting. Go ahead and
* forward the prediction frame contents. Be sure to return the
* frame number requested by the client, though.
*/
*input = _prediction;
input->frame = requested_frame;
Log("returning prediction frame number %d (%d).\n", input->frame, _prediction.frame);
return false;
}
void
InputQueue::AddInput(GameInput &input)
{
int new_frame;
Log("adding input frame number %d to queue.\n", input.frame);
/*
* These next two lines simply verify that inputs are passed in
* sequentially by the user, regardless of frame delay.
*/
ASSERT(_last_user_added_frame == GameInput::NullFrame ||
input.frame == _last_user_added_frame + 1);
_last_user_added_frame = input.frame;
/*
* Move the queue head to the correct point in preparation to
* input the frame into the queue.
*/
new_frame = AdvanceQueueHead(input.frame);
if (new_frame != GameInput::NullFrame) {
AddDelayedInputToQueue(input, new_frame);
}
/*
* Update the frame number for the input. This will also set the
* frame to GameInput::NullFrame for frames that get dropped (by
* design).
*/
input.frame = new_frame;
}
void
InputQueue::AddDelayedInputToQueue(GameInput &input, int frame_number)
{
Log("adding delayed input frame number %d to queue.\n", frame_number);
ASSERT(input.size == _prediction.size);
ASSERT(_last_added_frame == GameInput::NullFrame || frame_number == _last_added_frame + 1);
ASSERT(frame_number == 0 || _inputs[PREVIOUS_FRAME(_head)].frame == frame_number - 1);
/*
* Add the frame to the back of the queue
*/
_inputs[_head] = input;
_inputs[_head].frame = frame_number;
_head = (_head + 1) % INPUT_QUEUE_LENGTH;
_length++;
_first_frame = false;
_last_added_frame = frame_number;
if (_prediction.frame != GameInput::NullFrame) {
ASSERT(frame_number == _prediction.frame);
/*
* We've been predicting... See if the inputs we've gotten match
* what we've been predicting. If so, don't worry about it. If not,
* remember the first input which was incorrect so we can report it
* in GetFirstIncorrectFrame()
*/
if (_first_incorrect_frame == GameInput::NullFrame && !_prediction.equal(input, true)) {
Log("frame %d does not match prediction. marking error.\n", frame_number);
_first_incorrect_frame = frame_number;
}
/*
* If this input is the same frame as the last one requested and we
* still haven't found any mis-predicted inputs, we can dump out
* of predition mode entirely! Otherwise, advance the prediction frame
* count up.
*/
if (_prediction.frame == _last_frame_requested && _first_incorrect_frame == GameInput::NullFrame) {
Log("prediction is correct! dumping out of prediction mode.\n");
_prediction.frame = GameInput::NullFrame;
} else {
_prediction.frame++;
}
}
ASSERT(_length <= INPUT_QUEUE_LENGTH);
}
int
InputQueue::AdvanceQueueHead(int frame)
{
Log("advancing queue head to frame %d.\n", frame);
int expected_frame = _first_frame ? 0 : _inputs[PREVIOUS_FRAME(_head)].frame + 1;
frame += _frame_delay;
if (expected_frame > frame) {
/*
* This can occur when the frame delay has dropped since the last
* time we shoved a frame into the system. In this case, there's
* no room on the queue. Toss it.
*/
Log("Dropping input frame %d (expected next frame to be %d).\n",
frame, expected_frame);
return GameInput::NullFrame;
}
while (expected_frame < frame) {
/*
* This can occur when the frame delay has been increased since the last
* time we shoved a frame into the system. We need to replicate the
* last frame in the queue several times in order to fill the space
* left.
*/
Log("Adding padding frame %d to account for change in frame delay.\n",
expected_frame);
GameInput &last_frame = _inputs[PREVIOUS_FRAME(_head)];
AddDelayedInputToQueue(last_frame, expected_frame);
expected_frame++;
}
ASSERT(frame == 0 || frame == _inputs[PREVIOUS_FRAME(_head)].frame + 1);
return frame;
}
void
InputQueue::Log(const char *fmt, ...)
{
char buf[1024];
size_t offset;
std::va_list args;
offset = std::snprintf(buf, ARRAY_SIZE(buf), "input q%d | ", _id);
va_start(args, fmt);
vsnprintf(buf + offset, ARRAY_SIZE(buf) - offset - 1, fmt, args);
buf[ARRAY_SIZE(buf)-1] = '\0';
::Log(buf);
va_end(args);
}

View File

@ -0,0 +1,60 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _INPUT_QUEUE_H
#define _INPUT_QUEUE_H
#include "game_input.h"
#define INPUT_QUEUE_LENGTH 128
#define DEFAULT_INPUT_SIZE 4
class InputQueue {
public:
InputQueue(int input_size = DEFAULT_INPUT_SIZE);
~InputQueue();
public:
void Init(int id, int input_size);
int GetLastConfirmedFrame();
int GetFirstIncorrectFrame();
int GetLength() { return _length; }
void SetFrameDelay(int delay) { _frame_delay = delay; }
void ResetPrediction(int frame);
void DiscardConfirmedFrames(int frame);
bool GetConfirmedInput(int frame, GameInput *input);
bool GetInput(int frame, GameInput *input);
void AddInput(GameInput &input);
protected:
int AdvanceQueueHead(int frame);
void AddDelayedInputToQueue(GameInput &input, int i);
void Log(const char *fmt, ...);
protected:
int _id;
int _head;
int _tail;
int _length;
bool _first_frame;
int _last_user_added_frame;
int _last_added_frame;
int _first_incorrect_frame;
int _last_frame_requested;
int _frame_delay;
GameInput _inputs[INPUT_QUEUE_LENGTH];
GameInput _prediction;
};
#endif

23
dep/ggpo-x/src/log.cpp Normal file
View File

@ -0,0 +1,23 @@
#include "types.h"
#include <cstdio>
#include <cstdarg>
//#define ENABLE_LOGGING
void Log(const char *fmt, ...)
{
#ifdef ENABLE_LOGGING
std::va_list args;
va_start(args, fmt);
Logv(fmt, args);
va_end(args);
#endif
}
void Logv(const char *fmt, std::va_list args)
{
#ifdef ENABLE_LOGGING
std::vfprintf(stderr, fmt, args);
#endif
}

16
dep/ggpo-x/src/log.h Normal file
View File

@ -0,0 +1,16 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _LOG_H
#define _LOG_H
#include <cstdarg>
extern void Log(const char *fmt, ...);
extern void Logv(const char *fmt, std::va_list list);
#endif

213
dep/ggpo-x/src/main.cpp Normal file
View File

@ -0,0 +1,213 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#include "types.h"
#include "backends/p2p.h"
#include "backends/synctest.h"
#include "backends/spectator.h"
#include "ggponet.h"
#if 0
BOOL WINAPI
DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
srand(Platform::GetCurrentTimeMS() + Platform::GetProcessID());
return TRUE;
}
#endif
void
ggpo_log(GGPOSession *ggpo, const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
ggpo_logv(ggpo, fmt, args);
va_end(args);
}
void
ggpo_logv(GGPOSession *ggpo, const char *fmt, va_list args)
{
if (ggpo) {
ggpo->Logv(fmt, args);
}
}
GGPOErrorCode
ggpo_start_session(GGPOSession **session,
GGPOSessionCallbacks *cb,
int num_players,
int input_size,
int maxPrediction)
{
*session= new Peer2PeerBackend(cb,
num_players,
input_size,
maxPrediction);
return GGPO_OK;
}
GGPOErrorCode
ggpo_add_player(GGPOSession *ggpo,
GGPOPlayer *player,
GGPOPlayerHandle *handle)
{
if (!ggpo) {
return GGPO_ERRORCODE_INVALID_SESSION;
}
return ggpo->AddPlayer(player, handle);
}
GGPOErrorCode
ggpo_start_synctest(GGPOSession **ggpo,
GGPOSessionCallbacks *cb,
int num_players,
int input_size,
int frames)
{
*ggpo = new SyncTestBackend(cb, frames, num_players);
return GGPO_OK;
}
GGPOErrorCode
ggpo_set_frame_delay(GGPOSession *ggpo,
GGPOPlayerHandle player,
int frame_delay)
{
if (!ggpo) {
return GGPO_ERRORCODE_INVALID_SESSION;
}
return ggpo->SetFrameDelay(player, frame_delay);
}
GGPOErrorCode
ggpo_idle(GGPOSession *ggpo)
{
if (!ggpo) {
return GGPO_ERRORCODE_INVALID_SESSION;
}
return ggpo->DoPoll();
}
GGPOErrorCode
ggpo_network_idle(GGPOSession* ggpo)
{
if (!ggpo) {
return GGPO_ERRORCODE_INVALID_SESSION;
}
return ggpo->NetworkIdle();
}
GGPOErrorCode
ggpo_add_local_input(GGPOSession *ggpo,
GGPOPlayerHandle player,
void *values,
int size)
{
if (!ggpo) {
return GGPO_ERRORCODE_INVALID_SESSION;
}
return ggpo->AddLocalInput(player, values, size);
}
GGPOErrorCode
ggpo_synchronize_input(GGPOSession *ggpo,
void *values,
int size,
int *disconnect_flags)
{
if (!ggpo) {
return GGPO_ERRORCODE_INVALID_SESSION;
}
return ggpo->SyncInput(values, size, disconnect_flags);
}
GGPOErrorCode ggpo_disconnect_player(GGPOSession *ggpo,
GGPOPlayerHandle player)
{
if (!ggpo) {
return GGPO_ERRORCODE_INVALID_SESSION;
}
return ggpo->DisconnectPlayer(player);
}
GGPOErrorCode
ggpo_advance_frame(GGPOSession *ggpo, uint16_t checksum)
{
if (!ggpo) {
return GGPO_ERRORCODE_INVALID_SESSION;
}
return ggpo->IncrementFrame(checksum);
}
GGPOErrorCode
ggpo_get_current_frame(GGPOSession *ggpo, int& nFrame)
{
if (!ggpo) {
return GGPO_ERRORCODE_INVALID_SESSION;
}
return ggpo->CurrentFrame(nFrame);
}
GGPOErrorCode
ggpo_get_network_stats(GGPOSession *ggpo,
GGPOPlayerHandle player,
GGPONetworkStats *stats)
{
if (!ggpo) {
return GGPO_ERRORCODE_INVALID_SESSION;
}
return ggpo->GetNetworkStats(stats, player);
}
GGPOErrorCode ggpo_handle_packet(GGPOSession* ggpo, ENetPeer* peer, const ENetPacket* pkt)
{
if (!ggpo)
return GGPO_ERRORCODE_INVALID_SESSION;
return ggpo->OnPacket(peer, pkt);
}
GGPOErrorCode
ggpo_close_session(GGPOSession *ggpo)
{
if (!ggpo) {
return GGPO_ERRORCODE_INVALID_SESSION;
}
delete ggpo;
return GGPO_OK;
}
GGPOErrorCode
ggpo_set_disconnect_timeout(GGPOSession *ggpo, int timeout)
{
if (!ggpo) {
return GGPO_ERRORCODE_INVALID_SESSION;
}
return ggpo->SetDisconnectTimeout(timeout);
}
GGPOErrorCode
ggpo_set_disconnect_notify_start(GGPOSession *ggpo, int timeout)
{
if (!ggpo) {
return GGPO_ERRORCODE_INVALID_SESSION;
}
return ggpo->SetDisconnectNotifyStart(timeout);
}
GGPOErrorCode ggpo_start_spectating(GGPOSession **session,
GGPOSessionCallbacks *cb,
int num_players,
int input_size,
ENetPeer* host)
{
*session = new SpectatorBackend(cb, num_players, input_size, host);
return GGPO_OK;
}

View File

@ -0,0 +1,105 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _UDP_MSG_H
#define _UDP_MSG_H
#define MAX_COMPRESSED_BITS 4096
#define UDP_MSG_MAX_PLAYERS 4
#pragma pack(push, 1)
struct UdpMsg
{
enum MsgType {
Invalid = 0,
SyncRequest = 1,
SyncReply = 2,
Input = 3,
QualityReport = 4,
QualityReply = 5,
InputAck = 6,
};
struct connect_status {
unsigned int disconnected:1;
int last_frame:31;
};
struct {
uint16 magic;
uint16 sequence_number;
uint8 type; /* packet type */
} hdr;
union {
struct {
uint32 random_request; /* please reply back with this random data */
uint16 remote_magic;
uint8 remote_endpoint;
uint8 remote_inputDelay;
} sync_request;
struct {
uint32 random_reply; /* OK, here's your random data back */
} sync_reply;
struct {
int8 frame_advantage; /* what's the other guy's frame advantage? */
uint32 ping;
} quality_report;
struct {
uint32 pong;
} quality_reply;
struct {
connect_status peer_connect_status[UDP_MSG_MAX_PLAYERS];
uint32 start_frame;
int ack_frame;
uint16 num_bits;
uint32 checksum32;
uint8 input_size; // XXX: shouldn't be in every single packet!
uint8 bits[MAX_COMPRESSED_BITS]; /* must be last */
} input;
struct {
int ack_frame;
} input_ack;
} u;
public:
int PacketSize() {
return sizeof(hdr) + PayloadSize();
}
int PayloadSize() {
int size;
switch (hdr.type) {
case SyncRequest: return sizeof(u.sync_request);
case SyncReply: return sizeof(u.sync_reply);
case QualityReport: return sizeof(u.quality_report);
case QualityReply: return sizeof(u.quality_reply);
case InputAck: return sizeof(u.input_ack);
case Input:
size = (int)((char *)&u.input.bits - (char *)&u.input);
size += (u.input.num_bits + 7) / 8;
return size;
}
ASSERT(false);
return 0;
}
UdpMsg(MsgType t) { hdr.type = (uint8)t; }
};
#pragma pack(pop)
#endif

View File

@ -0,0 +1,663 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#include "types.h"
#include "udp_proto.h"
#include "bitvector.h"
#include <algorithm>
#include <cmath>
#include <iostream>
static const int UDP_HEADER_SIZE = 28; /* Size of IP + UDP headers */
static const int NUM_SYNC_PACKETS = 1;
static const int SYNC_RETRY_INTERVAL = 2000;
static const int SYNC_FIRST_RETRY_INTERVAL = 500;
static const int RUNNING_RETRY_INTERVAL = 200;
static const int QUALITY_REPORT_INTERVAL = 333;
static const int NETWORK_STATS_INTERVAL = 500;
static const int MAX_SEQ_DISTANCE = (1 << 15);
static const uint8_t ENET_CHANNEL_ID = 1;
UdpProtocol::UdpProtocol() :
_local_frame_advantage(0),
_remote_frame_advantage(0),
_queue(-1),
_magic_number(0),
_remote_magic_number(0),
_packets_sent(0),
_bytes_sent(0),
_stats_start_time(0),
_last_send_time(0),
_connected(false),
_next_send_seq(0),
_next_recv_seq(0),
_peer(nullptr)
{
_last_sent_input.init(-1, NULL, 1);
_last_received_input.init(-1, NULL, 1);
_last_acked_input.init(-1, NULL, 1);
memset(&_state, 0, sizeof _state);
memset(_peer_connect_status, 0, sizeof(_peer_connect_status));
for (int i = 0; i < ARRAY_SIZE(_peer_connect_status); i++) {
_peer_connect_status[i].last_frame = -1;
}
}
UdpProtocol::~UdpProtocol()
{
}
void UdpProtocol::SetFrameDelay(int delay)
{
_timesync.SetFrameDelay(delay);
}
int UdpProtocol::RemoteFrameDelay()const
{
return _timesync._remoteFrameDelay;
}
void UdpProtocol::Init(ENetPeer* peer, int queue, UdpMsg::connect_status *status)
{
_peer = peer;
_queue = queue;
_local_connect_status = status;
do {
_magic_number = (uint16)rand();
} while (_magic_number == 0);
}
void
UdpProtocol::SendInput(GameInput &input)
{
// when spectating and you hit the end of the buffer its time to acknowledge that the peer is dead.
if (_queue >= 1000 && _pending_output.size() >= 63) {
Disconnect();
return;
}
if (_peer) {
if (_current_state == Running) {
/*
* Check to see if this is a good time to adjust for the rift...
*/
_timesync.advance_frame(input, _local_frame_advantage, _remote_frame_advantage);
/*
* Save this input packet
*
* XXX: This queue may fill up for spectators who do not ack input packets in a timely
* manner. When this happens, we can either resize the queue (ug) or disconnect them
* (better, but still ug). For the meantime, make this queue really big to decrease
* the odds of this happening...
*/
_pending_output.push(input);
}
SendPendingOutput();
}
}
void
UdpProtocol::SendPendingOutput()
{
UdpMsg *msg = new UdpMsg(UdpMsg::Input);
int i, j, offset = 0;
uint8 *bits;
GameInput last;
if (_pending_output.size()) {
last = _last_acked_input;
bits = msg->u.input.bits;
msg->u.input.start_frame = _pending_output.front().frame;
msg->u.input.input_size = (uint8)_pending_output.front().size;
ASSERT(last.frame == -1 || last.frame + 1 == msg->u.input.start_frame);
for (j = 0; j < _pending_output.size(); j++) {
GameInput &current = _pending_output.item(j);
msg->u.input.checksum32 = current.checksum;
if (memcmp(current.bits, last.bits, current.size) != 0) {
ASSERT((GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS * 8) < (1 << BITVECTOR_NIBBLE_SIZE));
for (i = 0; i < current.size * 8; i++) {
ASSERT(i < (1 << BITVECTOR_NIBBLE_SIZE));
if (current.value(i) != last.value(i)) {
BitVector_SetBit(msg->u.input.bits, &offset);
(current.value(i) ? BitVector_SetBit : BitVector_ClearBit)(bits, &offset);
BitVector_WriteNibblet(bits, i, &offset);
}
}
}
BitVector_ClearBit(msg->u.input.bits, &offset);
last = _last_sent_input = current;
}
} else {
msg->u.input.start_frame = 0;
msg->u.input.input_size = 0;
}
msg->u.input.ack_frame = _last_received_input.frame;
msg->u.input.num_bits = (uint16)offset;
if (_local_connect_status) {
memcpy(msg->u.input.peer_connect_status, _local_connect_status, sizeof(UdpMsg::connect_status) * UDP_MSG_MAX_PLAYERS);
} else {
memset(msg->u.input.peer_connect_status, 0, sizeof(UdpMsg::connect_status) * UDP_MSG_MAX_PLAYERS);
}
ASSERT(offset < MAX_COMPRESSED_BITS);
SendMsg(msg);
}
void
UdpProtocol::SendInputAck()
{
UdpMsg *msg = new UdpMsg(UdpMsg::InputAck);
msg->u.input_ack.ack_frame = _last_received_input.frame;
SendMsg(msg);
}
bool
UdpProtocol::GetEvent(UdpProtocol::Event &e)
{
if (_event_queue.size() == 0) {
return false;
}
e = _event_queue.front();
_event_queue.pop();
return true;
}
void UdpProtocol::ApplyToEvents(std::function<void(UdpProtocol::Event&)> cb)
{
StartPollLoop();
UdpProtocol::Event evt;
while (GetEvent(evt)) {
cb(evt);
}
EndPollLoop();
}
void UdpProtocol::StartPollLoop()
{
_remoteCheckSumsThisFrame.clear();
}
void UdpProtocol::EndPollLoop()
{
if (_remoteCheckSumsThisFrame.size())
_remoteCheckSums.emplace(*_remoteCheckSumsThisFrame.rbegin());
}
bool
UdpProtocol::NetworkIdle()
{
if (!_peer) {
return true;
}
unsigned int now = GGPOGetCurrentTimeMS();
unsigned int next_interval;
switch (_current_state) {
case Syncing:
next_interval = (_state.sync.roundtrips_remaining == NUM_SYNC_PACKETS) ? SYNC_FIRST_RETRY_INTERVAL : SYNC_RETRY_INTERVAL;
if (_last_send_time && _last_send_time + next_interval < now) {
Log("No luck syncing after %d ms... Re-queueing sync packet.\n", next_interval);
SendSyncRequest();
}
break;
case Running:
// xxx: rig all this up with a timer wrapper
if (!_state.running.last_input_packet_recv_time || _state.running.last_input_packet_recv_time + RUNNING_RETRY_INTERVAL < now) {
Log("Haven't exchanged packets in a while (last received:%d last sent:%d). Resending.\n", _last_received_input.frame, _last_sent_input.frame);
SendPendingOutput();
_state.running.last_input_packet_recv_time = now;
}
if (!_state.running.last_quality_report_time || _state.running.last_quality_report_time + QUALITY_REPORT_INTERVAL < now) {
UdpMsg *msg = new UdpMsg(UdpMsg::QualityReport);
msg->u.quality_report.ping = GGPOGetCurrentTimeMS();
// encode frame advantage into a byte by multiplying the float by 10, and croppeing to 255 - any frame advantage
// of 25 or more means catastrophe has already befallen us.
msg->u.quality_report.frame_advantage = (uint8)MIN(255.0f,(_timesync.LocalAdvantage()*10.f));
SendMsg(msg);
_state.running.last_quality_report_time = now;
}
if (!_state.running.last_network_stats_interval || _state.running.last_network_stats_interval + NETWORK_STATS_INTERVAL < now) {
UpdateNetworkStats();
_state.running.last_network_stats_interval = now;
}
break;
case Disconnected:
break;
}
return true;
}
void
UdpProtocol::Disconnect()
{
_current_state = Disconnected;
_peer = nullptr;
}
void
UdpProtocol::SendSyncRequest()
{
_state.sync.random = rand() & 0xFFFF;
UdpMsg *msg = new UdpMsg(UdpMsg::SyncRequest);
msg->u.sync_request.random_request = _state.sync.random;
msg->u.sync_request.remote_inputDelay = (uint8_t)_timesync._frameDelay2;
SendMsg(msg);
}
void
UdpProtocol::SendMsg(UdpMsg *msg)
{
const int size = msg->PacketSize();
LogMsg("send", msg);
_packets_sent++;
_last_send_time = GGPOGetCurrentTimeMS();
_bytes_sent += size;
msg->hdr.magic = _magic_number;
msg->hdr.sequence_number = _next_send_seq++;
ENetPacket* pkt = enet_packet_create(msg, size, 0);
enet_peer_send(_peer, ENET_CHANNEL_ID, pkt);
// TODO: flush packets?
// TODO: get rid of the extra heap allocation here...
delete msg;
}
void
UdpProtocol::OnMsg(UdpMsg *msg, int len)
{
bool handled = false;
typedef bool (UdpProtocol::*DispatchFn)(UdpMsg *msg, int len);
static const DispatchFn table[] = {
&UdpProtocol::OnInvalid, /* Invalid */
&UdpProtocol::OnSyncRequest, /* SyncRequest */
&UdpProtocol::OnSyncReply, /* SyncReply */
&UdpProtocol::OnInput, /* Input */
&UdpProtocol::OnQualityReport, /* QualityReport */
&UdpProtocol::OnQualityReply, /* QualityReply */
&UdpProtocol::OnInputAck, /* InputAck */
};
// filter out messages that don't match what we expect
uint16 seq = msg->hdr.sequence_number;
if (msg->hdr.type != UdpMsg::SyncRequest &&
msg->hdr.type != UdpMsg::SyncReply) {
if (msg->hdr.magic != _remote_magic_number) {
LogMsg("recv rejecting", msg);
return;
}
// filter out out-of-order packets
uint16 skipped = (uint16)((int)seq - (int)_next_recv_seq);
// Log("checking sequence number -> next - seq : %d - %d = %d\n", seq, _next_recv_seq, skipped);
if (skipped > MAX_SEQ_DISTANCE) {
Log("dropping out of order packet (seq: %d, last seq:%d)\n", seq, _next_recv_seq);
return;
}
}
_next_recv_seq = seq;
LogMsg("recv", msg);
if (msg->hdr.type >= ARRAY_SIZE(table)) {
OnInvalid(msg, len);
} else {
handled = (this->*(table[msg->hdr.type]))(msg, len);
}
if (handled) {
_last_recv_time = GGPOGetCurrentTimeMS();
}
}
void
UdpProtocol::UpdateNetworkStats(void)
{
int now = GGPOGetCurrentTimeMS();
if (_stats_start_time == 0) {
_stats_start_time = now;
}
int total_bytes_sent = _bytes_sent + (UDP_HEADER_SIZE * _packets_sent);
float seconds = (float)((now - _stats_start_time) / 1000.0);
float Bps = total_bytes_sent / seconds;
float udp_overhead = (float)(100.0 * (UDP_HEADER_SIZE * _packets_sent) / _bytes_sent);
_kbps_sent = int(Bps / 1024);
Log("Network Stats -- Bandwidth: %d KBps Packets Sent: %5d (%.2f pps) "
"KB Sent: %.2f UDP Overhead: %.2f pct.\n",
_kbps_sent,
_packets_sent,
(float)_packets_sent * 1000 / (now - _stats_start_time),
total_bytes_sent / 1024.0,
udp_overhead);
}
void
UdpProtocol::QueueEvent(const UdpProtocol::Event &evt)
{
LogEvent("Queuing event", evt);
_event_queue.push(evt);
}
void
UdpProtocol::Synchronize()
{
if (_peer) {
_current_state = Syncing;
_state.sync.roundtrips_remaining = NUM_SYNC_PACKETS;
SendSyncRequest();
}
}
bool
UdpProtocol::GetPeerConnectStatus(int id, int *frame)
{
*frame = _peer_connect_status[id].last_frame;
return !_peer_connect_status[id].disconnected;
}
void
UdpProtocol::Log(const char *fmt, ...)
{
char buf[1024];
size_t offset;
va_list args;
std::snprintf(buf, ARRAY_SIZE(buf), "udpproto%d | ", _queue);
offset = strlen(buf);
va_start(args, fmt);
vsnprintf(buf + offset, ARRAY_SIZE(buf) - offset - 1, fmt, args);
buf[ARRAY_SIZE(buf)-1] = '\0';
::Log("%s", buf);
va_end(args);
}
void
UdpProtocol::LogMsg(const char *prefix, UdpMsg *msg)
{
switch (msg->hdr.type) {
case UdpMsg::SyncRequest:
Log("%s sync-request (%d).\n", prefix,
msg->u.sync_request.random_request);
break;
case UdpMsg::SyncReply:
Log("%s sync-reply (%d).\n", prefix,
msg->u.sync_reply.random_reply);
break;
case UdpMsg::QualityReport:
Log("%s quality report.\n", prefix);
break;
case UdpMsg::QualityReply:
Log("%s quality reply.\n", prefix);
break;
case UdpMsg::Input:
Log("%s game-compressed-input %d (+ %d bits).\n", prefix, msg->u.input.start_frame, msg->u.input.num_bits);
break;
case UdpMsg::InputAck:
Log("%s input ack.\n", prefix);
break;
default:
Log("Unknown UdpMsg type.");
}
}
void
UdpProtocol::LogEvent(const char *prefix, const UdpProtocol::Event &evt)
{
switch (evt.type) {
case UdpProtocol::Event::Synchronzied:
Log("%s (event: Synchronzied).\n", prefix);
break;
}
}
bool
UdpProtocol::OnInvalid(UdpMsg *msg, int len)
{
// ASSERT(FALSE && "Invalid msg in UdpProtocol");
return false;
}
bool
UdpProtocol::OnSyncRequest(UdpMsg *msg, int len)
{
if (_remote_magic_number != 0 && msg->hdr.magic != _remote_magic_number) {
Log("Ignoring sync request from unknown endpoint (%d != %d).\n",
msg->hdr.magic, _remote_magic_number);
return false;
}
UdpMsg *reply = new UdpMsg(UdpMsg::SyncReply);
reply->u.sync_reply.random_reply = msg->u.sync_request.random_request;
_timesync._remoteFrameDelay = msg->u.sync_request.remote_inputDelay;
SendMsg(reply);
return true;
}
bool
UdpProtocol::OnSyncReply(UdpMsg *msg, int len)
{
if (_current_state != Syncing) {
Log("Ignoring SyncReply while not synching.\n");
return msg->hdr.magic == _remote_magic_number;
}
if (msg->u.sync_reply.random_reply != _state.sync.random) {
Log("sync reply %d != %d. Keep looking...\n",
msg->u.sync_reply.random_reply, _state.sync.random);
return false;
}
if (!_connected) {
QueueEvent(Event(Event::Connected));
_connected = true;
}
Log("Checking sync state (%d round trips remaining).\n", _state.sync.roundtrips_remaining);
if (--_state.sync.roundtrips_remaining == 0) {
Log("Synchronized!\n");
QueueEvent(UdpProtocol::Event(UdpProtocol::Event::Synchronzied));
_current_state = Running;
_last_received_input.frame = -1;
_remote_magic_number = msg->hdr.magic;
} else {
UdpProtocol::Event evt(UdpProtocol::Event::Synchronizing);
evt.u.synchronizing.total = NUM_SYNC_PACKETS;
evt.u.synchronizing.count = NUM_SYNC_PACKETS - _state.sync.roundtrips_remaining;
QueueEvent(evt);
SendSyncRequest();
}
return true;
}
bool
UdpProtocol::OnInput(UdpMsg *msg, int len)
{
/*
* Update the peer connection status if this peer is still considered to be part
* of the network.
*/
UdpMsg::connect_status* remote_status = msg->u.input.peer_connect_status;
for (int i = 0; i < ARRAY_SIZE(_peer_connect_status); i++) {
ASSERT(remote_status[i].last_frame >= _peer_connect_status[i].last_frame);
_peer_connect_status[i].disconnected = _peer_connect_status[i].disconnected || remote_status[i].disconnected;
_peer_connect_status[i].last_frame = MAX(_peer_connect_status[i].last_frame, remote_status[i].last_frame);
}
/*
* Decompress the input.
*/
int last_received_frame_number = _last_received_input.frame;
if (msg->u.input.num_bits) {
int offset = 0;
uint8 *bits = (uint8 *)msg->u.input.bits;
int numBits = msg->u.input.num_bits;
int currentFrame = msg->u.input.start_frame;
_last_received_input.size = msg->u.input.input_size;
if (_last_received_input.frame < 0) {
_last_received_input.frame = msg->u.input.start_frame - 1;
}
while (offset < numBits) {
/*
* Keep walking through the frames (parsing bits) until we reach
* the inputs for the frame right after the one we're on.
*/
ASSERT(currentFrame <= (_last_received_input.frame + 1));
bool useInputs = currentFrame == _last_received_input.frame + 1;
while (BitVector_ReadBit(bits, &offset)) {
int on = BitVector_ReadBit(bits, &offset);
int button = BitVector_ReadNibblet(bits, &offset);
if (useInputs) {
if (on) {
_last_received_input.set(button);
} else {
_last_received_input.clear(button);
}
}
}
ASSERT(offset <= numBits);
/*
* Now if we want to use these inputs, go ahead and send them to
* the emulator.
*/
if (useInputs) {
/*
* Move forward 1 frame in the stream.
*/
char desc[1024];
ASSERT(currentFrame == _last_received_input.frame + 1);
_last_received_input.frame = currentFrame;
_last_received_input.checksum = msg->u.input.checksum32;
/*
* Send the event to the emualtor
*/
UdpProtocol::Event evt(UdpProtocol::Event::Input);
evt.u.input.input = _last_received_input;
_last_received_input.desc(desc, ARRAY_SIZE(desc));
_state.running.last_input_packet_recv_time = GGPOGetCurrentTimeMS();
Log("Sending frame %d to emu queue %d, (%s).\n", _last_received_input.frame, _queue,desc);
QueueEvent(evt);
} else {
Log("Skipping past frame:(%d) current is %d.\n", currentFrame, _last_received_input.frame);
}
/*
* Move forward 1 frame in the input stream.
*/
currentFrame++;
}
}
ASSERT(_last_received_input.frame >= last_received_frame_number);
/*
* Get rid of our buffered input
*/
while (_pending_output.size() && _pending_output.front().frame < msg->u.input.ack_frame) {
Log("Throwing away pending output frame %d\n", _pending_output.front().frame);
_last_acked_input = _pending_output.front();
_pending_output.pop();
}
return true;
}
bool
UdpProtocol::OnInputAck(UdpMsg *msg, int len)
{
/*
* Get rid of our buffered input
*/
while (_pending_output.size() && _pending_output.front().frame < msg->u.input_ack.ack_frame) {
Log("Throwing away pending output frame %d\n", _pending_output.front().frame);
_last_acked_input = _pending_output.front();
_pending_output.pop();
}
return true;
}
bool
UdpProtocol::OnQualityReport(UdpMsg *msg, int len)
{
// send a reply so the other side can compute the round trip transmit time.
UdpMsg *reply = new UdpMsg(UdpMsg::QualityReply);
reply->u.quality_reply.pong = msg->u.quality_report.ping;
SendMsg(reply);
_remote_frame_advantage = (float)(msg->u.quality_report.frame_advantage/10.f);
return true;
}
bool
UdpProtocol::OnQualityReply(UdpMsg *msg, int len)
{
_round_trip_time = GGPOGetCurrentTimeMS() - msg->u.quality_reply.pong;
return true;
}
void
UdpProtocol::GetNetworkStats(struct GGPONetworkStats *s)
{
s->network.ping = _round_trip_time;
s->network.send_queue_len = _pending_output.size();
s->network.kbps_sent = _kbps_sent;
s->timesync.remote_frames_behind = _timesync.RemoteAdvantage();
s->timesync.local_frames_behind = _timesync.LocalAdvantage();
s->timesync.avg_local_frames_behind = _timesync.AvgLocalAdvantageSinceStart();
s->timesync.avg_remote_frames_behind = _timesync.AvgRemoteAdvantageSinceStart();
}
void
UdpProtocol::SetLocalFrameNumber(int localFrame)
{
/*
* Estimate which frame the other guy is one by looking at the
* last frame they gave us plus some delta for the one-way packet
* trip time.
*/
float remoteFrame = _last_received_input.frame + (_round_trip_time * 60.f / 2000);
/*
* Our frame advantage is how many frames *behind* the other guy
* we are. Counter-intuative, I know. It's an advantage because
* it means they'll have to predict more often and our moves will
* pop more frequenetly.
*/
_local_frame_advantage = (float)(remoteFrame - localFrame)- _timesync._frameDelay2;
}
float
UdpProtocol::RecommendFrameDelay()
{
// XXX: require idle input should be a configuration parameter
return _timesync.recommend_frame_wait_duration(false);
}

View File

@ -0,0 +1,191 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _UDP_PROTO_H_
#define _UDP_PROTO_H_
#include "enet/enet.h"
#include "udp_msg.h"
#include "game_input.h"
#include "timesync.h"
#include "ggponet.h"
#include "ring_buffer.h"
#include <functional>
#include <vector>
#include <string>
#include <map>
class UdpProtocol
{
public:
struct Stats {
int ping;
float remote_frame_advantage;
float local_frame_advantage;
float av_remote_frame_advantage;
float av_local_frame_advantage;
int send_queue_len;
};
struct Event {
enum Type {
Unknown = -1,
Connected,
Synchronizing,
Synchronzied,
Input,
};
Type type;
union {
struct {
GameInput input;
} input;
struct {
int total;
int count;
} synchronizing;
struct {
int disconnect_timeout;
} network_interrupted;
} u;
Event(Type t = Unknown) : type(t) { }
};
public:
bool NetworkIdle();
public:
UdpProtocol();
~UdpProtocol();
ENetPeer* GetENetPeer() const { return _peer; }
void Init(ENetPeer* peer, int queue, UdpMsg::connect_status *status);
void Synchronize();
bool GetPeerConnectStatus(int id, int *frame);
bool IsInitialized() { return _peer != nullptr; }
bool IsSynchronized() { return _current_state == Running; }
bool IsRunning() { return _current_state == Running; }
void SendInput(GameInput &input);
void SendInputAck();
void OnMsg(UdpMsg *msg, int len);
void Disconnect();
void GetNetworkStats(struct GGPONetworkStats *stats);
bool GetEvent(UdpProtocol::Event &e);
void SetLocalFrameNumber(int num);
float RecommendFrameDelay();
int RemoteFrameDelay()const;
void SetFrameDelay(int delay);
void ApplyToEvents(std::function<void(UdpProtocol::Event&)> cb);
void StartPollLoop();
void EndPollLoop();
std::map<int, uint32> _remoteCheckSums;
std::map<int, uint32> _remoteCheckSumsThisFrame;
protected:
enum State {
Syncing,
Synchronzied,
Running,
Disconnected
};
struct QueueEntry {
int queue_time;
sockaddr_in dest_addr;
UdpMsg *msg;
QueueEntry() {}
QueueEntry(int time, sockaddr_in &dst, UdpMsg *m) : queue_time(time), dest_addr(dst), msg(m) { }
};
void UpdateNetworkStats(void);
void QueueEvent(const UdpProtocol::Event &evt);
void Log(const char *fmt, ...);
void LogMsg(const char *prefix, UdpMsg *msg);
void LogEvent(const char *prefix, const UdpProtocol::Event &evt);
void SendSyncRequest();
void SendMsg(UdpMsg *msg);
void SendPendingOutput();
bool OnInvalid(UdpMsg *msg, int len);
bool OnSyncRequest(UdpMsg *msg, int len);
bool OnSyncReply(UdpMsg *msg, int len);
bool OnInput(UdpMsg *msg, int len);
bool OnInputAck(UdpMsg *msg, int len);
bool OnQualityReport(UdpMsg *msg, int len);
bool OnQualityReply(UdpMsg *msg, int len);
protected:
/*
* Network transmission information
*/
ENetPeer *_peer;
uint16 _magic_number;
int _queue;
uint16 _remote_magic_number;
bool _connected;
/*
* Stats
*/
int _round_trip_time = 0;
int _packets_sent;
int _bytes_sent;
int _kbps_sent;
int _stats_start_time;
/*
* The state machine
*/
UdpMsg::connect_status *_local_connect_status;
UdpMsg::connect_status _peer_connect_status[UDP_MSG_MAX_PLAYERS];
State _current_state;
union {
struct {
uint32 roundtrips_remaining;
uint32 random;
} sync;
struct {
uint32 last_quality_report_time;
uint32 last_network_stats_interval;
uint32 last_input_packet_recv_time;
} running;
} _state;
/*
* Fairness.
*/
float _local_frame_advantage;
float _remote_frame_advantage;
/*
* Packet loss...
*/
RingBuffer<GameInput, 64> _pending_output;
GameInput _last_received_input;
GameInput _last_sent_input;
GameInput _last_acked_input;
unsigned int _last_send_time;
unsigned int _last_recv_time;
uint16 _next_send_seq;
uint16 _next_recv_seq;
/*
* Rift synchronization.
*/
TimeSync _timesync;
/*
* Event queue
*/
RingBuffer<UdpProtocol::Event, 64> _event_queue;
};
#endif

View File

@ -0,0 +1,71 @@
#include "platform.h"
#include <cstdio>
#include <cstdint>
#ifdef _WIN32
#define NOMINMAX 1
#define WIN32_LEAN_AND_MEAN 1
#include <Windows.h>
#else
#include <time.h>
#include <unistd.h>
#endif
void GGPOAssertFailed(const char* expr, const char* filename, int line)
{
#ifdef _WIN32
const uint32 pid = GetCurrentProcessId();
#else
const uint32 pid = getpid();
#endif
char assert_buf[1024];
std::snprintf(assert_buf, sizeof(assert_buf), "Assertion: %s @ %s:%d (pid:%d)", expr, __FILE__, __LINE__, pid);
#ifdef _WIN32
MessageBoxA(nullptr, assert_buf, "Assertion Failed", MB_ICONERROR | MB_OK);
#else
std::fprintf(stderr, "\n%s\n", assert_buf);
#endif
}
uint32 GGPOGetCurrentTimeMS()
{
#ifdef _WIN32
static std::uint64_t freq;
static std::uint64_t start_time;
static bool start_time_initialized = false;
if (!start_time_initialized)
{
start_time_initialized = true;
LARGE_INTEGER pfreq = {};
QueryPerformanceFrequency(&pfreq);
freq = pfreq.QuadPart / 1000u;
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&start_time));
}
std::uint64_t current_time;
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&current_time));
return (current_time - start_time) / freq;
#else
static constexpr auto get_current_time_ns = []() {
struct timespec tv;
clock_gettime(CLOCK_MONOTONIC, &tv);
return (static_cast<std::uint64_t>(tv.tv_nsec) + static_cast<std::uint64_t>(tv.tv_sec) * 1000000000);
};
static std::uint64_t start_time;
static bool start_time_initialized = false;
if (!start_time_initialized)
{
start_time_initialized = true;
start_time = get_current_time_ns();
}
const std::uint64_t current_time = get_current_time_ns();
return (current_time - start_time) / 1000000;
#endif
}

View File

@ -0,0 +1,6 @@
#pragma once
#include "types.h"
void GGPOAssertFailed(const char* expr, const char* filename, int line);
uint32 GGPOGetCurrentTimeMS();

View File

@ -0,0 +1,23 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#include "platform_linux.h"
struct timespec start = { 0 }
uint32 Platform::GetCurrentTimeMS() {
if (start.tv_sec == 0 && start.tv_nsec == 0) {
clock_gettime(CLOCK_MONOTONIC, &start);
return 0
}
struct timespec current;
clock_gettime(CLOCK_MONOTONIC, &current);
return ((current.tv_sec - start.tv_sec) * 1000) +
((current.tv_nsec - start.tv_nsec ) / 1000000) +
}

View File

@ -0,0 +1,27 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _GGPO_LINUX_H_
#define _GGPO_LINUX_H_
#include <stdio.h>
#include <stdarg.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
class Platform {
public: // types
typedef pid_t ProcessID;
public: // functions
static ProcessID GetProcessID() { return getpid(); }
static void AssertFailed(char *msg) { }
static uint32 GetCurrentTimeMS();
};
#endif

View File

@ -0,0 +1,64 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _RING_BUFFER_H
#define _RING_BUFFER_H
#include "types.h"
template<class T, int N> class RingBuffer
{
public:
RingBuffer<T, N>() :
_head(0),
_tail(0),
_size(0) {
}
T &front() {
ASSERT(_size != N);
return _elements[_tail];
}
T &item(int i) {
ASSERT(i < _size);
return _elements[(_tail + i) % N];
}
const T& item(int i) const {
ASSERT(i < _size);
return _elements[(_tail + i) % N];
}
void pop() {
ASSERT(_size != N);
_tail = (_tail + 1) % N;
_size--;
}
void push(const T &t) {
ASSERT(_size != (N-1));
_elements[_head] = t;
_head = (_head + 1) % N;
_size++;
}
int size() const {
return _size;
}
bool empty() {
return _size == 0;
}
protected:
T _elements[N];
int _head;
int _tail;
int _size;
};
#endif

View File

@ -0,0 +1,40 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _STATIC_BUFFER_H
#define _STATIC_BUFFER_H
#include <types.h>
template<class T, int N> class StaticBuffer
{
public:
StaticBuffer<T, N>() :
_size(0) {
}
T& operator[](int i) {
ASSERT(i >= 0 && i < _size);
return _elements[i];
}
void push_back(const T &t) {
ASSERT(_size != (N-1));
_elements[_size++] = t;
}
int size() {
return _size;
}
protected:
T _elements[N];
int _size;
};
#endif

306
dep/ggpo-x/src/sync.cpp Normal file
View File

@ -0,0 +1,306 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#include "sync.h"
#include <cstdlib>
Sync::Sync(UdpMsg::connect_status *connect_status, int maxPrediction) :
_local_connect_status(connect_status),
_input_queues(NULL),
_savedstate(maxPrediction)
{
_framecount = 0;
_last_confirmed_frame = -1;
_max_prediction_frames = 0;
}
Sync::~Sync()
{
/*
* Delete frames manually here rather than in a destructor of the SavedFrame
* structure so we can efficently copy frames via weak references.
*/
for (int i = 0; i < _savedstate.frames.size(); i++) {
_callbacks.free_buffer(_callbacks.context, _savedstate.frames[i].buf, _savedstate.frames[i].frame);
}
delete [] _input_queues;
_input_queues = NULL;
}
void
Sync::Init(Sync::Config &config)
{
_config = config;
_callbacks = config.callbacks;
_framecount = 0;
_rollingback = false;
_max_prediction_frames = config.num_prediction_frames;
CreateQueues();
}
void
Sync::SetLastConfirmedFrame(int frame)
{
_last_confirmed_frame = frame;
if (_last_confirmed_frame > 0) {
for (int i = 0; i < _config.num_players; i++) {
_input_queues[i].DiscardConfirmedFrames(frame - 1);
}
}
}
bool
Sync::AddLocalInput(int queue, GameInput &input)
{
int frames_behind = _framecount - _last_confirmed_frame;
if (_framecount >= _max_prediction_frames && frames_behind >= _max_prediction_frames) {
Log("Rejecting input from emulator: reached prediction barrier.\n");
return false;
}
if (_framecount == 0) {
SaveCurrentFrame();
}
Log("Sending undelayed local frame %d to queue %d.\n", _framecount, queue);
input.frame = _framecount;
_input_queues[queue].AddInput(input);
return true;
}
void
Sync::AddRemoteInput(int queue, GameInput &input)
{
_input_queues[queue].AddInput(input);
}
int
Sync::GetConfirmedInputs(void *values, int size, int frame)
{
int disconnect_flags = 0;
char *output = (char *)values;
ASSERT(size >= _config.num_players * _config.input_size);
memset(output, 0, size);
for (int i = 0; i < _config.num_players; i++) {
GameInput input;
if (_local_connect_status[i].disconnected && frame > _local_connect_status[i].last_frame) {
disconnect_flags |= (1 << i);
input.erase();
} else {
_input_queues[i].GetConfirmedInput(frame, &input);
}
memcpy(output + (i * _config.input_size), input.bits, _config.input_size);
}
return disconnect_flags;
}
int
Sync::SynchronizeInputs(void *values, int size)
{
int disconnect_flags = 0;
char *output = (char *)values;
ASSERT(size >= _config.num_players * _config.input_size);
memset(output, 0, size);
for (int i = 0; i < _config.num_players; i++) {
GameInput input;
if (_local_connect_status[i].disconnected && _framecount > _local_connect_status[i].last_frame) {
disconnect_flags |= (1 << i);
input.erase();
} else {
_input_queues[i].GetInput(_framecount, &input);
}
memcpy(output + (i * _config.input_size), input.bits, _config.input_size);
}
return disconnect_flags;
}
void
Sync::CheckSimulation()
{
int seek_to;
if (!CheckSimulationConsistency(&seek_to)) {
AdjustSimulation(seek_to);
}
}
void
Sync::IncrementFrame(void)
{
_framecount++;
SaveCurrentFrame();
}
void
Sync::AdjustSimulation(int seek_to)
{
int framecount = _framecount;
int count = _framecount - seek_to;
Log("Catching up\n");
_rollingback = true;
/*
* Flush our input queue and load the last frame.
*/
LoadFrame(seek_to, count);
ASSERT(_framecount == seek_to);
/*
* Advance frame by frame (stuffing notifications back to
* the master).
*/
ResetPrediction(_framecount);
for (int i = 0; i < count; i++) {
_callbacks.advance_frame(_callbacks.context, 0);
}
ASSERT(_framecount == framecount);
_rollingback = false;
Log("---\n");
}
void
Sync::LoadFrame(int frame, int framesToRollback)
{
// find the frame in question
if (frame == _framecount) {
Log("Skipping NOP.\n");
return;
}
// Move the head pointer back and load it up
_savedstate.head = FindSavedFrameIndex(frame);
SavedFrame *state = &_savedstate.frames[_savedstate.head];
Log("=== Loading frame info %d (size: %d checksum: %08x).\n",
state->frame, state->cbuf, state->checksum);
ASSERT(state->buf && state->cbuf);
_callbacks.load_game_state(_callbacks.context, state->buf, state->cbuf, framesToRollback, state->frame);
// Reset framecount and the head of the state ring-buffer to point in
// advance of the current frame (as if we had just finished executing it).
_framecount = state->frame;
_savedstate.head = (_savedstate.head + 1) % _savedstate.frames.size();
}
void
Sync::SaveCurrentFrame()
{
/*
* See StateCompress for the real save feature implemented by FinalBurn.
* Write everything into the head, then advance the head pointer.
*/
SavedFrame *state = &_savedstate.frames[_savedstate.head];
if (state->buf) {
_callbacks.free_buffer(_callbacks.context, state->buf, state->frame);
state->buf = NULL;
}
state->frame = _framecount;
_callbacks.save_game_state(_callbacks.context, &state->buf, &state->cbuf, &state->checksum, state->frame);
Log("=== Saved frame info %d (size: %d checksum: %08x).\n", state->frame, state->cbuf, state->checksum);
_savedstate.head = (_savedstate.head + 1) % (int)_savedstate.frames.size();
}
Sync::SavedFrame&
Sync::GetLastSavedFrame()
{
int i = _savedstate.head - 1;
if (i < 0) {
i = (int)_savedstate.frames.size() - 1;
}
return _savedstate.frames[i];
}
int
Sync::FindSavedFrameIndex(int frame)
{
int i, count = (int)_savedstate.frames.size();
for (i = 0; i < count; i++) {
if (_savedstate.frames[i].frame == frame) {
break;
}
}
if (i == count) {
abort();
}
return i;
}
bool
Sync::CreateQueues()
{
delete [] _input_queues;
_input_queues = new InputQueue[_config.num_players];
for (int i = 0; i < _config.num_players; i++) {
_input_queues[i].Init(i, _config.input_size);
}
return true;
}
bool
Sync::CheckSimulationConsistency(int *seekTo)
{
int first_incorrect = GameInput::NullFrame;
for (int i = 0; i < _config.num_players; i++) {
int incorrect = _input_queues[i].GetFirstIncorrectFrame();
Log("considering incorrect frame %d reported by queue %d.\n", incorrect, i);
if (incorrect != GameInput::NullFrame && (first_incorrect == GameInput::NullFrame || incorrect < first_incorrect)) {
first_incorrect = incorrect;
}
}
if (first_incorrect == GameInput::NullFrame) {
Log("prediction ok. proceeding.\n");
return true;
}
*seekTo = first_incorrect;
return false;
}
void
Sync::SetFrameDelay(int queue, int delay)
{
_input_queues[queue].SetFrameDelay(delay);
}
void
Sync::ResetPrediction(int frameNumber)
{
for (int i = 0; i < _config.num_players; i++) {
_input_queues[i].ResetPrediction(frameNumber);
}
}
bool
Sync::GetEvent(Event &e)
{
if (_event_queue.size()) {
e = _event_queue.front();
_event_queue.pop();
return true;
}
return false;
}

110
dep/ggpo-x/src/sync.h Normal file
View File

@ -0,0 +1,110 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _SYNC_H
#define _SYNC_H
#include "types.h"
#include "ggponet.h"
#include "game_input.h"
#include "input_queue.h"
#include "ring_buffer.h"
#include "network/udp_msg.h"
#include <vector>
#define MAX_PREDICTION_FRAMES 8
class SyncTestBackend;
class Sync {
public:
struct Config {
GGPOSessionCallbacks callbacks;
int num_prediction_frames;
int num_players;
int input_size;
};
struct Event {
enum {
ConfirmedInput,
} type;
union {
struct {
GameInput input;
} confirmedInput;
} u;
};
public:
Sync(UdpMsg::connect_status* connect_status, int maxPrediction);
virtual ~Sync();
void Init(Config& config);
void SetLastConfirmedFrame(int frame);
void SetFrameDelay(int queue, int delay);
bool AddLocalInput(int queue, GameInput& input);
void AddRemoteInput(int queue, GameInput& input);
int GetConfirmedInputs(void* values, int size, int frame);
int SynchronizeInputs(void* values, int size);
void CheckSimulation();
void AdjustSimulation(int seek_to);
void IncrementFrame(void);
int GetFrameCount() { return _framecount; }
bool InRollback() { return _rollingback; }
bool GetEvent(Event& e);
int MaxPredictionFrames() const { return _max_prediction_frames; }
protected:
friend class SyncTestBackend;
friend class Peer2PeerBackend;
struct SavedFrame {
byte* buf;
int cbuf;
int frame;
int checksum;
SavedFrame() : buf(NULL), cbuf(0), frame(-1), checksum(0) { }
};
struct SavedState {
SavedState(int max_prediction) {
frames.resize(max_prediction + 2);
head = 0;
}
std::vector<SavedFrame> frames;// [MAX_PREDICTION_FRAMES + 2] ;
int head;
};
void LoadFrame(int frame, int framesToRollback);
void SaveCurrentFrame();
int FindSavedFrameIndex(int frame);
SavedFrame& GetLastSavedFrame();
bool CreateQueues();
bool CheckSimulationConsistency(int* seekTo);
void ResetPrediction(int frameNumber);
protected:
GGPOSessionCallbacks _callbacks;
SavedState _savedstate;
Config _config;
bool _rollingback;
int _last_confirmed_frame;
int _framecount;
int _max_prediction_frames;
InputQueue *_input_queues;
RingBuffer<Event, 32> _event_queue;
UdpMsg::connect_status *_local_connect_status;
};
#endif

117
dep/ggpo-x/src/timesync.cpp Normal file
View File

@ -0,0 +1,117 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#include "timesync.h"
#include <string>
TimeSync::TimeSync()
{
memset(_local, 0, sizeof(_local));
memset(_remote, 0, sizeof(_remote));
_next_prediction = FRAME_WINDOW_SIZE * 3;
}
TimeSync::~TimeSync()
{
}
void TimeSync::SetFrameDelay(int frame)
{
_frameDelay2 = frame;
}
void
TimeSync::advance_frame(GameInput &input, float advantage, float radvantage)
{
// Remember the last frame and frame advantage
_last_inputs[input.frame % ARRAY_SIZE(_last_inputs)] = input;
_local[input.frame % ARRAY_SIZE(_local)] = advantage;
_remote[input.frame % ARRAY_SIZE(_remote)] = radvantage;
_avgLocal = ((nFrame * _avgLocal) + advantage) / (nFrame + 1);
_avgRemote = ((nFrame * _avgRemote) + radvantage) / (nFrame + 1);
nFrame++;
//Clear after first 3 seconds, as this is a bit crazy
if (!clearedInitial && nFrame == 240)
{
clearedInitial = true;
nFrame = 0;
}
}
float TimeSync::LocalAdvantage() const
{
int i ;
float advantage=0;
for (i = 0; i < ARRAY_SIZE(_local); i++) {
advantage += _local[i];
}
advantage /=(float)ARRAY_SIZE(_local);
return (advantage);
}
float TimeSync::RemoteAdvantage() const
{
int i;
float advantage = 0;;
for (i = 0; i < ARRAY_SIZE(_local); i++) {
advantage += _remote[i];
}
advantage /= (float)ARRAY_SIZE(_local);
return (advantage);
}
float
TimeSync::recommend_frame_wait_duration(bool require_idle_input)
{
auto advantage = LocalAdvantage();
auto radvantage = RemoteAdvantage();
// See if someone should take action. The person furthest ahead
// needs to slow down so the other user can catch up.
// Only do this if both clients agree on who's ahead!!
// if (advantage >= radvantage) {
// return 0;
// }
float sleep_frames = (((radvantage - advantage) / 2.0f));
// Both clients agree that we're the one ahead. Split
// the difference between the two to figure out how long to
// sleep for.
/* char logMessage[256];
sprintf_s<256>(logMessage, "Local Adv: %.2f, remoate adv %.2f", advantage, radvantage);
OutputDebugString(logMessage);
sprintf_s<256>(logMessage, ": Sleep for %.2f frames\n", sleep_frames);
OutputDebugString(logMessage);
Log("iteration %d: sleep frames is %d\n", count, sleep_frames);*/
// Some things just aren't worth correcting for. Make sure
// the difference is relevant before proceeding.
// if (sleep_frames < 0.2f){//{ MIN_FRAME_ADVANTAGE) {
// return 0;
// }
// Make sure our input had been "idle enough" before recommending
// a sleep. This tries to make the emulator sleep while the
// user's input isn't sweeping in arcs (e.g. fireball motions in
// Street Fighter), which could cause the player to miss moves.
//if (require_idle_input) {
// for (i = 1; i < ARRAY_SIZE(_last_inputs); i++) {
// if (!_last_inputs[i].equal(_last_inputs[0], true)) {
// Log("iteration %d: rejecting due to input stuff at position %d...!!!\n", count, i);
// return 0;
// }
// }
//}
//require_idle_input;
// Success!!! Recommend the number of frames to sleep and adjust
return sleep_frames > 0 ? (float)MIN(sleep_frames, MAX_FRAME_ADVANTAGE) : (float)MAX(sleep_frames, -MAX_FRAME_ADVANTAGE);
}

44
dep/ggpo-x/src/timesync.h Normal file
View File

@ -0,0 +1,44 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _TIMESYNC_H
#define _TIMESYNC_H
#include "types.h"
#include "game_input.h"
#define FRAME_WINDOW_SIZE 120
#define MIN_UNIQUE_FRAMES 10
#define MIN_FRAME_ADVANTAGE 3
#define MAX_FRAME_ADVANTAGE 9
class TimeSync {
public:
TimeSync();
virtual ~TimeSync ();
void advance_frame(GameInput &input, float advantage, float radvantage);
float recommend_frame_wait_duration(bool require_idle_input);
float LocalAdvantage() const;
float RemoteAdvantage() const;
float AvgLocalAdvantageSinceStart() const { return _avgLocal; }
float AvgRemoteAdvantageSinceStart() const { return _avgRemote; }
void SetFrameDelay(int frame);
int _frameDelay2 ;
int _remoteFrameDelay = 0;;
protected:
float _local[FRAME_WINDOW_SIZE];
float _remote[FRAME_WINDOW_SIZE];
GameInput _last_inputs[MIN_UNIQUE_FRAMES];
int _next_prediction;
int nFrame=0;
float _avgLocal = 0;
float _avgRemote = 0;
bool clearedInitial = false;
};
#endif

75
dep/ggpo-x/src/types.h Normal file
View File

@ -0,0 +1,75 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#ifndef _TYPES_H
#define _TYPES_H
/*
* Keep the compiler happy
*/
/*
* Disable specific compiler warnings
* 4018 - '<' : signed/unsigned mismatch
* 4100 - 'xxx' : unreferenced formal parameter
* 4127 - conditional expression is constant
* 4201 - nonstandard extension used : nameless struct/union
* 4389 - '!=' : signed/unsigned mismatch
* 4800 - 'int' : forcing value to bool 'true' or 'false' (performance warning)
*/
#ifdef _MSC_VER
#pragma warning(disable: 4018 4100 4127 4201 4389 4800)
#endif
/*
* Simple types
*/
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;
typedef unsigned char byte;
typedef char int8;
typedef short int16;
typedef int int32;
#include <cstdint>
#include "platform.h"
#include "log.h"
/*
* Macros
*/
#define ASSERT(x) \
do \
{ \
if (!(x)) \
{ \
GGPOAssertFailed(#x, __FILE__, __LINE__); \
} \
} while (false)
#ifndef ARRAY_SIZE
# define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
#endif
#ifndef MAX_INT
# define MAX_INT 0xEFFFFFF
#endif
#ifndef MAX
# define MAX(x, y) (((x) > (y)) ? (x) : (y))
#endif
#ifndef MIN
# define MIN(x, y) (((x) < (y)) ? (x) : (y))
#endif
#ifndef BIGGEST
# define BIGGEST(x, y) (((abs(x)) > (abs(y))) ? (x) : (y))
#endif
#endif // _TYPES_H

View File

@ -75,6 +75,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "reshadefx", "dep\reshadefx\
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "enet", "dep\enet\enet.vcxproj", "{460A096B-FCC7-465C-8E4B-434AF490A9EA}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ggpo-x", "dep\ggpo-x\ggpo-x.vcxproj", "{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@ -1627,6 +1629,54 @@ Global
{460A096B-FCC7-465C-8E4B-434AF490A9EA}.ReleaseLTCG-Clang|x64.Build.0 = ReleaseLTCG-Clang|x64
{460A096B-FCC7-465C-8E4B-434AF490A9EA}.ReleaseLTCG-Clang|x86.ActiveCfg = ReleaseLTCG-Clang|Win32
{460A096B-FCC7-465C-8E4B-434AF490A9EA}.ReleaseLTCG-Clang|x86.Build.0 = ReleaseLTCG-Clang|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Debug|ARM64.ActiveCfg = Debug|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Debug|ARM64.Build.0 = Debug|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Debug|x64.ActiveCfg = Debug|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Debug|x64.Build.0 = Debug|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Debug|x86.ActiveCfg = Debug|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Debug|x86.Build.0 = Debug|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Debug-Clang|ARM64.ActiveCfg = Debug-Clang|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Debug-Clang|ARM64.Build.0 = Debug-Clang|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Debug-Clang|x64.ActiveCfg = Debug-Clang|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Debug-Clang|x64.Build.0 = Debug-Clang|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Debug-Clang|x86.ActiveCfg = Debug-Clang|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Debug-Clang|x86.Build.0 = Debug-Clang|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.DebugFast|ARM64.ActiveCfg = DebugFast|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.DebugFast|ARM64.Build.0 = DebugFast|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.DebugFast|x64.ActiveCfg = DebugFast|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.DebugFast|x64.Build.0 = DebugFast|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.DebugFast|x86.ActiveCfg = DebugFast|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.DebugFast|x86.Build.0 = DebugFast|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.DebugFast-Clang|ARM64.ActiveCfg = DebugFast-Clang|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.DebugFast-Clang|ARM64.Build.0 = DebugFast-Clang|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.DebugFast-Clang|x64.ActiveCfg = DebugFast-Clang|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.DebugFast-Clang|x64.Build.0 = DebugFast-Clang|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.DebugFast-Clang|x86.ActiveCfg = DebugFast-Clang|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.DebugFast-Clang|x86.Build.0 = DebugFast-Clang|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Release|ARM64.ActiveCfg = Release|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Release|ARM64.Build.0 = Release|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Release|x64.ActiveCfg = Release|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Release|x64.Build.0 = Release|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Release|x86.ActiveCfg = Release|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Release|x86.Build.0 = Release|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Release-Clang|ARM64.ActiveCfg = Release-Clang|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Release-Clang|ARM64.Build.0 = Release-Clang|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Release-Clang|x64.ActiveCfg = Release-Clang|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Release-Clang|x64.Build.0 = Release-Clang|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Release-Clang|x86.ActiveCfg = Release-Clang|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.Release-Clang|x86.Build.0 = Release-Clang|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.ReleaseLTCG|ARM64.ActiveCfg = ReleaseLTCG|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.ReleaseLTCG|ARM64.Build.0 = ReleaseLTCG|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.ReleaseLTCG|x64.ActiveCfg = ReleaseLTCG|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.ReleaseLTCG|x64.Build.0 = ReleaseLTCG|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.ReleaseLTCG|x86.ActiveCfg = ReleaseLTCG|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.ReleaseLTCG|x86.Build.0 = ReleaseLTCG|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.ReleaseLTCG-Clang|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.ReleaseLTCG-Clang|ARM64.Build.0 = ReleaseLTCG-Clang|ARM64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.ReleaseLTCG-Clang|x64.ActiveCfg = ReleaseLTCG-Clang|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.ReleaseLTCG-Clang|x64.Build.0 = ReleaseLTCG-Clang|x64
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.ReleaseLTCG-Clang|x86.ActiveCfg = ReleaseLTCG-Clang|Win32
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A}.ReleaseLTCG-Clang|x86.Build.0 = ReleaseLTCG-Clang|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1656,6 +1706,7 @@ Global
{F351C4D8-594A-4850-B77B-3C1249812CCE} = {BA490C0E-497D-4634-A21E-E65012006385}
{27B8D4BB-4F01-4432-BC14-9BF6CA458EEE} = {BA490C0E-497D-4634-A21E-E65012006385}
{460A096B-FCC7-465C-8E4B-434AF490A9EA} = {BA490C0E-497D-4634-A21E-E65012006385}
{EDF3634A-CE8A-4625-92BD-27BAD5D30A9A} = {BA490C0E-497D-4634-A21E-E65012006385}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {26E40B32-7C1D-48D0-95F4-1A500E054028}