dep: Add GGPO-X
This commit is contained in:
parent
fd0d12a4f4
commit
1c3742dc8e
|
@ -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.
|
|
@ -0,0 +1,53 @@
|
|||
<?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.h" />
|
||||
<ClInclude Include="src\network\udp_msg.h" />
|
||||
<ClInclude Include="src\network\udp_proto.h" />
|
||||
<ClInclude Include="src\platform_windows.h" />
|
||||
<ClInclude Include="src\poll.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.cpp" />
|
||||
<ClCompile Include="src\network\udp_proto.cpp" />
|
||||
<ClCompile Include="src\platform_windows.cpp" />
|
||||
<ClCompile Include="src\poll.cpp" />
|
||||
<ClCompile Include="src\sync.cpp" />
|
||||
<ClCompile Include="src\timesync.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\msvc\vsprops\StaticLibrary.props" />
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>TurnOffAllWarnings</WarningLevel>
|
||||
<PreprocessorDefinitions>_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)src;$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<Import Project="..\msvc\vsprops\Targets.props" />
|
||||
</Project>
|
|
@ -0,0 +1,74 @@
|
|||
<?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_linux.h" />
|
||||
<ClInclude Include="src\platform_windows.h" />
|
||||
<ClInclude Include="src\poll.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>
|
||||
<ClInclude Include="src\network\udp.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_linux.cpp" />
|
||||
<ClCompile Include="src\platform_windows.cpp" />
|
||||
<ClCompile Include="src\poll.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>
|
||||
<ClCompile Include="src\network\udp.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>
|
|
@ -0,0 +1,607 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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_
|
||||
|
||||
#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 {
|
||||
char ip_address[32];
|
||||
unsigned short port;
|
||||
} 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_DISCONNECTED_FROM_PEER = 1004,
|
||||
GGPO_EVENTCODE_TIMESYNC = 1005,
|
||||
GGPO_EVENTCODE_CONNECTION_INTERRUPTED = 1006,
|
||||
GGPO_EVENTCODE_CONNECTION_RESUMED = 1007,
|
||||
GGPO_EVENTCODE_CHAT = 1008,
|
||||
GGPO_EVENTCODE_DESYNC = 1009
|
||||
} 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 senderID;
|
||||
const char* msg;
|
||||
} chat;
|
||||
struct {
|
||||
int nFrameOfDesync;
|
||||
uint16_t ourCheckSum;
|
||||
uint16_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 {
|
||||
/*
|
||||
* begin_game callback - This callback has been deprecated. You must
|
||||
* implement it, but should ignore the 'game' parameter.
|
||||
*/
|
||||
bool (__cdecl *begin_game)(void* context, const char *game);
|
||||
|
||||
/*
|
||||
* 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);
|
||||
|
||||
/*
|
||||
* 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);
|
||||
|
||||
/*
|
||||
* 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,
|
||||
const char *game,
|
||||
int num_players,
|
||||
int input_size,
|
||||
unsigned short localport,
|
||||
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,
|
||||
char *game,
|
||||
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,
|
||||
const char *game,
|
||||
int num_players,
|
||||
int input_size,
|
||||
unsigned short local_port,
|
||||
char *host_ip,
|
||||
unsigned short host_port);
|
||||
|
||||
/*
|
||||
* 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_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_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_client_chat(GGPOSession *,
|
||||
const char* message);
|
||||
|
||||
/*
|
||||
* 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);
|
||||
|
||||
/*
|
||||
* 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
|
|
@ -0,0 +1,35 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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 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 Chat(const char* text) = 0;// { return GGPO_OK; }
|
||||
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; }
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,821 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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"
|
||||
|
||||
static const int RECOMMENDATION_INTERVAL = 120;
|
||||
static const int DEFAULT_DISCONNECT_TIMEOUT = 5000;
|
||||
static const int DEFAULT_DISCONNECT_NOTIFY_START = 750;
|
||||
|
||||
Peer2PeerBackend::Peer2PeerBackend(GGPOSessionCallbacks *cb,
|
||||
const char *gamename,
|
||||
uint16 localport,
|
||||
int num_players,
|
||||
int input_size, int nframes) :
|
||||
_num_players(num_players),
|
||||
_input_size(input_size),
|
||||
_sync(_local_connect_status, nframes),
|
||||
_disconnect_timeout(DEFAULT_DISCONNECT_TIMEOUT),
|
||||
_disconnect_notify_start(DEFAULT_DISCONNECT_NOTIFY_START),
|
||||
_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);
|
||||
|
||||
/*
|
||||
* Initialize the UDP port
|
||||
*/
|
||||
_udp.Init(localport, &_poll, this);
|
||||
|
||||
_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;
|
||||
}
|
||||
|
||||
/*
|
||||
* Preload the ROM
|
||||
*/
|
||||
_callbacks.begin_game(_callbacks.context, gamename);
|
||||
}
|
||||
|
||||
Peer2PeerBackend::~Peer2PeerBackend()
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
Peer2PeerBackend::AddRemotePlayer(char *ip,
|
||||
uint16 port,
|
||||
int queue)
|
||||
{
|
||||
/*
|
||||
* Start the state machine (xxx: no)
|
||||
*/
|
||||
_synchronizing = true;
|
||||
|
||||
_endpoints[queue].Init(&_udp, _poll, queue, ip, port, _local_connect_status);
|
||||
_endpoints[queue].SetDisconnectTimeout(_disconnect_timeout);
|
||||
_endpoints[queue].SetDisconnectNotifyStart(_disconnect_notify_start);
|
||||
_endpoints[queue].Synchronize();
|
||||
}
|
||||
|
||||
GGPOErrorCode Peer2PeerBackend::AddSpectator(char *ip,
|
||||
uint16 port)
|
||||
{
|
||||
if (_num_spectators == GGPO_MAX_SPECTATORS) {
|
||||
return GGPO_ERRORCODE_TOO_MANY_SPECTATORS;
|
||||
}
|
||||
/*
|
||||
* Currently, we can only add spectators before the game starts.
|
||||
*/
|
||||
if (!_synchronizing) {
|
||||
return GGPO_ERRORCODE_INVALID_REQUEST;
|
||||
}
|
||||
int queue = _num_spectators++;
|
||||
|
||||
_spectators[queue].Init(&_udp, _poll, queue + 1000, ip, port, _local_connect_status);
|
||||
_spectators[queue].SetDisconnectTimeout(_disconnect_timeout);
|
||||
_spectators[queue].SetDisconnectNotifyStart(_disconnect_notify_start);
|
||||
_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);
|
||||
|
||||
char buf[256];
|
||||
sprintf_s<256>(buf, "DESYNC Checksum frame %d, local: %d, remote %d, size of checksum maps: %d,%d", checkSumFrame, localChecksum, remoteChecksum, (int)_confirmedCheckSums.size(), (int)ep._remoteCheckSums.size());
|
||||
// OutputDebugStringA(buf);
|
||||
}
|
||||
|
||||
if (checkSumFrame % 100 == 0)
|
||||
{
|
||||
char buf[256];
|
||||
sprintf_s<256>(buf, "Checksum frame %d, local: %d, remote %d, size of checksum maps: %d,%d\n", checkSumFrame, localChecksum, remoteChecksum, (int)_confirmedCheckSums.size(), (int)ep._remoteCheckSums.size());
|
||||
//OutputDebugStringA(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto k : keysToRemove)
|
||||
{
|
||||
ep._remoteCheckSums.erase(k);
|
||||
}
|
||||
}
|
||||
for (auto k : keysToRemove)
|
||||
{
|
||||
char buf[256];
|
||||
sprintf_s<256>(buf, "Erase checksums for frame %d\n",k);
|
||||
// OutputDebugStringA(buf);
|
||||
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()
|
||||
{
|
||||
// Pass on chat
|
||||
for (int i = 0; i < _num_players; i++) {
|
||||
_endpoints[i].ConsumeChat([&](const char* msg) {
|
||||
GGPOEvent info;
|
||||
info.u.chat.senderID = i;
|
||||
info.code = GGPO_EVENTCODE_CHAT;
|
||||
info.u.chat.msg = msg;
|
||||
_callbacks.on_event(_callbacks.context, &info);
|
||||
});
|
||||
}
|
||||
|
||||
if (!_sync.InRollback()) {
|
||||
_poll.Pump(0);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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.ip_address, player->u.remote.port);
|
||||
}
|
||||
|
||||
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.ip_address, player->u.remote.port, queue);
|
||||
}
|
||||
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) {
|
||||
char buf[128];
|
||||
input.checksum = _pendingCheckSums.at(_confirmedCheckSumFrame);
|
||||
_confirmedCheckSums[_confirmedCheckSumFrame] = input.checksum;
|
||||
_pendingCheckSums.erase(_confirmedCheckSumFrame);
|
||||
sprintf_s<128>(buf, "Frame %d: Send checksum for frame %d, val %d\n", input.frame, _confirmedCheckSumFrame, input.checksum);
|
||||
//OutputDebugStringA(buf);
|
||||
}
|
||||
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();
|
||||
char buf[256];
|
||||
uint16_t cSum = checksum1;
|
||||
Log("End of frame (%d)...\n", _sync.GetFrameCount());
|
||||
static int maxDif = 0;
|
||||
if (_pendingCheckSums.count(_sync.GetFrameCount()))
|
||||
{
|
||||
auto max = _pendingCheckSums.rbegin()->first;
|
||||
auto diff = max - currentFrame;
|
||||
maxDif = max(maxDif, diff);
|
||||
int oldChecksum = _pendingCheckSums[_sync.GetFrameCount()];
|
||||
_pendingCheckSums[_sync.GetFrameCount()] = cSum;
|
||||
sprintf_s<256>(buf, "Replace local checksum for frame %d: %d with %d, newest frame is %d, max diff %d\n", _sync.GetFrameCount(), oldChecksum, _pendingCheckSums[_sync.GetFrameCount()], max, maxDif);
|
||||
|
||||
|
||||
if (currentFrame <= _confirmedCheckSumFrame)
|
||||
{
|
||||
sprintf_s<256>(buf, "Changing frame %d in a rollback, but we've already sent frame %d\n", currentFrame, _confirmedCheckSumFrame);
|
||||
|
||||
OutputDebugStringA(buf);
|
||||
throw std::exception(buf);
|
||||
}
|
||||
if (diff >= (_sync.MaxPredictionFrames())) {
|
||||
|
||||
sprintf_s<256>(buf, "diff is bigger than max prediction\n");
|
||||
|
||||
OutputDebugStringA(buf);
|
||||
throw std::exception(buf);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sprintf_s<256>(buf, "Added local checksum for frame %d: %d\n", _sync.GetFrameCount(), cSum);
|
||||
//OutputDebugStringA(buf);
|
||||
}
|
||||
|
||||
_pendingCheckSums[_sync.GetFrameCount()]= cSum ;
|
||||
|
||||
|
||||
|
||||
_sync.IncrementFrame();
|
||||
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, uint16 cs)
|
||||
{
|
||||
if (framenumber <= _sync.MaxPredictionFrames())
|
||||
return;
|
||||
framenumber; cs;
|
||||
//auto frameOfChecksumToSend = framenumber - (_sync.MaxPredictionFrames() + 1);
|
||||
|
||||
}
|
||||
|
||||
int Peer2PeerBackend::HowFarBackForChecksums()const
|
||||
{
|
||||
return 16;
|
||||
}/*
|
||||
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)
|
||||
{
|
||||
char buf[256];
|
||||
sprintf_s<256>(buf, "Received checksum for frame %d, remote cs is %d\n", checkSumFrame, remoteChecksum);
|
||||
//OutputDebugStringA(buf);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UdpProtocol::Event::Disconnected:
|
||||
DisconnectPlayer(QueueToPlayerHandle(queue));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Peer2PeerBackend::OnUdpProtocolSpectatorEvent(UdpProtocol::Event &evt, int queue)
|
||||
{
|
||||
GGPOPlayerHandle handle = QueueToSpectatorHandle(queue);
|
||||
OnUdpProtocolEvent(evt, handle);
|
||||
|
||||
GGPOEvent info;
|
||||
|
||||
switch (evt.type) {
|
||||
case UdpProtocol::Event::Disconnected:
|
||||
_spectators[queue].Disconnect();
|
||||
|
||||
info.code = GGPO_EVENTCODE_DISCONNECTED_FROM_PEER;
|
||||
info.u.disconnected.player = handle;
|
||||
_callbacks.on_event(_callbacks.context, &info);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
case UdpProtocol::Event::NetworkInterrupted:
|
||||
info.code = GGPO_EVENTCODE_CONNECTION_INTERRUPTED;
|
||||
info.u.connection_interrupted.player = handle;
|
||||
info.u.connection_interrupted.disconnect_timeout = evt.u.network_interrupted.disconnect_timeout;
|
||||
_callbacks.on_event(_callbacks.context, &info);
|
||||
break;
|
||||
|
||||
case UdpProtocol::Event::NetworkResumed:
|
||||
info.code = GGPO_EVENTCODE_CONNECTION_RESUMED;
|
||||
info.u.connection_resumed.player = handle;
|
||||
_callbacks.on_event(_callbacks.context, &info);
|
||||
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();
|
||||
|
||||
_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");
|
||||
}*/
|
||||
|
||||
info.code = GGPO_EVENTCODE_DISCONNECTED_FROM_PEER;
|
||||
info.u.disconnected.player = QueueToPlayerHandle(queue);
|
||||
_callbacks.on_event(_callbacks.context, &info);
|
||||
|
||||
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::SetDisconnectTimeout(int timeout)
|
||||
{
|
||||
_disconnect_timeout = timeout;
|
||||
for (int i = 0; i < _num_players; i++) {
|
||||
if (_endpoints[i].IsInitialized()) {
|
||||
_endpoints[i].SetDisconnectTimeout(_disconnect_timeout);
|
||||
}
|
||||
}
|
||||
return GGPO_OK;
|
||||
}
|
||||
|
||||
GGPOErrorCode
|
||||
Peer2PeerBackend::SetDisconnectNotifyStart(int timeout)
|
||||
{
|
||||
_disconnect_notify_start = timeout;
|
||||
for (int i = 0; i < _num_players; i++) {
|
||||
if (_endpoints[i].IsInitialized()) {
|
||||
_endpoints[i].SetDisconnectNotifyStart(_disconnect_notify_start);
|
||||
}
|
||||
}
|
||||
return GGPO_OK;
|
||||
}
|
||||
|
||||
GGPOErrorCode
|
||||
Peer2PeerBackend::Chat(const char* text)
|
||||
{
|
||||
if (strlen(text) >= MAX_CHAT_LENGTH)
|
||||
return GGPO_CHAT_MESSAGE_TOO_LONG;
|
||||
|
||||
// Send the input to all the remote players.
|
||||
for (int i = 0; i < _num_players; i++) {
|
||||
if (_endpoints[i].IsInitialized()) {
|
||||
_endpoints[i].SendChat(text);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Peer2PeerBackend::OnMsg(sockaddr_in &from, UdpMsg *msg, int len)
|
||||
{
|
||||
for (int i = 0; i < _num_players; i++) {
|
||||
if (_endpoints[i].HandlesMsg(from, msg)) {
|
||||
_endpoints[i].OnMsg(msg, len);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < _num_spectators; i++) {
|
||||
if (_spectators[i].HandlesMsg(from, msg)) {
|
||||
_spectators[i].OnMsg(msg, len);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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 "poll.h"
|
||||
#include "sync.h"
|
||||
#include "backend.h"
|
||||
#include "timesync.h"
|
||||
#include "network/udp_proto.h"
|
||||
#include <map>
|
||||
class Peer2PeerBackend : public GGPOSession, Udp::Callbacks {
|
||||
public:
|
||||
Peer2PeerBackend(GGPOSessionCallbacks *cb, const char *gamename, uint16 localport, int num_players, int input_size, int nframes);
|
||||
virtual ~Peer2PeerBackend();
|
||||
|
||||
|
||||
public:
|
||||
virtual GGPOErrorCode DoPoll() 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 SetDisconnectTimeout(int timeout) override;
|
||||
virtual GGPOErrorCode SetDisconnectNotifyStart(int timeout) override;
|
||||
virtual GGPOErrorCode Chat(const char* text) override;
|
||||
virtual GGPOErrorCode CurrentFrame(int& current) override;
|
||||
public:
|
||||
virtual void OnMsg(sockaddr_in &from, UdpMsg *msg, int len);
|
||||
|
||||
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(char *remoteip, uint16 reportport, int queue);
|
||||
GGPOErrorCode AddSpectator(char *remoteip, uint16 reportport);
|
||||
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;
|
||||
Poll _poll;
|
||||
Sync _sync;
|
||||
Udp _udp;
|
||||
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;
|
||||
int _disconnect_timeout;
|
||||
int _disconnect_notify_start;
|
||||
|
||||
UdpMsg::connect_status _local_connect_status[UDP_MSG_MAX_PLAYERS];
|
||||
struct ChecksumEntry {
|
||||
int nFrame;
|
||||
int checkSum;;
|
||||
};
|
||||
std::map<int, uint16> _pendingCheckSums;
|
||||
std::map<int, uint16> _confirmedCheckSums;
|
||||
|
||||
// uint16 GetChecksumForConfirmedFrame(int frameNumber) const;
|
||||
void CheckRemoteChecksum(int framenumber, uint16 cs);
|
||||
int HowFarBackForChecksums()const;
|
||||
int _confirmedCheckSumFrame = -500;
|
||||
void CheckDesync();
|
||||
};
|
||||
|
||||
#endif
|
|
@ -0,0 +1,180 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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,
|
||||
const char* gamename,
|
||||
uint16 localport,
|
||||
int num_players,
|
||||
int input_size,
|
||||
char *hostip,
|
||||
u_short hostport) :
|
||||
_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;
|
||||
}
|
||||
|
||||
/*
|
||||
* Initialize the UDP port
|
||||
*/
|
||||
_udp.Init(localport, &_poll, this);
|
||||
|
||||
/*
|
||||
* Init the host endpoint
|
||||
*/
|
||||
_host.Init(&_udp, _poll, 0, hostip, hostport, NULL);
|
||||
_host.Synchronize();
|
||||
|
||||
/*
|
||||
* Preload the ROM
|
||||
*/
|
||||
_callbacks.begin_game(_callbacks.context, gamename);
|
||||
}
|
||||
|
||||
SpectatorBackend::~SpectatorBackend()
|
||||
{
|
||||
}
|
||||
|
||||
GGPOErrorCode
|
||||
SpectatorBackend::DoPoll()
|
||||
{
|
||||
_poll.Pump(0);
|
||||
|
||||
PollUdpProtocolEvents();
|
||||
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::NetworkInterrupted:
|
||||
info.code = GGPO_EVENTCODE_CONNECTION_INTERRUPTED;
|
||||
info.u.connection_interrupted.player = 0;
|
||||
info.u.connection_interrupted.disconnect_timeout = evt.u.network_interrupted.disconnect_timeout;
|
||||
_callbacks.on_event(_callbacks.context, &info);
|
||||
break;
|
||||
|
||||
case UdpProtocol::Event::NetworkResumed:
|
||||
info.code = GGPO_EVENTCODE_CONNECTION_RESUMED;
|
||||
info.u.connection_resumed.player = 0;
|
||||
_callbacks.on_event(_callbacks.context, &info);
|
||||
break;
|
||||
|
||||
case UdpProtocol::Event::Disconnected:
|
||||
info.code = GGPO_EVENTCODE_DISCONNECTED_FROM_PEER;
|
||||
info.u.disconnected.player = 0;
|
||||
_callbacks.on_event(_callbacks.context, &info);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SpectatorBackend::OnMsg(sockaddr_in &from, UdpMsg *msg, int len)
|
||||
{
|
||||
if (_host.HandlesMsg(from, msg)) {
|
||||
_host.OnMsg(msg, len);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 _SPECTATOR_H
|
||||
#define _SPECTATOR_H
|
||||
|
||||
#include "types.h"
|
||||
#include "poll.h"
|
||||
#include "sync.h"
|
||||
#include "backend.h"
|
||||
#include "timesync.h"
|
||||
#include "network/udp_proto.h"
|
||||
|
||||
#define SPECTATOR_FRAME_BUFFER_SIZE 64
|
||||
|
||||
class SpectatorBackend : public GGPOSession, Udp::Callbacks {
|
||||
public:
|
||||
SpectatorBackend(GGPOSessionCallbacks *cb, const char *gamename, uint16 localport, int num_players, int input_size, char *hostip, u_short hostport);
|
||||
virtual ~SpectatorBackend();
|
||||
|
||||
|
||||
public:
|
||||
virtual GGPOErrorCode DoPoll();
|
||||
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 Chat(const char* text) override { return GGPO_ERRORCODE_UNSUPPORTED; }
|
||||
virtual GGPOErrorCode CurrentFrame(int& current) override;
|
||||
|
||||
|
||||
public:
|
||||
virtual void OnMsg(sockaddr_in &from, UdpMsg *msg, int len);
|
||||
|
||||
protected:
|
||||
void PollUdpProtocolEvents(void);
|
||||
void CheckInitialSync(void);
|
||||
|
||||
void OnUdpProtocolEvent(UdpProtocol::Event &e);
|
||||
|
||||
protected:
|
||||
GGPOSessionCallbacks _callbacks;
|
||||
Poll _poll;
|
||||
Udp _udp;
|
||||
UdpProtocol _host;
|
||||
bool _synchronizing;
|
||||
int _input_size;
|
||||
int _num_players;
|
||||
int _next_input_to_send;
|
||||
GameInput _inputs[SPECTATOR_FRAME_BUFFER_SIZE];
|
||||
};
|
||||
|
||||
#endif
|
|
@ -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"
|
||||
|
||||
SyncTestBackend::SyncTestBackend(GGPOSessionCallbacks *cb,
|
||||
char *gamename,
|
||||
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();
|
||||
strcpy_s(_game, gamename);
|
||||
|
||||
/*
|
||||
* Initialize the synchronziation layer
|
||||
*/
|
||||
Sync::Config config = { 0 };
|
||||
config.callbacks = _callbacks;
|
||||
config.num_prediction_frames = MAX_PREDICTION_FRAMES;
|
||||
_sync.Init(config);
|
||||
|
||||
/*
|
||||
* Preload the ROM
|
||||
*/
|
||||
_callbacks.begin_game(_callbacks.context, gamename);
|
||||
}
|
||||
|
||||
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::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;
|
||||
info.cbuf = _sync.GetLastSavedFrame().cbuf;
|
||||
info.buf = (char *)malloc(info.cbuf);
|
||||
memcpy(info.buf, _sync.GetLastSavedFrame().buf, info.cbuf);
|
||||
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);
|
||||
free(info.buf);
|
||||
}
|
||||
_last_verified = frame;
|
||||
_rollingback = false;
|
||||
}
|
||||
|
||||
return GGPO_OK;
|
||||
}
|
||||
|
||||
void
|
||||
SyncTestBackend::RaiseSyncError(const char *fmt, ...)
|
||||
{
|
||||
char buf[1024];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsprintf_s(buf, ARRAY_SIZE(buf), fmt, args);
|
||||
va_end(args);
|
||||
|
||||
puts(buf);
|
||||
OutputDebugStringA(buf);
|
||||
EndLog();
|
||||
DebugBreak();
|
||||
}
|
||||
|
||||
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[MAX_PATH];
|
||||
CreateDirectoryA("synclogs", NULL);
|
||||
sprintf_s(filename, ARRAY_SIZE(filename), "synclogs\\%s-%04d-%s.log",
|
||||
saving ? "state" : "log",
|
||||
_sync.GetFrameCount(),
|
||||
_rollingback ? "replay" : "original");
|
||||
|
||||
fopen_s(&_logfp, filename, "w");
|
||||
}
|
||||
|
||||
void
|
||||
SyncTestBackend::EndLog()
|
||||
{
|
||||
if (_logfp) {
|
||||
fprintf(_logfp, "Closing log file.\n");
|
||||
fclose(_logfp);
|
||||
_logfp = NULL;
|
||||
}
|
||||
}
|
||||
void
|
||||
SyncTestBackend::LogSaveStates(SavedInfo &info)
|
||||
{
|
||||
char filename[MAX_PATH];
|
||||
sprintf_s(filename, ARRAY_SIZE(filename), "synclogs\\state-%04d-original.log", _sync.GetFrameCount());
|
||||
_callbacks.log_game_state(_callbacks.context, filename, (unsigned char *)info.buf, info.cbuf);
|
||||
|
||||
sprintf_s(filename, ARRAY_SIZE(filename), "synclogs\\state-%04d-replay.log", _sync.GetFrameCount());
|
||||
_callbacks.log_game_state(_callbacks.context, filename, _sync.GetLastSavedFrame().buf, _sync.GetLastSavedFrame().cbuf);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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 : public GGPOSession {
|
||||
public:
|
||||
SyncTestBackend(GGPOSessionCallbacks *cb, char *gamename, int frames, int num_players);
|
||||
virtual ~SyncTestBackend();
|
||||
|
||||
virtual GGPOErrorCode DoPoll();
|
||||
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 Chat(const char* text) override { return GGPO_ERRORCODE_UNSUPPORTED; }
|
||||
virtual GGPOErrorCode CurrentFrame(int& current) override;
|
||||
protected:
|
||||
struct SavedInfo {
|
||||
int frame;
|
||||
int checksum;
|
||||
char *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;
|
||||
char _game[128];
|
||||
|
||||
GameInput _current_input;
|
||||
GameInput _last_input;
|
||||
RingBuffer<SavedInfo, 32> _saved_frames;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
|
@ -0,0 +1,88 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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"
|
||||
|
||||
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);
|
||||
size_t remaining = buf_size;
|
||||
if (show_frame) {
|
||||
remaining -= sprintf_s(buf, buf_size, "(frame:%d size:%d ", frame, size);
|
||||
} else {
|
||||
remaining -= sprintf_s(buf, buf_size, "(size:%d ", size);
|
||||
}
|
||||
|
||||
for (int i = 0; i < size * 8; i++) {
|
||||
char buf2[16];
|
||||
if (value(i)) {
|
||||
int c = sprintf_s(buf2, ARRAY_SIZE(buf2), "%2d ", i);
|
||||
strncat_s(buf, remaining, buf2, ARRAY_SIZE(buf2));
|
||||
remaining -= c;
|
||||
}
|
||||
}
|
||||
strncat_s(buf, remaining, ")", 1);
|
||||
}
|
||||
|
||||
void
|
||||
GameInput::log(char *prefix, bool show_frame) const
|
||||
{
|
||||
char buf[1024];
|
||||
size_t c = strlen(prefix);
|
||||
strcpy_s(buf, prefix);
|
||||
desc(buf + c, ARRAY_SIZE(buf) - c, show_frame);
|
||||
strncat_s(buf, ARRAY_SIZE(buf) - strlen(buf), "\n", 1);
|
||||
Log(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;
|
||||
}
|
||||
|
|
@ -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];
|
||||
uint16 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
|
|
@ -0,0 +1,320 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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"
|
||||
|
||||
#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;
|
||||
va_list args;
|
||||
|
||||
offset = sprintf_s(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);
|
||||
}
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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"
|
||||
|
||||
static FILE *logfile = NULL;
|
||||
|
||||
void LogFlush()
|
||||
{
|
||||
if (logfile) {
|
||||
fflush(logfile);
|
||||
}
|
||||
}
|
||||
|
||||
static char logbuf[4 * 1024 * 1024];
|
||||
|
||||
void Log(const char *fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
Logv(fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void Logv(const char *fmt, va_list args)
|
||||
{
|
||||
if (!Platform::GetConfigBool("ggpo.log") || Platform::GetConfigBool("ggpo.log.ignore")) {
|
||||
return;
|
||||
}
|
||||
if (!logfile) {
|
||||
sprintf_s(logbuf, ARRAY_SIZE(logbuf), "log-%d.log", Platform::GetProcessID());
|
||||
fopen_s(&logfile, logbuf, "w");
|
||||
}
|
||||
Logv(logfile, fmt, args);
|
||||
}
|
||||
|
||||
void Logv(FILE *fp, const char *fmt, va_list args)
|
||||
{
|
||||
if (Platform::GetConfigBool("ggpo.log.timestamps")) {
|
||||
static int start = 0;
|
||||
int t = 0;
|
||||
if (!start) {
|
||||
start = Platform::GetCurrentTimeMS();
|
||||
} else {
|
||||
t = Platform::GetCurrentTimeMS() - start;
|
||||
}
|
||||
fprintf(fp, "%d.%03d : ", t / 1000, t % 1000);
|
||||
}
|
||||
|
||||
vfprintf(fp, fmt, args);
|
||||
fflush(fp);
|
||||
|
||||
vsprintf_s(logbuf, ARRAY_SIZE(logbuf), fmt, args);
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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
|
||||
|
||||
extern void Log(const char *fmt, ...);
|
||||
extern void Logv(const char *fmt, va_list list);
|
||||
extern void Logv(FILE *fp, const char *fmt, va_list args);
|
||||
extern void LogFlush();
|
||||
extern void LogFlushOnLog(bool flush);
|
||||
|
||||
#endif
|
|
@ -0,0 +1,221 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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,
|
||||
const char *game,
|
||||
int num_players,
|
||||
int input_size,
|
||||
unsigned short localport,
|
||||
int maxPrediction)
|
||||
{
|
||||
*session= new Peer2PeerBackend(cb,
|
||||
game,
|
||||
localport,
|
||||
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,
|
||||
char *game,
|
||||
int num_players,
|
||||
int input_size,
|
||||
int frames)
|
||||
{
|
||||
*ggpo = new SyncTestBackend(cb, game, 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_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_client_chat(GGPOSession *ggpo, const char *text)
|
||||
{
|
||||
if (!ggpo) {
|
||||
return GGPO_ERRORCODE_INVALID_SESSION;
|
||||
}
|
||||
return ggpo->Chat(text);
|
||||
}
|
||||
|
||||
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_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,
|
||||
const char *game,
|
||||
int num_players,
|
||||
int input_size,
|
||||
unsigned short local_port,
|
||||
char *host_ip,
|
||||
unsigned short host_port)
|
||||
{
|
||||
*session= new SpectatorBackend(cb,
|
||||
game,
|
||||
local_port,
|
||||
num_players,
|
||||
input_size,
|
||||
host_ip,
|
||||
host_port);
|
||||
return GGPO_OK;
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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.h"
|
||||
|
||||
SOCKET
|
||||
CreateSocket(uint16 bind_port, int retries)
|
||||
{
|
||||
SOCKET s;
|
||||
sockaddr_in sin;
|
||||
uint16 port;
|
||||
int optval = 1;
|
||||
|
||||
s = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (const char *)&optval, sizeof optval);
|
||||
setsockopt(s, SOL_SOCKET, SO_DONTLINGER, (const char *)&optval, sizeof optval);
|
||||
|
||||
// non-blocking...
|
||||
u_long iMode = 1;
|
||||
ioctlsocket(s, FIONBIO, &iMode);
|
||||
|
||||
sin.sin_family = AF_INET;
|
||||
sin.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
for (port = bind_port; port <= bind_port + retries; port++) {
|
||||
sin.sin_port = htons(port);
|
||||
if (bind(s, (sockaddr *)&sin, sizeof sin) != SOCKET_ERROR) {
|
||||
Log("Udp bound to port: %d.\n", port);
|
||||
return s;
|
||||
}
|
||||
}
|
||||
closesocket(s);
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
|
||||
Udp::Udp() :
|
||||
_socket(INVALID_SOCKET),
|
||||
_callbacks(NULL)
|
||||
{
|
||||
}
|
||||
|
||||
Udp::~Udp(void)
|
||||
{
|
||||
if (_socket != INVALID_SOCKET) {
|
||||
closesocket(_socket);
|
||||
_socket = INVALID_SOCKET;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Udp::Init(uint16 port, Poll *poll, Callbacks *callbacks)
|
||||
{
|
||||
_callbacks = callbacks;
|
||||
|
||||
_poll = poll;
|
||||
_poll->RegisterLoop(this);
|
||||
|
||||
Log("binding udp socket to port %d.\n", port);
|
||||
_socket = CreateSocket(port, 0);
|
||||
}
|
||||
|
||||
void
|
||||
Udp::SendTo(char *buffer, int len, int flags, struct sockaddr *dst, int destlen)
|
||||
{
|
||||
struct sockaddr_in *to = (struct sockaddr_in *)dst;
|
||||
|
||||
int res = sendto(_socket, buffer, len, flags, dst, destlen);
|
||||
if (res == SOCKET_ERROR) {
|
||||
DWORD err = WSAGetLastError();
|
||||
Log("unknown error in sendto (erro: %d wsaerr: %d).\n", res, err);
|
||||
ASSERT(FALSE && "Unknown error in sendto");
|
||||
}
|
||||
char dst_ip[1024];
|
||||
Log("sent packet length %d to %s:%d (ret:%d).\n", len, inet_ntop(AF_INET, (void *)&to->sin_addr, dst_ip, ARRAY_SIZE(dst_ip)), ntohs(to->sin_port), res);
|
||||
}
|
||||
|
||||
bool
|
||||
Udp::OnLoopPoll(void *cookie)
|
||||
{
|
||||
uint8 recv_buf[MAX_UDP_PACKET_SIZE];
|
||||
sockaddr_in recv_addr;
|
||||
int recv_addr_len;
|
||||
|
||||
for (;;) {
|
||||
recv_addr_len = sizeof(recv_addr);
|
||||
int len = recvfrom(_socket, (char *)recv_buf, MAX_UDP_PACKET_SIZE, 0, (struct sockaddr *)&recv_addr, &recv_addr_len);
|
||||
|
||||
// TODO: handle len == 0... indicates a disconnect.
|
||||
|
||||
if (len == -1) {
|
||||
int error = WSAGetLastError();
|
||||
if (error != WSAEWOULDBLOCK) {
|
||||
Log("recvfrom WSAGetLastError returned %d (%x).\n", error, error);
|
||||
}
|
||||
break;
|
||||
} else if (len > 0) {
|
||||
char src_ip[1024];
|
||||
Log("recvfrom returned (len:%d from:%s:%d).\n", len, inet_ntop(AF_INET, (void*)&recv_addr.sin_addr, src_ip, ARRAY_SIZE(src_ip)), ntohs(recv_addr.sin_port) );
|
||||
UdpMsg *msg = (UdpMsg *)recv_buf;
|
||||
_callbacks->OnMsg(recv_addr, msg, len);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Udp::Log(const char *fmt, ...)
|
||||
{
|
||||
char buf[1024];
|
||||
size_t offset;
|
||||
va_list args;
|
||||
|
||||
strcpy_s(buf, "udp | ");
|
||||
offset = strlen(buf);
|
||||
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);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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_H
|
||||
#define _UDP_H
|
||||
|
||||
#include "poll.h"
|
||||
#include "udp_msg.h"
|
||||
#include "ggponet.h"
|
||||
#include "ring_buffer.h"
|
||||
|
||||
#define MAX_UDP_ENDPOINTS 16
|
||||
|
||||
static const int MAX_UDP_PACKET_SIZE = 4096;
|
||||
|
||||
class Udp : public IPollSink
|
||||
{
|
||||
public:
|
||||
struct Stats {
|
||||
int bytes_sent;
|
||||
int packets_sent;
|
||||
float kbps_sent;
|
||||
};
|
||||
|
||||
struct Callbacks {
|
||||
virtual ~Callbacks() { }
|
||||
virtual void OnMsg(sockaddr_in &from, UdpMsg *msg, int len) = 0;
|
||||
};
|
||||
|
||||
|
||||
protected:
|
||||
void Log(const char *fmt, ...);
|
||||
|
||||
public:
|
||||
Udp();
|
||||
|
||||
void Init(uint16 port, Poll *p, Callbacks *callbacks);
|
||||
|
||||
void SendTo(char *buffer, int len, int flags, struct sockaddr *dst, int destlen);
|
||||
|
||||
bool OnLoopPoll(void *cookie) override;
|
||||
|
||||
public:
|
||||
~Udp(void);
|
||||
|
||||
protected:
|
||||
// Network transmission information
|
||||
SOCKET _socket;
|
||||
|
||||
// state management
|
||||
Callbacks *_callbacks;
|
||||
Poll *_poll;
|
||||
|
||||
|
||||
};
|
||||
|
||||
#endif
|
|
@ -0,0 +1,114 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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
|
||||
#define MAX_CHAT_LENGTH 120
|
||||
#pragma pack(push, 1)
|
||||
|
||||
struct UdpMsg
|
||||
{
|
||||
enum MsgType {
|
||||
Invalid = 0,
|
||||
SyncRequest = 1,
|
||||
SyncReply = 2,
|
||||
Input = 3,
|
||||
QualityReport = 4,
|
||||
QualityReply = 5,
|
||||
KeepAlive = 6,
|
||||
InputAck = 7,
|
||||
Chat = 8,
|
||||
};
|
||||
|
||||
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 disconnect_requested:1;
|
||||
int ack_frame:31;
|
||||
|
||||
uint16 num_bits;
|
||||
uint16 checksum16;
|
||||
uint8 input_size; // XXX: shouldn't be in every single packet!
|
||||
uint8 bits[MAX_COMPRESSED_BITS]; /* must be last */
|
||||
} input;
|
||||
|
||||
struct {
|
||||
int ack_frame:31;
|
||||
} input_ack;
|
||||
struct {
|
||||
char msg[MAX_CHAT_LENGTH];
|
||||
} chat;
|
||||
} 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 Chat: return MAX_CHAT_LENGTH;
|
||||
case KeepAlive: return 0;
|
||||
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
|
|
@ -0,0 +1,819 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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 <cmath>
|
||||
#include <iostream>
|
||||
static const int UDP_HEADER_SIZE = 28; /* Size of IP + UDP headers */
|
||||
static const int NUM_SYNC_PACKETS = 5;
|
||||
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 KEEP_ALIVE_INTERVAL = 200;
|
||||
static const int QUALITY_REPORT_INTERVAL = 333;
|
||||
static const int NETWORK_STATS_INTERVAL = 500;
|
||||
static const int UDP_SHUTDOWN_TIMER = 5000;
|
||||
static const int MAX_SEQ_DISTANCE = (1 << 15);
|
||||
|
||||
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),
|
||||
_shutdown_timeout(0),
|
||||
_disconnect_timeout(0),
|
||||
_disconnect_notify_start(0),
|
||||
_disconnect_notify_sent(false),
|
||||
_disconnect_event_sent(false),
|
||||
_connected(false),
|
||||
_next_send_seq(0),
|
||||
_next_recv_seq(0),
|
||||
_udp(NULL)
|
||||
{
|
||||
_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;
|
||||
}
|
||||
memset(&_peer_addr, 0, sizeof _peer_addr);
|
||||
_oo_packet.msg = NULL;
|
||||
|
||||
_send_latency = Platform::GetConfigInt("ggpo.network.delay");
|
||||
_oop_percent = Platform::GetConfigInt("ggpo.oop.percent");
|
||||
}
|
||||
|
||||
UdpProtocol::~UdpProtocol()
|
||||
{
|
||||
ClearSendQueue();
|
||||
}
|
||||
void UdpProtocol::SetFrameDelay(int delay)
|
||||
{
|
||||
_timesync.SetFrameDelay(delay);
|
||||
}
|
||||
|
||||
int UdpProtocol::RemoteFrameDelay()const
|
||||
{
|
||||
return _timesync._remoteFrameDelay;
|
||||
}
|
||||
void
|
||||
UdpProtocol::Init(Udp *udp,
|
||||
Poll &poll,
|
||||
int queue,
|
||||
char *ip,
|
||||
u_short port,
|
||||
UdpMsg::connect_status *status)
|
||||
{
|
||||
_udp = udp;
|
||||
_queue = queue;
|
||||
_local_connect_status = status;
|
||||
|
||||
_peer_addr.sin_family = AF_INET;
|
||||
_peer_addr.sin_port = htons(port);
|
||||
inet_pton(AF_INET, ip, &_peer_addr.sin_addr.s_addr);
|
||||
|
||||
do {
|
||||
_magic_number = (uint16)rand();
|
||||
} while (_magic_number == 0);
|
||||
poll.RegisterLoop(this);
|
||||
}
|
||||
|
||||
void
|
||||
UdpProtocol::SendInput(GameInput &input)
|
||||
{
|
||||
if (_udp) {
|
||||
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 ¤t = _pending_output.item(j);
|
||||
msg->u.input.checksum16 = 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;
|
||||
|
||||
msg->u.input.disconnect_requested = _current_state == Disconnected;
|
||||
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());
|
||||
}
|
||||
void UdpProtocol::SendChat(const char* message)
|
||||
{
|
||||
UdpMsg* msg = new UdpMsg(UdpMsg::Chat);
|
||||
strcpy_s<MAX_CHAT_LENGTH>(msg->u.chat.msg, message);
|
||||
SendMsg(msg);
|
||||
}
|
||||
|
||||
bool
|
||||
UdpProtocol::OnLoopPoll(void *cookie)
|
||||
{
|
||||
if (!_udp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned int now = Platform::GetCurrentTimeMS();
|
||||
unsigned int next_interval;
|
||||
|
||||
PumpSendQueue();
|
||||
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 = Platform::GetCurrentTimeMS();
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (_last_send_time && _last_send_time + KEEP_ALIVE_INTERVAL < now) {
|
||||
Log("Sending keep alive packet\n");
|
||||
SendMsg(new UdpMsg(UdpMsg::KeepAlive));
|
||||
}
|
||||
|
||||
if (_disconnect_timeout && _disconnect_notify_start &&
|
||||
!_disconnect_notify_sent && (_last_recv_time + _disconnect_notify_start < now)) {
|
||||
Log("Endpoint has stopped receiving packets for %d ms. Sending notification.\n", _disconnect_notify_start);
|
||||
Event e(Event::NetworkInterrupted);
|
||||
e.u.network_interrupted.disconnect_timeout = _disconnect_timeout - _disconnect_notify_start;
|
||||
QueueEvent(e);
|
||||
_disconnect_notify_sent = true;
|
||||
}
|
||||
|
||||
if (_disconnect_timeout && (_last_recv_time + _disconnect_timeout < now)) {
|
||||
if (!_disconnect_event_sent) {
|
||||
Log("Endpoint has stopped receiving packets for %d ms. Disconnecting.\n", _disconnect_timeout);
|
||||
QueueEvent(Event(Event::Disconnected));
|
||||
_disconnect_event_sent = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Disconnected:
|
||||
if (_shutdown_timeout < now) {
|
||||
Log("Shutting down udp connection.\n");
|
||||
_udp = NULL;
|
||||
_shutdown_timeout = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
UdpProtocol::Disconnect()
|
||||
{
|
||||
_current_state = Disconnected;
|
||||
_shutdown_timeout = Platform::GetCurrentTimeMS() + UDP_SHUTDOWN_TIMER;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
LogMsg("send", msg);
|
||||
|
||||
_packets_sent++;
|
||||
_last_send_time = Platform::GetCurrentTimeMS();
|
||||
_bytes_sent += msg->PacketSize();
|
||||
|
||||
msg->hdr.magic = _magic_number;
|
||||
msg->hdr.sequence_number = _next_send_seq++;
|
||||
|
||||
_send_queue.push(QueueEntry(Platform::GetCurrentTimeMS(), _peer_addr, msg));
|
||||
PumpSendQueue();
|
||||
}
|
||||
|
||||
bool
|
||||
UdpProtocol::HandlesMsg(sockaddr_in &from,
|
||||
UdpMsg *msg)
|
||||
{
|
||||
if (!_udp) {
|
||||
return false;
|
||||
}
|
||||
return _peer_addr.sin_addr.S_un.S_addr == from.sin_addr.S_un.S_addr &&
|
||||
_peer_addr.sin_port == from.sin_port;
|
||||
}
|
||||
|
||||
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::OnKeepAlive, /* KeepAlive */
|
||||
&UdpProtocol::OnInputAck, /* InputAck */
|
||||
&UdpProtocol::OnChat, /* 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 = Platform::GetCurrentTimeMS();
|
||||
if (_disconnect_notify_sent && _current_state == Running) {
|
||||
QueueEvent(Event(Event::NetworkResumed));
|
||||
_disconnect_notify_sent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
UdpProtocol::UpdateNetworkStats(void)
|
||||
{
|
||||
int now = Platform::GetCurrentTimeMS();
|
||||
|
||||
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: %.2f KBps Packets Sent: %5d (%.2f pps) "
|
||||
"KB Sent: %.2f UDP Overhead: %.2f %%.\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 (_udp) {
|
||||
_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;
|
||||
|
||||
sprintf_s(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(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::KeepAlive:
|
||||
Log("%s keep alive.\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;
|
||||
case UdpMsg::Chat:
|
||||
Log("%s chat.\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)
|
||||
{
|
||||
/*
|
||||
* If a disconnect is requested, go ahead and disconnect now.
|
||||
*/
|
||||
bool disconnect_requested = msg->u.input.disconnect_requested;
|
||||
if (disconnect_requested) {
|
||||
if (_current_state != Disconnected && !_disconnect_event_sent) {
|
||||
Log("Disconnecting endpoint on remote request.\n");
|
||||
QueueEvent(Event(Event::Disconnected));
|
||||
_disconnect_event_sent = true;
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* 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.checksum16;
|
||||
/*
|
||||
* 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 = Platform::GetCurrentTimeMS();
|
||||
|
||||
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 = Platform::GetCurrentTimeMS() - msg->u.quality_reply.pong;
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
UdpProtocol::OnKeepAlive(UdpMsg *msg, int len)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
void UdpProtocol::ConsumeChat(std::function<void(const char*)> onChat)
|
||||
{
|
||||
for (const auto& msg : _chatMessages)
|
||||
onChat(msg.c_str());
|
||||
_chatMessages.clear();
|
||||
}
|
||||
bool UdpProtocol::OnChat(UdpMsg* msg, int len)
|
||||
{
|
||||
_chatMessages.push_back(msg->u.chat.msg);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UdpProtocol::SetDisconnectTimeout(int timeout)
|
||||
{
|
||||
_disconnect_timeout = timeout;
|
||||
}
|
||||
|
||||
void
|
||||
UdpProtocol::SetDisconnectNotifyStart(int timeout)
|
||||
{
|
||||
_disconnect_notify_start = timeout;
|
||||
}
|
||||
|
||||
void
|
||||
UdpProtocol::PumpSendQueue()
|
||||
{
|
||||
while (!_send_queue.empty()) {
|
||||
QueueEntry &entry = _send_queue.front();
|
||||
|
||||
if (_send_latency) {
|
||||
// should really come up with a gaussian distributation based on the configured
|
||||
// value, but this will do for now.
|
||||
int jitter = (_send_latency * 2 / 3) + ((rand() % _send_latency) / 3);
|
||||
if (Platform::GetCurrentTimeMS() < _send_queue.front().queue_time + jitter) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (_oop_percent && !_oo_packet.msg && ((rand() % 100) < _oop_percent)) {
|
||||
int delay = rand() % (_send_latency * 10 + 1000);
|
||||
Log("creating rogue oop (seq: %d delay: %d)\n", entry.msg->hdr.sequence_number, delay);
|
||||
_oo_packet.send_time = Platform::GetCurrentTimeMS() + delay;
|
||||
_oo_packet.msg = entry.msg;
|
||||
_oo_packet.dest_addr = entry.dest_addr;
|
||||
} else {
|
||||
ASSERT(entry.dest_addr.sin_addr.s_addr);
|
||||
|
||||
_udp->SendTo((char *)entry.msg, entry.msg->PacketSize(), 0,
|
||||
(struct sockaddr *)&entry.dest_addr, sizeof entry.dest_addr);
|
||||
|
||||
delete entry.msg;
|
||||
}
|
||||
_send_queue.pop();
|
||||
}
|
||||
if (_oo_packet.msg && _oo_packet.send_time < Platform::GetCurrentTimeMS()) {
|
||||
Log("sending rogue oop!");
|
||||
_udp->SendTo((char *)_oo_packet.msg, _oo_packet.msg->PacketSize(), 0,
|
||||
(struct sockaddr *)&_oo_packet.dest_addr, sizeof _oo_packet.dest_addr);
|
||||
|
||||
delete _oo_packet.msg;
|
||||
_oo_packet.msg = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
UdpProtocol::ClearSendQueue()
|
||||
{
|
||||
while (!_send_queue.empty()) {
|
||||
delete _send_queue.front().msg;
|
||||
_send_queue.pop();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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 "poll.h"
|
||||
#include "udp.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 IPollSink
|
||||
{
|
||||
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;
|
||||
Udp::Stats udp;
|
||||
};
|
||||
|
||||
struct Event {
|
||||
enum Type {
|
||||
Unknown = -1,
|
||||
Connected,
|
||||
Synchronizing,
|
||||
Synchronzied,
|
||||
Input,
|
||||
Disconnected,
|
||||
NetworkInterrupted,
|
||||
NetworkResumed,
|
||||
};
|
||||
|
||||
Type type;
|
||||
union {
|
||||
struct {
|
||||
GameInput input;
|
||||
} input;
|
||||
struct {
|
||||
int total;
|
||||
int count;
|
||||
} synchronizing;
|
||||
struct {
|
||||
int disconnect_timeout;
|
||||
} network_interrupted;
|
||||
} u;
|
||||
|
||||
UdpProtocol::Event(Type t = Unknown) : type(t) { }
|
||||
};
|
||||
|
||||
public:
|
||||
virtual bool OnLoopPoll(void *cookie);
|
||||
|
||||
public:
|
||||
UdpProtocol();
|
||||
virtual ~UdpProtocol();
|
||||
|
||||
void Init(Udp *udp, Poll &p, int queue, char *ip, u_short port, UdpMsg::connect_status *status);
|
||||
|
||||
void Synchronize();
|
||||
bool GetPeerConnectStatus(int id, int *frame);
|
||||
bool IsInitialized() { return _udp != NULL; }
|
||||
bool IsSynchronized() { return _current_state == Running; }
|
||||
bool IsRunning() { return _current_state == Running; }
|
||||
void SendInput(GameInput &input);
|
||||
void SendChat(const char* message);
|
||||
void SendInputAck();
|
||||
bool HandlesMsg(sockaddr_in &from, UdpMsg *msg);
|
||||
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 SetDisconnectTimeout(int timeout);
|
||||
void SetDisconnectNotifyStart(int timeout);
|
||||
void SetFrameDelay(int delay);
|
||||
void ConsumeChat(std::function<void(const char*)> onChat);
|
||||
void ApplyToEvents(std::function<void(UdpProtocol::Event&)> cb);
|
||||
void StartPollLoop();
|
||||
void EndPollLoop();
|
||||
std::map<int, uint16> _remoteCheckSums;
|
||||
std::map<int, uint16> _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 ClearSendQueue(void);
|
||||
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 PumpSendQueue();
|
||||
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);
|
||||
bool OnKeepAlive(UdpMsg *msg, int len);
|
||||
bool OnChat(UdpMsg *msg, int len);
|
||||
|
||||
protected:
|
||||
/*
|
||||
* Network transmission information
|
||||
*/
|
||||
Udp *_udp;
|
||||
sockaddr_in _peer_addr;
|
||||
uint16 _magic_number;
|
||||
int _queue;
|
||||
uint16 _remote_magic_number;
|
||||
bool _connected;
|
||||
int _send_latency;
|
||||
int _oop_percent;
|
||||
struct {
|
||||
int send_time;
|
||||
sockaddr_in dest_addr;
|
||||
UdpMsg* msg;
|
||||
} _oo_packet;
|
||||
RingBuffer<QueueEntry, 64> _send_queue;
|
||||
|
||||
/*
|
||||
* 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;
|
||||
unsigned int _shutdown_timeout;
|
||||
unsigned int _disconnect_event_sent;
|
||||
unsigned int _disconnect_timeout;
|
||||
unsigned int _disconnect_notify_start;
|
||||
bool _disconnect_notify_sent;
|
||||
|
||||
uint16 _next_send_seq;
|
||||
uint16 _next_recv_seq;
|
||||
|
||||
/*
|
||||
* Rift synchronization.
|
||||
*/
|
||||
TimeSync _timesync;
|
||||
|
||||
/*
|
||||
* Event queue
|
||||
*/
|
||||
RingBuffer<UdpProtocol::Event, 64> _event_queue;
|
||||
std::vector<std::string> _chatMessages;
|
||||
};
|
||||
|
||||
#endif
|
|
@ -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, ¤t);
|
||||
|
||||
return ((current.tv_sec - start.tv_sec) * 1000) +
|
||||
((current.tv_nsec - start.tv_nsec ) / 1000000) +
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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.
|
||||
*/
|
||||
|
||||
#include "platform_windows.h"
|
||||
|
||||
int
|
||||
Platform::GetConfigInt(const char* name)
|
||||
{
|
||||
char buf[1024];
|
||||
if (GetEnvironmentVariable(name, buf, ARRAY_SIZE(buf)) == 0) {
|
||||
return 0;
|
||||
}
|
||||
return atoi(buf);
|
||||
}
|
||||
|
||||
bool Platform::GetConfigBool(const char* name)
|
||||
{
|
||||
char buf[1024];
|
||||
if (GetEnvironmentVariable(name, buf, ARRAY_SIZE(buf)) == 0) {
|
||||
return false;
|
||||
}
|
||||
return atoi(buf) != 0 || _stricmp(buf, "true") == 0;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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_WINDOWS_H_
|
||||
#define _GGPO_WINDOWS_H_
|
||||
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include "types.h"
|
||||
|
||||
class Platform {
|
||||
public: // types
|
||||
typedef DWORD ProcessID;
|
||||
|
||||
public: // functions
|
||||
static ProcessID GetProcessID() { return GetCurrentProcessId(); }
|
||||
static void AssertFailed(char *msg) { MessageBoxA(NULL, msg, "GGPO Assertion Failed", MB_OK | MB_ICONEXCLAMATION); }
|
||||
static uint32 GetCurrentTimeMS() { return timeGetTime(); }
|
||||
static int GetConfigInt(const char* name);
|
||||
static bool GetConfigBool(const char* name);
|
||||
};
|
||||
|
||||
#endif
|
|
@ -0,0 +1,72 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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 "poll.h"
|
||||
|
||||
Poll::Poll(void) :
|
||||
_start_time(0)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
//void
|
||||
//Poll::RegisterMsgLoop(IPollSink *sink, void *cookie)
|
||||
//{
|
||||
// _msg_sinks.push_back(PollSinkCb(sink, cookie));
|
||||
//}
|
||||
|
||||
void
|
||||
Poll::RegisterLoop(IPollSink *sink, void *cookie)
|
||||
{
|
||||
_loop_sinks.push_back(PollSinkCb(sink, cookie));
|
||||
}
|
||||
//void
|
||||
//Poll::RegisterPeriodic(IPollSink *sink, int interval, void *cookie)
|
||||
//{
|
||||
// _periodic_sinks.push_back(PollPeriodicSinkCb(sink, cookie, interval));
|
||||
//}
|
||||
|
||||
void
|
||||
Poll::Run()
|
||||
{
|
||||
while (Pump(100)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
Poll::Pump(int timeout)
|
||||
{
|
||||
int i;
|
||||
bool finished = false;
|
||||
|
||||
for (i = 0; i < _loop_sinks.size(); i++) {
|
||||
PollSinkCb &cb = _loop_sinks[i];
|
||||
finished = !cb.sink->OnLoopPoll(cb.cookie) || finished;
|
||||
}
|
||||
return finished;
|
||||
}
|
||||
//
|
||||
//int
|
||||
//Poll::ComputeWaitTime(int elapsed)
|
||||
//{
|
||||
// int waitTime = INFINITE;
|
||||
// size_t count = _periodic_sinks.size();
|
||||
//
|
||||
// if (count > 0) {
|
||||
// for (int i = 0; i < count; i++) {
|
||||
// PollPeriodicSinkCb &cb = _periodic_sinks[i];
|
||||
// int timeout = (cb.interval + cb.last_fired) - elapsed;
|
||||
// if (waitTime == INFINITE || (timeout < waitTime)) {
|
||||
// waitTime = MAX(timeout, 0);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return waitTime;
|
||||
//}
|
|
@ -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 _POLL_H
|
||||
#define _POLL_H
|
||||
|
||||
#include "static_buffer.h"
|
||||
|
||||
#define MAX_POLLABLE_HANDLES 64
|
||||
|
||||
|
||||
class IPollSink {
|
||||
public:
|
||||
virtual ~IPollSink() { }
|
||||
//virtual bool OnMsgPoll(void*) = 0;//{ return true; }
|
||||
// virtual bool OnPeriodicPoll(void*, int) = 0;// { return true; }
|
||||
virtual bool OnLoopPoll(void*) = 0;// { return true; }
|
||||
};
|
||||
|
||||
class Poll {
|
||||
public:
|
||||
Poll(void);
|
||||
void RegisterLoop(IPollSink *sink, void *cookie = NULL);
|
||||
|
||||
void Run();
|
||||
bool Pump(int timeout);
|
||||
|
||||
protected:
|
||||
//int ComputeWaitTime(int elapsed);
|
||||
|
||||
struct PollSinkCb {
|
||||
IPollSink *sink;
|
||||
void *cookie;
|
||||
PollSinkCb() : sink(NULL), cookie(NULL) { }
|
||||
PollSinkCb(IPollSink *s, void *c) : sink(s), cookie(c) { }
|
||||
};
|
||||
|
||||
struct PollPeriodicSinkCb : public PollSinkCb {
|
||||
int interval;
|
||||
int last_fired;
|
||||
PollPeriodicSinkCb() : PollSinkCb(NULL, NULL), interval(0), last_fired(0) { }
|
||||
PollPeriodicSinkCb(IPollSink *s, void *c, int i) :
|
||||
PollSinkCb(s, c), interval(i), last_fired(0) { }
|
||||
};
|
||||
|
||||
int _start_time;
|
||||
// StaticBuffer<PollSinkCb, 16> _msg_sinks;
|
||||
StaticBuffer<PollSinkCb, 16> _loop_sinks;
|
||||
// StaticBuffer<PollPeriodicSinkCb, 16> _periodic_sinks;
|
||||
};
|
||||
|
||||
#endif
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,304 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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"
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
|
||||
// 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->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) {
|
||||
ASSERT(FALSE);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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 SyncTestBackend;
|
||||
|
||||
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
|
||||
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,87 @@
|
|||
/* -----------------------------------------------------------------------
|
||||
* 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)
|
||||
*/
|
||||
#pragma warning(disable: 4018 4100 4127 4201 4389 4800)
|
||||
|
||||
/*
|
||||
* 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>
|
||||
/*
|
||||
* Additional headers
|
||||
*/
|
||||
#if defined(_WINDOWS)
|
||||
# include "platform_windows.h"
|
||||
#elif defined(__GNUC__)
|
||||
# include "platform_linux.h"
|
||||
#else
|
||||
# error Unsupported platform
|
||||
#endif
|
||||
|
||||
#include "log.h"
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Macros
|
||||
*/
|
||||
#define ASSERT(x) \
|
||||
do { \
|
||||
if (!(x)) { \
|
||||
char assert_buf[1024]; \
|
||||
snprintf(assert_buf, sizeof(assert_buf) - 1, "Assertion: %s @ %s:%d (pid:%d)", #x, __FILE__, __LINE__, Platform::GetProcessID()); \
|
||||
Log("%s\n", assert_buf); \
|
||||
Log("\n"); \
|
||||
Log("\n"); \
|
||||
Log("\n"); \
|
||||
Platform::AssertFailed(assert_buf); \
|
||||
exit(0); \
|
||||
} \
|
||||
} 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
|
|
@ -114,6 +114,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "zstd", "dep\zstd\zstd.vcxpr
|
|||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cpuinfo", "dep\cpuinfo\cpuinfo.vcxproj", "{EE55AA65-EA6B-4861-810B-78354B53A807}"
|
||||
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
|
||||
|
@ -840,6 +842,30 @@ Global
|
|||
{EE55AA65-EA6B-4861-810B-78354B53A807}.ReleaseLTCG|x64.Build.0 = ReleaseLTCG|x64
|
||||
{EE55AA65-EA6B-4861-810B-78354B53A807}.ReleaseLTCG|x86.ActiveCfg = ReleaseLTCG|Win32
|
||||
{EE55AA65-EA6B-4861-810B-78354B53A807}.ReleaseLTCG|x86.Build.0 = ReleaseLTCG|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}.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}.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}.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
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -866,6 +892,7 @@ Global
|
|||
{751D9F62-881C-454E-BCE8-CB9CF5F1D22F} = {BA490C0E-497D-4634-A21E-E65012006385}
|
||||
{73EE0C55-6FFE-44E7-9C12-BAA52434A797} = {BA490C0E-497D-4634-A21E-E65012006385}
|
||||
{EE55AA65-EA6B-4861-810B-78354B53A807} = {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}
|
||||
|
|
Loading…
Reference in New Issue