diff --git a/README.md b/README.md index 1d20ceeb..13c86f0d 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,21 @@ It's designed around the networking requirements of competitive multiplayer games like first person shooters. +Generally speaking, if you have a custom game engine written in C++ and you want to network your game, yojimbo is a good choice. + +![image](https://github.com/mas-bandwidth/yojimbo/assets/696656/098935f2-ba2b-4540-8d7f-474acc7f2cd8) + It has the following features: * Cryptographically secure authentication via [connect tokens](https://github.com/networkprotocol/netcode/blob/master/STANDARD.md) * Client/server connection management and timeouts * Encrypted and signed packets sent over UDP * Packet fragmentation and reassembly -* Reliable-ordered messages and data blocks -* Estimates of packet loss, latency and bandwidth usage +* Bitpacker and serialization system +* Unreliable-unordered messages for time sensitive data +* Reliable-ordered messages with aggressive resend until ack +* Data blocks larger than maximum packet size can be attached to reliable-ordered messages +* Estimates of latency, jitter, packet loss, bandwidth sent, received and acked per-connection yojimbo is stable and production ready. @@ -23,31 +30,15 @@ You can get the latest source code by cloning it from github: git clone https://github.com/mas-bandwidth/yojimbo.git -Alternatively, you can download one of the latest [releases](https://github.com/mas-bandwidth/yojimbo/releases) +Alternatively, you can download the latest [release](https://github.com/mas-bandwidth/yojimbo/releases). ## Author -The author of this library is Glenn Fiedler. - -Open source libraries by the same author include: [netcode](https://github.com/mas-bandwidth/netcode), [reliable](https://github.com/mas-bandwidth/reliable), and [serialize](https://github.com/mas-bandwidth/serialize) - -## Sponsors +The author of this library is [Glenn Fiedler](https://www.linkedin.com/in/glenn-fiedler-11b735302/). -**yojimbo** was generously sponsored by: +Yojimbo is built on top of other open source libraries by the same author: [netcode](https://github.com/mas-bandwidth/netcode), [reliable](https://github.com/mas-bandwidth/reliable), and [serialize](https://github.com/mas-bandwidth/serialize) -* **Gold Sponsors** - * [Remedy Entertainment](http://www.remedygames.com/) - * [Cloud Imperium Games](https://cloudimperiumgames.com) - -* **Silver Sponsors** - * [Moon Studios](http://www.oriblindforest.com/#!moon-3/) - * The Network Protocol Company - -* **Bronze Sponsors** - * Kite & Lightning - * [Data Realms](http://datarealms.com) - -And by individual supporters on Patreon. Thank you. You made this possible! +If you find this software useful, please consider [sponsoring it](https://github.com/sponsors/mas-bandwidth). Thanks! ## License diff --git a/USAGE.md b/USAGE.md index f9c826c3..c7f7bf2d 100644 --- a/USAGE.md +++ b/USAGE.md @@ -200,7 +200,7 @@ OnlineGameScreen::OnlineGameScreen(const yojimbo::Address& serverAddress) : m_client(yojimbo::GetDefaultAllocator(), yojimbo::Address("0.0.0.0"), m_connectionConfig, m_adapter, 0.0) { uint64_t clientId; - yojimbo::random_bytes((uint8_t*)&clientId, 8); + yojimbo_random_bytes((uint8_t*)&clientId, 8); m_client.InsecureConnect(DEFAULT_PRIVATE_KEY, clientId, m_serverAddress); } ``` diff --git a/include/yojimbo_base_client.h b/include/yojimbo_base_client.h index 6910c33a..db7d3c80 100644 --- a/include/yojimbo_base_client.h +++ b/include/yojimbo_base_client.h @@ -46,7 +46,7 @@ namespace yojimbo @param allocator The allocator for all memory used by the client. @param config The base client/server configuration. @param time The current time in seconds. See ClientInterface::AdvanceTime - @param allocator The adapter to the game program. Specifies allocators, message factory to use etc. + @param adapter The adapter to the game program. Specifies allocators, message factory to use etc. */ explicit BaseClient( class Allocator & allocator, const ClientServerConfig & config, class Adapter & adapter, double time ); diff --git a/include/yojimbo_client_interface.h b/include/yojimbo_client_interface.h index 88b0c927..35fdd432 100644 --- a/include/yojimbo_client_interface.h +++ b/include/yojimbo_client_interface.h @@ -102,7 +102,7 @@ namespace yojimbo /** Is the client connected to a server? - This is true once a client successfully finishes connection negotiatio, and connects to a server. It is false while connecting to a server. + This is true once a client successfully finishes connection negotiation, and connects to a server. It is false while connecting to a server. @returns true if the client is connected to a server. */ diff --git a/include/yojimbo_config.h b/include/yojimbo_config.h index 541d1a30..12954043 100644 --- a/include/yojimbo_config.h +++ b/include/yojimbo_config.h @@ -32,8 +32,8 @@ #endif #define YOJIMBO_MAJOR_VERSION 1 -#define YOJIMBO_MINOR_VERSION 0 -#define YOJIMBO_PATCH_VERSION 0 +#define YOJIMBO_MINOR_VERSION 2 +#define YOJIMBO_PATCH_VERSION 4 #if !defined(YOJIMBO_DEBUG) && !defined(YOJIMBO_RELEASE) #if defined(NDEBUG) @@ -170,8 +170,9 @@ namespace yojimbo ConnectionConfig() { - numChannels = 1; + numChannels = 2; maxPacketSize = 8 * 1024; + channel[0].type = CHANNEL_TYPE_RELIABLE_ORDERED; } }; diff --git a/include/yojimbo_constants.h b/include/yojimbo_constants.h index 3833c631..1ff45a1e 100644 --- a/include/yojimbo_constants.h +++ b/include/yojimbo_constants.h @@ -31,7 +31,7 @@ namespace yojimbo { const int MaxClients = 64; ///< The maximum number of clients supported by this library. You can increase this if you want, but this library is designed around patterns that work best for [2,64] player games. If your game has less than 64 clients, reducing this will save memory. - const int MaxChannels = 64; ///< The maximum number of message channels supported by this library. If you need less than 64 channels per-packet, reducing this will save memory. + const int MaxChannels = 2; ///< The maximum number of message channels supported by this library. If you need less than 64 channels per-packet, reducing this will save memory. Minimum is 2. const int KeyBytes = 32; ///< Size of encryption key for dedicated client/server in bytes. Must be equal to key size for libsodium encryption primitive. Do not change. diff --git a/include/yojimbo_network_info.h b/include/yojimbo_network_info.h index 7195a192..0dc40af2 100644 --- a/include/yojimbo_network_info.h +++ b/include/yojimbo_network_info.h @@ -36,7 +36,13 @@ namespace yojimbo struct NetworkInfo { - float RTT; ///< Round trip time estimate (milliseconds). + float RTT; ///< Round trip time estimate (milliseconds). Exponentially smoothed average tracking most recent RTT value. + float minRTT; ///< Minimum RTT seen over the last n samples (see rtt_history_size in reliable config). This is a more stable and accurate RTT value under typical Wi-Fi jitter. + float maxRTT; ///< Maximum RTT seen over the last n samples. + float averageRTT; ///< Average RTT seen over the last n samples. + float averageJitter; ///< Average jitter relative to min RTT over the last n samples. + float maxJitter; ///< Max jitter relative to min RTT seen over the last n samples. + float stddevJitter; ///< One standard deviation of jitter relative to average RTT over the last n samples. float packetLoss; ///< Packet loss percent. float sentBandwidth; ///< Sent bandwidth (kbps). float receivedBandwidth; ///< Received bandwidth (kbps). diff --git a/include/yojimbo_server.h b/include/yojimbo_server.h index 33247b23..4db011e3 100644 --- a/include/yojimbo_server.h +++ b/include/yojimbo_server.h @@ -63,6 +63,8 @@ namespace yojimbo uint64_t GetClientId( int clientIndex ) const; + const uint8_t * GetClientUserData( int clientIndex ) const; + netcode_address_t * GetClientAddress( int clientIndex ) const; int GetNumConnectedClients() const; diff --git a/include/yojimbo_server_interface.h b/include/yojimbo_server_interface.h index a5b278b4..98b68af4 100644 --- a/include/yojimbo_server_interface.h +++ b/include/yojimbo_server_interface.h @@ -143,6 +143,13 @@ namespace yojimbo virtual uint64_t GetClientId( int clientIndex ) const = 0; + /** + Get the user data of the client. + @param clientIndex the index of the client slot in [0,maxClients-1], where maxClients corresponds to the value passed into the last call to Server::Start. + @returns The user data of the client. + */ + const uint8_t * GetClientUserData( int clientIndex ) const; + /** Get the address of the client @param clientIndex the index of the client slot in [0,maxClients-1], where maxClients corresponds to the value passed into the last call to Server::Start. diff --git a/matcher/Dockerfile b/matcher/Dockerfile new file mode 100644 index 00000000..2c7bc60f --- /dev/null +++ b/matcher/Dockerfile @@ -0,0 +1,28 @@ +FROM golang:1.20.13 AS matcher_build + +# Matcher +WORKDIR /matcher + +# Copy go.mod and go.sum files to the workspace separately and download dependecies. +# Doing this separately will cache these as its own separate layer +COPY ./go.mod . +COPY ./go.sum . +RUN go mod download + +# Copy the source code as the last step +COPY . . + +# Build the binary +RUN CGO_ENABLED=0 go build -o matcher.bin main.go + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +# Then we copy and run it from a slim image +FROM alpine:3.5 +WORKDIR /matcher + +COPY --from=matcher_build /matcher/matcher.bin . + +EXPOSE 8081 + +ENTRYPOINT ["/matcher/matcher.bin"] diff --git a/matcher/README.md b/matcher/README.md new file mode 100644 index 00000000..f63e28ef --- /dev/null +++ b/matcher/README.md @@ -0,0 +1,31 @@ +# Yojimbo Matcher Sample + +This is a sample matcher server written in go that will provide a connection token via the following endpoint: + +``` +GET /match/{protocolID}/{clientID} +``` + +# Building the Docker image: + +To build the image run the following command from the `matcher` directory: + +```sh +docker build --tag=matcher . +``` + +# Running the Docker container: + +Run the container image mapping the port to your host machine: + +```sh +docker run -d -p 8081:8081 --name matcher matcher +``` + +# Using the matcher: + +To hit the container with a test request: + +```sh +PROTOCOL_ID=123 && CLIENT_ID=42 && curl http://localhost:8081/match/${PROTOCOL_ID}/${CLIENT_ID} +``` diff --git a/matcher/go.mod b/matcher/go.mod new file mode 100644 index 00000000..c3f612d3 --- /dev/null +++ b/matcher/go.mod @@ -0,0 +1,11 @@ +module github.com/mas-bandwidth/matcher + +go 1.20 + +require ( + github.com/go-chi/chi/v5 v5.0.8 + github.com/pkg/errors v0.9.1 + golang.org/x/crypto v0.18.0 +) + +require golang.org/x/sys v0.16.0 // indirect diff --git a/matcher/go.sum b/matcher/go.sum new file mode 100644 index 00000000..c4f4c78c --- /dev/null +++ b/matcher/go.sum @@ -0,0 +1,8 @@ +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/matcher/main.go b/matcher/main.go new file mode 100644 index 00000000..256c5f63 --- /dev/null +++ b/matcher/main.go @@ -0,0 +1,255 @@ +package main + +import ( + "crypto/rand" + "encoding/base64" + "encoding/binary" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "strconv" + "time" + + "github.com/go-chi/chi/v5" + "github.com/pkg/errors" + "golang.org/x/crypto/chacha20poly1305" +) + +const ( + port = 8081 + serverAddress = "127.0.0.1" + serverPort = 40000 + keyBytes = 32 + authBytes = 16 + connectTokenExpiry = 45 + connectTokenBytes = 2048 + connectTokenPrivateBytes = 1024 + userDataBytes = 256 + timeoutSeconds = 5 + versionInfo = "NETCODE 1.02\x00" + verboseError = true + addressIPV4 = 1 + addressIPV6 = 2 +) + +var ( + stdoutLogger = log.New(os.Stdout, "yojimbo-matcher: ", log.Llongfile) + stderrLogger = log.New(os.Stderr, "yojimbo-matcher: ", log.Llongfile) + privateKey = []byte{ + 0x60, 0x6a, 0xbe, 0x6e, 0xc9, 0x19, 0x10, 0xea, + 0x9a, 0x65, 0x62, 0xf6, 0x6f, 0x2b, 0x30, 0xe4, + 0x43, 0x71, 0xd6, 0x2c, 0xd1, 0x99, 0x27, 0x26, + 0x6b, 0x3c, 0x60, 0xf4, 0xb7, 0x15, 0xab, 0xa1, + } +) + +func writeAddresses(buffer []byte, addresses []net.UDPAddr) int { + binary.LittleEndian.PutUint32(buffer[0:], (uint32)(len(addresses))) + offset := 4 + for _, addr := range addresses { + ipv4 := addr.IP.To4() + port := addr.Port + if ipv4 != nil { + buffer[offset] = addressIPV4 + buffer[offset+1] = ipv4[0] + buffer[offset+2] = ipv4[1] + buffer[offset+3] = ipv4[2] + buffer[offset+4] = ipv4[3] + buffer[offset+5] = (byte)(port & 0xFF) + buffer[offset+6] = (byte)(port >> 8) + offset += 7 + } else { + buffer[offset] = addressIPV6 + copy(buffer[offset+1:], addr.IP) + buffer[offset+17] = (byte)(port & 0xFF) + buffer[offset+18] = (byte)(port >> 8) + offset += 19 + } + } + return offset +} + +type connectTokenPrivate struct { + clientID uint64 + TimeoutSeconds int32 + ServerAddresses []net.UDPAddr + ClientToServerKey [keyBytes]byte + ServerToClientKey [keyBytes]byte + UserData [userDataBytes]byte +} + +func newConnectTokenPrivate(clientID uint64, serverAddresses []net.UDPAddr, timeoutSeconds int32, userData []byte, clientToServerKey []byte, serverToClientKey []byte) *connectTokenPrivate { + connectTokenPrivate := &connectTokenPrivate{} + connectTokenPrivate.clientID = clientID + connectTokenPrivate.TimeoutSeconds = timeoutSeconds + connectTokenPrivate.ServerAddresses = serverAddresses + copy(connectTokenPrivate.UserData[:], userData) + copy(connectTokenPrivate.ClientToServerKey[:], clientToServerKey) + copy(connectTokenPrivate.ServerToClientKey[:], serverToClientKey) + return connectTokenPrivate +} + +func (token *connectTokenPrivate) Write(buffer []byte) { + binary.LittleEndian.PutUint64(buffer[0:], token.clientID) + binary.LittleEndian.PutUint32(buffer[8:], (uint32)(token.TimeoutSeconds)) + addressBytes := writeAddresses(buffer[12:], token.ServerAddresses) + copy(buffer[12+addressBytes:], token.ClientToServerKey[:]) + copy(buffer[12+addressBytes+keyBytes:], token.ServerToClientKey[:]) + copy(buffer[12+addressBytes+keyBytes*2:], token.UserData[:]) +} + +type connectToken struct { + protocolID uint64 + CreateTimestamp uint64 + ExpireTimestamp uint64 + Sequence uint64 + PrivateData *connectTokenPrivate + TimeoutSeconds int32 + ServerAddresses []net.UDPAddr + ClientToServerKey [keyBytes]byte + ServerToClientKey [keyBytes]byte + PrivateKey [keyBytes]byte +} + +func newConnectToken(clientID uint64, serverAddresses []net.UDPAddr, protocolID uint64, expireSeconds uint64, timeoutSeconds int32, userData []byte, privateKey []byte) (*connectToken, error) { + connectToken := &connectToken{} + connectToken.protocolID = protocolID + connectToken.CreateTimestamp = uint64(time.Now().Unix()) + if expireSeconds >= 0 { + connectToken.ExpireTimestamp = connectToken.CreateTimestamp + expireSeconds + } else { + connectToken.ExpireTimestamp = 0xFFFFFFFFFFFFFFFF + } + connectToken.TimeoutSeconds = timeoutSeconds + connectToken.ServerAddresses = serverAddresses + err := fillWithRandomBytes(connectToken.ClientToServerKey[:]) + if err != nil { + return nil, errors.Wrap(err, "failed to fill client to server key with random bytes") + } + err = fillWithRandomBytes(connectToken.ServerToClientKey[:]) + if err != nil { + return nil, errors.Wrap(err, "failed to fill server to client key with random bytes") + } + copy(connectToken.PrivateKey[:], privateKey[:]) + connectToken.PrivateData = newConnectTokenPrivate(clientID, serverAddresses, timeoutSeconds, userData, connectToken.ClientToServerKey[:], connectToken.ServerToClientKey[:]) + return connectToken, nil +} + +func fillWithRandomBytes(buf []byte) error { + _, err := rand.Read(buf) + if err != nil { + return err + } + return nil +} + +func encryptAEAD(message []byte, additional []byte, nonce []byte, key []byte) error { + aead, err := chacha20poly1305.NewX(key) + if err != nil { + return errors.Wrap(err, "failed to create cipher") + } + + // Encrypt the message and append the authentication tag. + aead.Seal(message[:0], nonce, message, additional) + + return nil +} + +func (token *connectToken) Write(buffer []byte) error { + copy(buffer, versionInfo) + binary.LittleEndian.PutUint64(buffer[13:], token.protocolID) + binary.LittleEndian.PutUint64(buffer[21:], token.CreateTimestamp) + binary.LittleEndian.PutUint64(buffer[29:], token.ExpireTimestamp) + nonce := make([]byte, 24) + err := fillWithRandomBytes(nonce) + if err != nil { + return errors.Wrap(err, "failed to fill nonce with random bytes") + } + copy(buffer[37:], nonce[:]) + token.PrivateData.Write(buffer[61:]) + additional := make([]byte, 13+8+8) + copy(additional, versionInfo[0:13]) + binary.LittleEndian.PutUint64(additional[13:], token.protocolID) + binary.LittleEndian.PutUint64(additional[21:], token.ExpireTimestamp) + err = encryptAEAD(buffer[61:61+connectTokenPrivateBytes-authBytes], additional[:], nonce[:], token.PrivateKey[:]) + if err != nil { + return errors.Wrap(err, "failed to encrypt message") + } + binary.LittleEndian.PutUint32(buffer[connectTokenPrivateBytes+61:], (uint32)(token.TimeoutSeconds)) + offset := writeAddresses(buffer[connectTokenPrivateBytes+61+4:], token.ServerAddresses) + copy(buffer[connectTokenPrivateBytes+61+4+offset:], token.ClientToServerKey[:]) + copy(buffer[connectTokenPrivateBytes+61+4+offset+keyBytes:], token.ServerToClientKey[:]) + return nil +} + +func generateConnectToken(clientID uint64, serverAddresses []net.UDPAddr, protocolID uint64, expireSeconds uint64, timeoutSeconds int32, userData []byte, privateKey []byte) ([]byte, error) { + connectToken, err := newConnectToken(clientID, serverAddresses, protocolID, expireSeconds, timeoutSeconds, userData, privateKey) + if err != nil { + return nil, errors.Wrap(err, "failed to create connect token") + } + buffer := make([]byte, connectTokenBytes) + err = connectToken.Write(buffer) + if err != nil { + return nil, errors.Wrap(err, "failed to serialize connect token") + } + return buffer, nil +} + +func writeError(w http.ResponseWriter, err error, statusCode int) { + stderrLogger.Printf("%+v\n", err) + errMessage := "An error occured on the server while processing the request" + if verboseError { + errMessage = err.Error() + } + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(statusCode) + fmt.Fprint(w, errMessage) +} + +func matchHandler(w http.ResponseWriter, r *http.Request) { + + clientID, err := strconv.ParseUint(chi.URLParam(r, "clientID"), 10, 64) + if err != nil { + writeError(w, fmt.Errorf("Unable to parse clientID: %s", chi.URLParam(r, "clientID")), http.StatusBadRequest) + return + } + protocolID, err := strconv.ParseUint(chi.URLParam(r, "protocolID"), 10, 64) + if err != nil { + writeError(w, fmt.Errorf("Unable to parse protocolID: %s", chi.URLParam(r, "protocolID")), http.StatusBadRequest) + return + } + + serverAddresses := make([]net.UDPAddr, 1) + serverAddresses[0] = net.UDPAddr{IP: net.ParseIP(serverAddress), Port: serverPort} + + userData := make([]byte, userDataBytes) + connectToken, err := generateConnectToken(clientID, serverAddresses, protocolID, connectTokenExpiry, timeoutSeconds, userData, privateKey) + if err != nil { + writeError(w, errors.Wrap(err, "Failed to generate connect token"), http.StatusInternalServerError) + return + } + connectTokenBase64 := base64.StdEncoding.EncodeToString(connectToken) + w.Header().Set("Content-Type", "application/text") + _, err = io.WriteString(w, connectTokenBase64) + if err != nil { + writeError(w, errors.Wrap(err, "Failed to write response"), http.StatusInternalServerError) + return + } + stderrLogger.Printf("Matched client %.16x to %s:%d\n", clientID, serverAddress, serverPort) +} + +func main() { + stderrLogger.Printf("Started matchmaker on port %d\n", port) + + router := chi.NewRouter() + router.Get("/match/{protocolID:[0-9]+}/{clientID:[0-9]+}", matchHandler) + + err := http.ListenAndServe(":"+strconv.Itoa(port), router) + if err != nil { + stderrLogger.Fatalf("%+v\n", err) + } +} diff --git a/premake5.lua b/premake5.lua index f2f1f715..e9d2640b 100644 --- a/premake5.lua +++ b/premake5.lua @@ -7,7 +7,7 @@ solution "Yojimbo" configurations { "Debug", "Release" } includedirs { ".", "include", "sodium", "tlsf", "netcode", "reliable", "serialize" } if not os.istarget "windows" then - targetdir "bin/" + targetdir "bin/" end rtti "Off" warnings "Extra" @@ -64,6 +64,7 @@ project "client" links { "yojimbo", "sodium-builtin", "tlsf", "netcode", "reliable" } filter "system:not windows" links { "yojimbo", "sodium", "tlsf", "netcode", "reliable" } + libdirs { "/opt/homebrew/lib" } project "server" files { "server.cpp", "shared.h" } @@ -71,6 +72,7 @@ project "server" links { "yojimbo", "sodium-builtin", "tlsf", "netcode", "reliable" } filter "system:not windows" links { "yojimbo", "sodium", "tlsf", "netcode", "reliable" } + libdirs { "/opt/homebrew/lib" } project "loopback" files { "loopback.cpp", "shared.h" } @@ -78,6 +80,7 @@ project "loopback" links { "yojimbo", "sodium-builtin", "tlsf", "netcode", "reliable" } filter "system:not windows" links { "yojimbo", "sodium", "tlsf", "netcode", "reliable" } + libdirs { "/opt/homebrew/lib" } project "soak" files { "soak.cpp", "shared.h" } @@ -85,6 +88,7 @@ project "soak" links { "yojimbo", "sodium-builtin", "tlsf", "netcode", "reliable" } filter "system:not windows" links { "yojimbo", "sodium", "tlsf", "netcode", "reliable" } + libdirs { "/opt/homebrew/lib" } project "test" files { "test.cpp" } @@ -93,6 +97,7 @@ project "test" links { "yojimbo", "sodium-builtin", "tlsf", "netcode", "reliable" } filter "system:not windows" links { "yojimbo", "sodium", "tlsf", "netcode", "reliable" } + libdirs { "/opt/homebrew/lib" } newaction { @@ -102,7 +107,7 @@ newaction execute = function () - files_to_delete = + files_to_delete = { "Makefile", "*.make", @@ -120,7 +125,7 @@ newaction "*.xcworkspace" } - directories_to_delete = + directories_to_delete = { "obj", "ipch", diff --git a/source/yojimbo_base_client.cpp b/source/yojimbo_base_client.cpp index 01c8efeb..eab88c26 100644 --- a/source/yojimbo_base_client.cpp +++ b/source/yojimbo_base_client.cpp @@ -235,24 +235,32 @@ namespace yojimbo bool BaseClient::CanSendMessage( int channelIndex ) const { yojimbo_assert( m_connection ); + yojimbo_assert( channelIndex >= 0 ); + yojimbo_assert( channelIndex < m_config.numChannels ); return m_connection->CanSendMessage( channelIndex ); } bool BaseClient::HasMessagesToSend( int channelIndex ) const { yojimbo_assert( m_connection ); + yojimbo_assert( channelIndex >= 0 ); + yojimbo_assert( channelIndex < m_config.numChannels ); return m_connection->HasMessagesToSend( channelIndex ); } void BaseClient::SendMessage( int channelIndex, Message * message ) { yojimbo_assert( m_connection ); + yojimbo_assert( channelIndex >= 0 ); + yojimbo_assert( channelIndex < m_config.numChannels ); m_connection->SendMessage( channelIndex, message, GetContext() ); } Message * BaseClient::ReceiveMessage( int channelIndex ) { yojimbo_assert( m_connection ); + yojimbo_assert( channelIndex >= 0 ); + yojimbo_assert( channelIndex < m_config.numChannels ); return m_connection->ReceiveMessage( channelIndex ); } @@ -273,6 +281,12 @@ namespace yojimbo info.numPacketsReceived = counters[RELIABLE_ENDPOINT_COUNTER_NUM_PACKETS_RECEIVED]; info.numPacketsAcked = counters[RELIABLE_ENDPOINT_COUNTER_NUM_PACKETS_ACKED]; info.RTT = reliable_endpoint_rtt( m_endpoint ); + info.minRTT = reliable_endpoint_rtt_min( m_endpoint ); + info.maxRTT = reliable_endpoint_rtt_min( m_endpoint ); + info.averageRTT = reliable_endpoint_rtt_avg( m_endpoint ); + info.averageJitter = reliable_endpoint_jitter_avg_vs_min_rtt( m_endpoint ); + info.maxJitter = reliable_endpoint_jitter_max_vs_min_rtt( m_endpoint ); + info.stddevJitter = reliable_endpoint_jitter_stddev_vs_avg_rtt( m_endpoint ); info.packetLoss = reliable_endpoint_packet_loss( m_endpoint ); reliable_endpoint_bandwidth( m_endpoint, &info.sentBandwidth, &info.receivedBandwidth, &info.ackedBandwidth ); } diff --git a/source/yojimbo_base_server.cpp b/source/yojimbo_base_server.cpp index d60c4403..7f31d1c2 100644 --- a/source/yojimbo_base_server.cpp +++ b/source/yojimbo_base_server.cpp @@ -45,24 +45,33 @@ namespace yojimbo void BaseServer::Start( int maxClients ) { + yojimbo_assert( maxClients <= MaxClients ); + Stop(); + m_running = true; m_maxClients = maxClients; + yojimbo_assert( !m_globalMemory ); yojimbo_assert( !m_globalAllocator ); m_globalMemory = (uint8_t*) YOJIMBO_ALLOCATE( *m_allocator, m_config.serverGlobalMemory ); + m_globalAllocator = m_adapter->CreateAllocator( *m_allocator, m_globalMemory, m_config.serverGlobalMemory ); yojimbo_assert( m_globalAllocator ); + if ( m_config.networkSimulator ) { m_networkSimulator = YOJIMBO_NEW( *m_globalAllocator, NetworkSimulator, *m_globalAllocator, m_config.maxSimulatorPackets, m_time ); } + for ( int i = 0; i < m_maxClients; ++i ) { yojimbo_assert( !m_clientMemory[i] ); yojimbo_assert( !m_clientAllocator[i] ); m_clientMemory[i] = (uint8_t*) YOJIMBO_ALLOCATE( *m_allocator, m_config.serverPerClientMemory ); + yojimbo_assert( m_clientMemory[i] ); + m_clientAllocator[i] = m_adapter->CreateAllocator( *m_allocator, m_clientMemory[i], m_config.serverPerClientMemory ); yojimbo_assert( m_clientAllocator[i] ); @@ -104,6 +113,8 @@ namespace yojimbo yojimbo_assert( m_globalMemory ); yojimbo_assert( m_globalAllocator ); YOJIMBO_DELETE( *m_globalAllocator, NetworkSimulator, m_networkSimulator ); + yojimbo_assert( m_maxClients > 0 ); + yojimbo_assert( m_maxClients <= MaxClients ); for ( int i = 0; i < m_maxClients; ++i ) { yojimbo_assert( m_clientMemory[i] ); @@ -119,6 +130,14 @@ namespace yojimbo YOJIMBO_DELETE( *m_allocator, Allocator, m_globalAllocator ); YOJIMBO_FREE( *m_allocator, m_globalMemory ); } + for ( int i = 0; i < MaxClients; ++i ) + { + m_clientMemory[i] = NULL; + m_clientAllocator[i] = NULL; + m_clientMessageFactory[i] = NULL; + m_clientConnection[i] = NULL; + m_clientEndpoint[i] = NULL; + } m_running = false; m_maxClients = 0; m_packetBuffer = NULL; @@ -223,6 +242,8 @@ namespace yojimbo { yojimbo_assert( clientIndex >= 0 ); yojimbo_assert( clientIndex < m_maxClients ); + yojimbo_assert( channelIndex >= 0 ); + yojimbo_assert( channelIndex < m_config.numChannels ); yojimbo_assert( m_clientConnection[clientIndex] ); return m_clientConnection[clientIndex]->CanSendMessage( channelIndex ); } @@ -232,6 +253,8 @@ namespace yojimbo yojimbo_assert( clientIndex >= 0 ); yojimbo_assert( clientIndex < m_maxClients ); yojimbo_assert( m_clientConnection[clientIndex] ); + yojimbo_assert( channelIndex >= 0 ); + yojimbo_assert( channelIndex < m_config.numChannels ); return m_clientConnection[clientIndex]->HasMessagesToSend( channelIndex ); } @@ -240,6 +263,8 @@ namespace yojimbo yojimbo_assert( clientIndex >= 0 ); yojimbo_assert( clientIndex < m_maxClients ); yojimbo_assert( m_clientConnection[clientIndex] ); + yojimbo_assert( channelIndex >= 0 ); + yojimbo_assert( channelIndex < m_config.numChannels ); return m_clientConnection[clientIndex]->SendMessage( channelIndex, message, GetContext() ); } @@ -248,6 +273,8 @@ namespace yojimbo yojimbo_assert( clientIndex >= 0 ); yojimbo_assert( clientIndex < m_maxClients ); yojimbo_assert( m_clientConnection[clientIndex] ); + yojimbo_assert( channelIndex >= 0 ); + yojimbo_assert( channelIndex < m_config.numChannels ); return m_clientConnection[clientIndex]->ReceiveMessage( channelIndex ); } diff --git a/source/yojimbo_reliable_ordered_channel.cpp b/source/yojimbo_reliable_ordered_channel.cpp index 41492860..711e861a 100644 --- a/source/yojimbo_reliable_ordered_channel.cpp +++ b/source/yojimbo_reliable_ordered_channel.cpp @@ -264,7 +264,7 @@ namespace yojimbo uint16_t previousMessageId = 0; int usedBits = ConservativeMessageHeaderBits; int giveUpCounter = 0; -#if YOJIMBO_DEBUG +#ifdef YOJIMBO_DEBUG const int maxBits = availableBits; #endif // YOJIMBO_DEBUG diff --git a/source/yojimbo_server.cpp b/source/yojimbo_server.cpp index 634bbc22..dd16ffeb 100644 --- a/source/yojimbo_server.cpp +++ b/source/yojimbo_server.cpp @@ -7,17 +7,17 @@ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ @@ -31,7 +31,7 @@ namespace yojimbo { - Server::Server( Allocator & allocator, const uint8_t privateKey[], const Address & address, const ClientServerConfig & config, Adapter & adapter, double time ) + Server::Server( Allocator & allocator, const uint8_t privateKey[], const Address & address, const ClientServerConfig & config, Adapter & adapter, double time ) : BaseServer( allocator, config, adapter, time ) { yojimbo_assert( KeyBytes == NETCODE_KEY_BYTES ); @@ -50,14 +50,13 @@ namespace yojimbo void Server::Start( int maxClients ) { - if ( IsRunning() ) - Stop(); - + yojimbo_assert( maxClients <= MaxClients ); + BaseServer::Start( maxClients ); - + char addressString[MaxAddressLength]; m_address.ToString( addressString, MaxAddressLength ); - + struct netcode_server_config_t netcodeConfig; netcode_default_server_config(&netcodeConfig); netcodeConfig.protocol_id = m_config.protocolId; @@ -68,15 +67,15 @@ namespace yojimbo netcodeConfig.callback_context = this; netcodeConfig.connect_disconnect_callback = StaticConnectDisconnectCallbackFunction; netcodeConfig.send_loopback_packet_callback = StaticSendLoopbackPacketCallbackFunction; - + m_server = netcode_server_create(addressString, &netcodeConfig, GetTime()); - + if ( !m_server ) { Stop(); return; } - + netcode_server_start( m_server, maxClients ); m_boundAddress.SetPort( netcode_server_get_port( m_server ) ); @@ -186,6 +185,11 @@ namespace yojimbo return netcode_server_client_id( m_server, clientIndex ); } + const uint8_t * Server::GetClientUserData( int clientIndex ) const + { + return (const uint8_t*)netcode_server_client_user_data( m_server, clientIndex ); + } + netcode_address_t * Server::GetClientAddress( int clientIndex ) const { return netcode_server_client_address( m_server, clientIndex ); diff --git a/test.cpp b/test.cpp index 480b27c8..fbb90ffc 100644 --- a/test.cpp +++ b/test.cpp @@ -618,6 +618,8 @@ void PumpConnectionUpdate( ConnectionConfig & connectionConfig, double & time, C receiverSequence++; } +const int ReliableChannel = 0; + void test_connection_reliable_ordered_messages() { TestMessageFactory messageFactory( GetDefaultAllocator() ); @@ -636,7 +638,7 @@ void test_connection_reliable_ordered_messages() TestMessage * message = (TestMessage*) messageFactory.CreateMessage( TEST_MESSAGE ); check( message ); message->sequence = i; - sender.SendMessage( 0, message ); + sender.SendMessage( ReliableChannel, message ); } const int SenderPort = 10000; @@ -658,7 +660,7 @@ void test_connection_reliable_ordered_messages() while ( true ) { - Message * message = receiver.ReceiveMessage( 0 ); + Message * message = receiver.ReceiveMessage( ReliableChannel ); if ( !message ) break; @@ -704,7 +706,7 @@ void test_connection_reliable_ordered_blocks() for ( int j = 0; j < blockSize; ++j ) blockData[j] = i + j; message->AttachBlock( messageFactory.GetAllocator(), blockData, blockSize ); - sender.SendMessage( 0, message ); + sender.SendMessage( ReliableChannel, message ); } const int SenderPort = 10000; @@ -726,7 +728,7 @@ void test_connection_reliable_ordered_blocks() while ( true ) { - Message * message = receiver.ReceiveMessage( 0 ); + Message * message = receiver.ReceiveMessage( ReliableChannel ); if ( !message ) break; @@ -784,7 +786,7 @@ void test_connection_reliable_ordered_messages_and_blocks() TestMessage * message = (TestMessage*) messageFactory.CreateMessage( TEST_MESSAGE ); check( message ); message->sequence = i; - sender.SendMessage( 0, message ); + sender.SendMessage( ReliableChannel, message ); } else { @@ -796,7 +798,7 @@ void test_connection_reliable_ordered_messages_and_blocks() for ( int j = 0; j < blockSize; ++j ) blockData[j] = i + j; message->AttachBlock( messageFactory.GetAllocator(), blockData, blockSize ); - sender.SendMessage( 0, message ); + sender.SendMessage( ReliableChannel, message ); } } @@ -819,7 +821,7 @@ void test_connection_reliable_ordered_messages_and_blocks() while ( true ) { - Message * message = receiver.ReceiveMessage( 0 ); + Message * message = receiver.ReceiveMessage( ReliableChannel ); if ( !message ) break; @@ -875,6 +877,9 @@ void test_connection_reliable_ordered_messages_and_blocks_multiple_channels() { const int NumChannels = 2; + yojimbo_assert( NumChannels >= 0 ); + yojimbo_assert( NumChannels <= MaxChannels ); + double time = 100.0; TestMessageFactory messageFactory( GetDefaultAllocator() ); @@ -1179,7 +1184,7 @@ void PumpClientServerUpdate( double & time, Client ** client, int numClients, Se yojimbo_sleep( 0.0f ); } -void SendClientToServerMessages( Client & client, int numMessagesToSend, int channelIndex = 0 ) +void SendClientToServerMessages( Client & client, int numMessagesToSend, int channelIndex = ReliableChannel ) { for ( int i = 0; i < numMessagesToSend; ++i ) { @@ -1209,7 +1214,7 @@ void SendClientToServerMessages( Client & client, int numMessagesToSend, int cha } } -void SendServerToClientMessages( Server & server, int clientIndex, int numMessagesToSend, int channelIndex = 0 ) +void SendServerToClientMessages( Server & server, int clientIndex, int numMessagesToSend, int channelIndex = ReliableChannel ) { for ( int i = 0; i < numMessagesToSend; ++i ) { @@ -1239,11 +1244,11 @@ void SendServerToClientMessages( Server & server, int clientIndex, int numMessag } } -void ProcessServerToClientMessages( Client & client, int & numMessagesReceivedFromServer ) +void ProcessServerToClientMessages( Client & client, int & numMessagesReceivedFromServer, int channelIndex = ReliableChannel ) { while ( true ) { - Message * message = client.ReceiveMessage( 0 ); + Message * message = client.ReceiveMessage( channelIndex ); if ( !message ) break; @@ -1283,11 +1288,11 @@ void ProcessServerToClientMessages( Client & client, int & numMessagesReceivedFr } } -void ProcessClientToServerMessages( Server & server, int clientIndex, int & numMessagesReceivedFromClient ) +void ProcessClientToServerMessages( Server & server, int clientIndex, int & numMessagesReceivedFromClient, int channelIndex = ReliableChannel ) { while ( true ) { - Message * message = server.ReceiveMessage( clientIndex, 0 ); + Message * message = server.ReceiveMessage( clientIndex, channelIndex ); if ( !message ) break; @@ -1511,19 +1516,23 @@ void test_client_server_start_stop_restart() Server server( GetDefaultAllocator(), privateKey, serverAddress, config, adapter, time ); - server.Start( MaxClients ); - server.SetLatency( 250 ); server.SetJitter( 100 ); server.SetPacketLoss( 25 ); server.SetDuplicates( 25 ); - int numClients[] = { 3, 5, 1 }; + int numClients[] = { 3, 5, 1, 32, 5 }; const int NumIterations = sizeof( numClients ) / sizeof( int ); for ( int iteration = 0; iteration < NumIterations; ++iteration ) { + numClients[iteration] = numClients[iteration] % MaxClients; + if ( numClients[iteration] == 0 ) + { + numClients[iteration] = 1; + } + server.Start( numClients[iteration] ); Client * clients[MaxClients]; @@ -1908,6 +1917,8 @@ void test_reliable_outbound_sequence_outdated() config.numChannels = 2; config.timeout = -1; + yojimbo_assert( config.numChannels <= MaxChannels ); + const int BlockSize = config.channel[0].blockFragmentSize * 2; Client client( GetDefaultAllocator(), clientAddress, config, adapter, time );