diff --git a/dep/ggpo-x/LICENSE b/dep/ggpo-x/LICENSE new file mode 100644 index 000000000..0fae5e81c --- /dev/null +++ b/dep/ggpo-x/LICENSE @@ -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. diff --git a/dep/ggpo-x/ggpo-x.vcxproj b/dep/ggpo-x/ggpo-x.vcxproj new file mode 100644 index 000000000..cf78e9bdf --- /dev/null +++ b/dep/ggpo-x/ggpo-x.vcxproj @@ -0,0 +1,53 @@ + + + + + {EDF3634A-CE8A-4625-92BD-27BAD5D30A9A} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TurnOffAllWarnings + _WINDOWS;%(PreprocessorDefinitions) + $(ProjectDir)src;$(ProjectDir)include;%(AdditionalIncludeDirectories) + + + + \ No newline at end of file diff --git a/dep/ggpo-x/ggpo-x.vcxproj.filters b/dep/ggpo-x/ggpo-x.vcxproj.filters new file mode 100644 index 000000000..6f2e6dfd4 --- /dev/null +++ b/dep/ggpo-x/ggpo-x.vcxproj.filters @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + backends + + + backends + + + backends + + + backends + + + network + + + network + + + network + + + + + + + + + + + + + + + backends + + + backends + + + backends + + + network + + + network + + + + + {9734509d-391e-44d7-bbce-e3bc73dfcea3} + + + {1618c8a6-e50c-419c-8476-cb1c09fd8dce} + + + \ No newline at end of file diff --git a/dep/ggpo-x/include/ggponet.h b/dep/ggpo-x/include/ggponet.h new file mode 100644 index 000000000..0b7dbf35b --- /dev/null +++ b/dep/ggpo-x/include/ggponet.h @@ -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 +#include +// 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 diff --git a/dep/ggpo-x/src/backends/backend.h b/dep/ggpo-x/src/backends/backend.h new file mode 100644 index 000000000..9b2466a8d --- /dev/null +++ b/dep/ggpo-x/src/backends/backend.h @@ -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 + diff --git a/dep/ggpo-x/src/backends/p2p.cpp b/dep/ggpo-x/src/backends/p2p.cpp new file mode 100644 index 000000000..ab090f9c9 --- /dev/null +++ b/dep/ggpo-x/src/backends/p2p.cpp @@ -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 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; + } +} diff --git a/dep/ggpo-x/src/backends/p2p.h b/dep/ggpo-x/src/backends/p2p.h new file mode 100644 index 000000000..62cfea0f9 --- /dev/null +++ b/dep/ggpo-x/src/backends/p2p.h @@ -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 +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 _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 _pendingCheckSums; + std::map _confirmedCheckSums; + + // uint16 GetChecksumForConfirmedFrame(int frameNumber) const; + void CheckRemoteChecksum(int framenumber, uint16 cs); + int HowFarBackForChecksums()const; + int _confirmedCheckSumFrame = -500; + void CheckDesync(); +}; + +#endif diff --git a/dep/ggpo-x/src/backends/spectator.cpp b/dep/ggpo-x/src/backends/spectator.cpp new file mode 100644 index 000000000..5d1011819 --- /dev/null +++ b/dep/ggpo-x/src/backends/spectator.cpp @@ -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); + } +} + diff --git a/dep/ggpo-x/src/backends/spectator.h b/dep/ggpo-x/src/backends/spectator.h new file mode 100644 index 000000000..4d4645140 --- /dev/null +++ b/dep/ggpo-x/src/backends/spectator.h @@ -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 diff --git a/dep/ggpo-x/src/backends/synctest.cpp b/dep/ggpo-x/src/backends/synctest.cpp new file mode 100644 index 000000000..692bab4d8 --- /dev/null +++ b/dep/ggpo-x/src/backends/synctest.cpp @@ -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); +} diff --git a/dep/ggpo-x/src/backends/synctest.h b/dep/ggpo-x/src/backends/synctest.h new file mode 100644 index 000000000..f0955a35f --- /dev/null +++ b/dep/ggpo-x/src/backends/synctest.h @@ -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 _saved_frames; +}; + +#endif + diff --git a/dep/ggpo-x/src/bitvector.cpp b/dep/ggpo-x/src/bitvector.cpp new file mode 100644 index 000000000..ff87cd5ec --- /dev/null +++ b/dep/ggpo-x/src/bitvector.cpp @@ -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; +} + diff --git a/dep/ggpo-x/src/bitvector.h b/dep/ggpo-x/src/bitvector.h new file mode 100644 index 000000000..495b2306a --- /dev/null +++ b/dep/ggpo-x/src/bitvector.h @@ -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 diff --git a/dep/ggpo-x/src/game_input.cpp b/dep/ggpo-x/src/game_input.cpp new file mode 100644 index 000000000..1cccd3f19 --- /dev/null +++ b/dep/ggpo-x/src/game_input.cpp @@ -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; +} + diff --git a/dep/ggpo-x/src/game_input.h b/dep/ggpo-x/src/game_input.h new file mode 100644 index 000000000..a19b834ba --- /dev/null +++ b/dep/ggpo-x/src/game_input.h @@ -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 +#include + +// 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 diff --git a/dep/ggpo-x/src/input_queue.cpp b/dep/ggpo-x/src/input_queue.cpp new file mode 100644 index 000000000..ee143dc76 --- /dev/null +++ b/dep/ggpo-x/src/input_queue.cpp @@ -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); +} diff --git a/dep/ggpo-x/src/input_queue.h b/dep/ggpo-x/src/input_queue.h new file mode 100644 index 000000000..460a50168 --- /dev/null +++ b/dep/ggpo-x/src/input_queue.h @@ -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 + + + diff --git a/dep/ggpo-x/src/log.cpp b/dep/ggpo-x/src/log.cpp new file mode 100644 index 000000000..2f84cf98f --- /dev/null +++ b/dep/ggpo-x/src/log.cpp @@ -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); +} + diff --git a/dep/ggpo-x/src/log.h b/dep/ggpo-x/src/log.h new file mode 100644 index 000000000..52627d84f --- /dev/null +++ b/dep/ggpo-x/src/log.h @@ -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 diff --git a/dep/ggpo-x/src/main.cpp b/dep/ggpo-x/src/main.cpp new file mode 100644 index 000000000..36d0b264b --- /dev/null +++ b/dep/ggpo-x/src/main.cpp @@ -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; +} + diff --git a/dep/ggpo-x/src/network/udp.cpp b/dep/ggpo-x/src/network/udp.cpp new file mode 100644 index 000000000..5c2250470 --- /dev/null +++ b/dep/ggpo-x/src/network/udp.cpp @@ -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); +} diff --git a/dep/ggpo-x/src/network/udp.h b/dep/ggpo-x/src/network/udp.h new file mode 100644 index 000000000..c725150c1 --- /dev/null +++ b/dep/ggpo-x/src/network/udp.h @@ -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 diff --git a/dep/ggpo-x/src/network/udp_msg.h b/dep/ggpo-x/src/network/udp_msg.h new file mode 100644 index 000000000..e21d24f2a --- /dev/null +++ b/dep/ggpo-x/src/network/udp_msg.h @@ -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 diff --git a/dep/ggpo-x/src/network/udp_proto.cpp b/dep/ggpo-x/src/network/udp_proto.cpp new file mode 100644 index 000000000..cdbfd6240 --- /dev/null +++ b/dep/ggpo-x/src/network/udp_proto.cpp @@ -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 +#include +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 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(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 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(); + } +} diff --git a/dep/ggpo-x/src/network/udp_proto.h b/dep/ggpo-x/src/network/udp_proto.h new file mode 100644 index 000000000..14947e27f --- /dev/null +++ b/dep/ggpo-x/src/network/udp_proto.h @@ -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 +#include +#include +#include +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 onChat); + void ApplyToEvents(std::function cb); + void StartPollLoop(); + void EndPollLoop(); + std::map _remoteCheckSums; + std::map _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 _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 _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 _event_queue; + std::vector _chatMessages; +}; + +#endif diff --git a/dep/ggpo-x/src/platform_linux.cpp b/dep/ggpo-x/src/platform_linux.cpp new file mode 100644 index 000000000..84f88c767 --- /dev/null +++ b/dep/ggpo-x/src/platform_linux.cpp @@ -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) + +} + diff --git a/dep/ggpo-x/src/platform_linux.h b/dep/ggpo-x/src/platform_linux.h new file mode 100644 index 000000000..b36e05112 --- /dev/null +++ b/dep/ggpo-x/src/platform_linux.h @@ -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 +#include +#include +#include +#include + +class Platform { +public: // types + typedef pid_t ProcessID; + +public: // functions + static ProcessID GetProcessID() { return getpid(); } + static void AssertFailed(char *msg) { } + static uint32 GetCurrentTimeMS(); +}; + +#endif diff --git a/dep/ggpo-x/src/platform_windows.cpp b/dep/ggpo-x/src/platform_windows.cpp new file mode 100644 index 000000000..31a25d32a --- /dev/null +++ b/dep/ggpo-x/src/platform_windows.cpp @@ -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; +} diff --git a/dep/ggpo-x/src/platform_windows.h b/dep/ggpo-x/src/platform_windows.h new file mode 100644 index 000000000..1d308ceb0 --- /dev/null +++ b/dep/ggpo-x/src/platform_windows.h @@ -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 +#include +#include +#include +#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 diff --git a/dep/ggpo-x/src/poll.cpp b/dep/ggpo-x/src/poll.cpp new file mode 100644 index 000000000..b5c0b5005 --- /dev/null +++ b/dep/ggpo-x/src/poll.cpp @@ -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; +//} diff --git a/dep/ggpo-x/src/poll.h b/dep/ggpo-x/src/poll.h new file mode 100644 index 000000000..597cf58ba --- /dev/null +++ b/dep/ggpo-x/src/poll.h @@ -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 _msg_sinks; + StaticBuffer _loop_sinks; + // StaticBuffer _periodic_sinks; +}; + +#endif diff --git a/dep/ggpo-x/src/ring_buffer.h b/dep/ggpo-x/src/ring_buffer.h new file mode 100644 index 000000000..81c939604 --- /dev/null +++ b/dep/ggpo-x/src/ring_buffer.h @@ -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 + +template class RingBuffer +{ +public: + RingBuffer() : + _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 diff --git a/dep/ggpo-x/src/static_buffer.h b/dep/ggpo-x/src/static_buffer.h new file mode 100644 index 000000000..759878b87 --- /dev/null +++ b/dep/ggpo-x/src/static_buffer.h @@ -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 + +template class StaticBuffer +{ +public: + StaticBuffer() : + _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 diff --git a/dep/ggpo-x/src/sync.cpp b/dep/ggpo-x/src/sync.cpp new file mode 100644 index 000000000..2440e4179 --- /dev/null +++ b/dep/ggpo-x/src/sync.cpp @@ -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; +} + + diff --git a/dep/ggpo-x/src/sync.h b/dep/ggpo-x/src/sync.h new file mode 100644 index 000000000..76308ae2b --- /dev/null +++ b/dep/ggpo-x/src/sync.h @@ -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 +#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 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_queue; + UdpMsg::connect_status *_local_connect_status; +}; + +#endif + diff --git a/dep/ggpo-x/src/timesync.cpp b/dep/ggpo-x/src/timesync.cpp new file mode 100644 index 000000000..4fb8d6beb --- /dev/null +++ b/dep/ggpo-x/src/timesync.cpp @@ -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 +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); +} diff --git a/dep/ggpo-x/src/timesync.h b/dep/ggpo-x/src/timesync.h new file mode 100644 index 000000000..2452d16aa --- /dev/null +++ b/dep/ggpo-x/src/timesync.h @@ -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 diff --git a/dep/ggpo-x/src/types.h b/dep/ggpo-x/src/types.h new file mode 100644 index 000000000..d4d5080cb --- /dev/null +++ b/dep/ggpo-x/src/types.h @@ -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 +/* + * 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 diff --git a/duckstation.sln b/duckstation.sln index 0e655dbbe..a6949ecfb 100644 --- a/duckstation.sln +++ b/duckstation.sln @@ -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}