├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── linux.yml │ └── windows.yml ├── .gitignore ├── .gitmodules ├── CODE_GUIDELINES.md ├── Code ├── connect │ ├── include │ │ ├── Client.hpp │ │ ├── Packet.hpp │ │ ├── Server.hpp │ │ ├── SteamInterface.hpp │ │ └── SynchronizedClock.hpp │ └── src │ │ ├── Client.cpp │ │ ├── Packet.cpp │ │ ├── Server.cpp │ │ ├── SteamInterface.cpp │ │ └── SynchronizedClock.cpp └── tests │ └── src │ ├── connect.cpp │ └── main.cpp ├── LICENSE ├── Makefile ├── README.md ├── azure-pipelines.yml └── xmake.lua /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{c,h,cpp,hpp}] 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | *.sh text eol=lf 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: tiltedphoques 2 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Build linux 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-20.04] 12 | arch: [x64] 13 | mode: [debug, release] 14 | 15 | runs-on: ${{ matrix.os }} 16 | if: "!contains(github.event.head_commit.message, 'ci skip')" 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | # Install Nazara dependencies 22 | - name: Update apt repositories 23 | run: sudo apt-get update 24 | 25 | # Install xmake 26 | - name: Setup xmake 27 | uses: xmake-io/github-action-setup-xmake@v1 28 | 29 | # Update xmake repository (in order to have the file that will be cached) 30 | - name: Update xmake repository 31 | run: xmake repo --update 32 | 33 | # Cache xmake dependencies 34 | - uses: actions/cache@v2 35 | with: 36 | path: ~/.xmake/packages 37 | key: ${{ runner.os }}-${{ matrix.arch }}-${{ matrix.mode }}-${{ hashFiles('xmake.lua', 'xmake-repo') }}-${{ hashFiles('~/.xmake/xmake.conf') }}-${{ hashFiles('~/.xmake/repositories/**') }} 38 | 39 | # Setup compilation mode and install project dependencies 40 | - name: Configure xmake and install dependencies 41 | run: xmake config --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --yes --verbose 42 | 43 | # Build the game 44 | - name: Build 45 | run: xmake 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Build windows 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | os: [windows-latest] 12 | arch: [x86, x64] 13 | mode: [release, debug] 14 | 15 | runs-on: ${{ matrix.os }} 16 | if: "!contains(github.event.head_commit.message, 'ci skip')" 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Checkout submodules 22 | run: | 23 | git submodule sync --recursive 24 | git submodule update --init --force --recursive --depth=1 25 | 26 | # Install xmake 27 | - name: Setup xmake 28 | uses: xmake-io/github-action-setup-xmake@v1 29 | 30 | # Update xmake repository (in order to have the file that will be cached) 31 | - name: Update xmake repository 32 | run: xmake.exe repo --update 33 | 34 | # Cache xmake dependencies 35 | - uses: actions/cache@v2 36 | with: 37 | path: C:\Users\runneradmin\AppData\Local\.xmake\packages 38 | key: ${{ runner.os }}-${{ matrix.arch }}-${{ matrix.mode }}-${{ hashFiles('xmake.lua') }}-${{ hashFiles('C:\Users\runneradmin\AppData\Local\.xmake\xmake.conf') }}-${{ hashFiles('C:\Users\runneradmin\AppData\Local\.xmake\repositories\**') }} 39 | 40 | # Setup compilation mode and install project dependencies 41 | - name: Configure xmake and install dependencies 42 | run: xmake.exe config --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --yes --verbose 43 | 44 | # Build the game 45 | - name: Build 46 | run: xmake.exe 47 | 48 | # No tests for TiltedConnect 49 | # Run tests 50 | #- name: Tests 51 | # run: build/windows/${{ matrix.arch }}/${{ matrix.mode }}/Tests.exe 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.dmp 2 | *.ilk 3 | Build/*.log 4 | Build/projects 5 | Build/obj 6 | Build/lib 7 | Build/bin 8 | *__pycache__* 9 | .xmake/* 10 | vsxmake* 11 | Build/* 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Libraries/TiltedCore"] 2 | path = Libraries/TiltedCore 3 | url = https://github.com/tiltedphoques/TiltedCore 4 | [submodule "ThirdParty/GameNetworkingSockets"] 5 | path = ThirdParty/GameNetworkingSockets 6 | url = https://github.com/tiltedphoques/GameNetworkingSockets 7 | -------------------------------------------------------------------------------- /CODE_GUIDELINES.md: -------------------------------------------------------------------------------- 1 | # Code guidelines 2 | 3 | ## Language 4 | 5 | We are using C++17, any C++17 feature supported by vs2017 is allowed. 6 | 7 | Please try to use templates responsibly, we don't want compilation times to explode and to deal with bloated binaries. 8 | 9 | Try to follow SRP as much as possible, a huge class containing tons of functionnalities is not better than many small components, it's easier to re-use them and to extend. 10 | 11 | ## Naming 12 | 13 | ### Variables 14 | 15 | The first letter is lower case, other words must start with an upper case : ``someVariableName``. 16 | 17 | Function arguments must be prefixed with an ``a`` for 'argument' : ``aFunctionArgument``. 18 | 19 | Const variables must be prefixed with a ``c`` for 'const' : ``const int cSomeInt;`` 20 | 21 | Pointers must be prefixed with a ``p`` for 'pointer' : ``int* pSomePointer``. 22 | 23 | The rules above must be used together for example ``void SomeFunc(const int* acpSomeArgument)``. 24 | 25 | Static variables must be prefixed with ``s_`` : ``static int s_someInt;``. 26 | 27 | Global variables must be prefixed with ``g_`` : ``extern int g_someGlobalInt;``. 28 | 29 | ### Classes 30 | 31 | Class names must start with an upper case : ``class SomeClass``. 32 | 33 | Class attributes must be prefixed with ``m_`` and must use the same rules as variables : ``int* m_pSomeMemberPointer;``. 34 | 35 | ### Functions 36 | 37 | All functions must start with an upper case : ``void SomeFunc();``. 38 | 39 | ## Generalities 40 | 41 | Names must be self explanatory, ``size_t a;`` is not acceptable, ``size_t incomingPacketCount;`` is good. 42 | 43 | ``auto`` is allowed when dealing with long names, it is not accepted for primitive types as we don't want the compiler to give us a signed int when we are using it as unsigned. 44 | 45 | Don't use java style blocks, a ``{`` needs to be on a new line. 46 | 47 | Don't use exceptions, don't use STL code that can throw, use the nothrow version if available or the unsafe version. Use the Outcome class to return the value or an error. 48 | -------------------------------------------------------------------------------- /Code/connect/include/Client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "steam/steamnetworkingsockets.h" 5 | #include "SteamInterface.hpp" 6 | #include "SynchronizedClock.hpp" 7 | 8 | namespace TiltedPhoques 9 | { 10 | struct Packet; 11 | struct Client 12 | { 13 | enum EDisconnectReason 14 | { 15 | kTimeout, 16 | kLocalProblem, 17 | kKicked, 18 | kCannotResolve, 19 | kAborted, 20 | kNormal 21 | }; 22 | 23 | struct Statistics 24 | { 25 | uint32_t SentBytes{}; 26 | uint32_t RecvBytes{}; 27 | uint32_t UncompressedSentBytes{}; 28 | uint32_t UncompressedRecvBytes{}; 29 | }; 30 | 31 | Client() noexcept; 32 | virtual ~Client(); 33 | 34 | Client(const Client&) = delete; 35 | Client& operator=(const Client&) = delete; 36 | 37 | Client(Client&&) noexcept; 38 | Client& operator=(Client&&) noexcept; 39 | 40 | bool Connect(const SteamNetworkingIPAddr& acEndpoint) noexcept; 41 | bool Connect(const std::string& acEndpoint) noexcept; 42 | bool ConnectByIp(const std::string& acEndpoint) noexcept; 43 | void Close() noexcept; 44 | 45 | void Update() noexcept; 46 | 47 | virtual void OnConsume(const void* apData, uint32_t aSize) = 0; 48 | virtual void OnConnected() = 0; 49 | virtual void OnDisconnected(EDisconnectReason aReason) = 0; 50 | virtual void OnUpdate() = 0; 51 | 52 | void Send(Packet* apPacket, EPacketFlags acPacketFlags = kReliable) const noexcept; 53 | 54 | [[nodiscard]] bool IsConnected() const noexcept; 55 | [[nodiscard]] SteamNetConnectionRealTimeStatus_t GetConnectionStatus() const noexcept; 56 | [[nodiscard]] Statistics GetStatistics() const noexcept; 57 | [[nodiscard]] const SynchronizedClock& GetClock() const noexcept; 58 | 59 | private: 60 | 61 | static void SteamNetConnectionStatusChangedCallback(SteamNetConnectionStatusChangedCallback_t* apInfo); 62 | static void UVGetAddrInfoCallback(void* apHandle, int aStatus, void* apResult); 63 | void OnSteamNetConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t* apInfo); 64 | 65 | void HandleMessage(const void* apData, uint32_t aSize) noexcept; 66 | void HandleServerTime(const void* apData, uint32_t aSize) noexcept; 67 | void HandleCompressedPayload(const void* apData, uint32_t aSize) noexcept; 68 | 69 | HSteamNetConnection m_connection; 70 | ISteamNetworkingSockets* m_pInterface; 71 | SynchronizedClock m_clock; 72 | uint64_t m_lastStatisticsPoint{}; 73 | mutable Statistics m_currentFrame{}; 74 | Statistics m_previousFrame{}; 75 | void* m_pLoop{}; 76 | void* m_pHandle{}; 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /Code/connect/include/Packet.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace TiltedPhoques 7 | { 8 | struct Packet 9 | { 10 | TP_ALLOCATOR; 11 | 12 | Packet() noexcept; 13 | Packet(uint32_t aSize) noexcept; 14 | virtual ~Packet() noexcept; 15 | 16 | TP_NOCOPYMOVE(Packet); 17 | 18 | // Get the start of the write buffer 19 | [[nodiscard]] char* GetData() const noexcept; 20 | 21 | // Get the writable data size, note that the actual packet size may differ from that 22 | [[nodiscard]] uint32_t GetSize() const noexcept; 23 | 24 | // Get the writable data size + protocol data size 25 | [[nodiscard]] uint32_t GetTotalSize() const noexcept; 26 | 27 | // Returns true if the packet has an associated buffer, usually used to check if the underlying allocator had enough space 28 | [[nodiscard]] bool IsValid() const noexcept; 29 | 30 | protected: 31 | 32 | friend struct Server; 33 | friend struct Client; 34 | 35 | char* m_pData; 36 | uint32_t m_size; 37 | }; 38 | 39 | struct PacketView : Packet 40 | { 41 | PacketView(char* aPointer, uint32_t aSize); 42 | virtual ~PacketView(); 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /Code/connect/include/Server.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "SteamInterface.hpp" 7 | #include 8 | 9 | namespace TiltedPhoques 10 | { 11 | struct Packet; 12 | struct Server 13 | { 14 | Server() noexcept; 15 | virtual ~Server(); 16 | 17 | TP_NOCOPYMOVE(Server); 18 | 19 | bool Host(uint16_t aPort, uint32_t aTickRate, bool bEnableDualStackIP = true) noexcept; 20 | void Close() noexcept; 21 | 22 | void Update() noexcept; 23 | 24 | enum EDisconnectReason : int 25 | { 26 | Unknown, 27 | Quit, 28 | Kicked, 29 | Banned, 30 | BadConnection, 31 | TimedOut 32 | }; 33 | 34 | virtual void OnUpdate() = 0; 35 | virtual void OnConsume(const void* apData, uint32_t aSize, ConnectionId_t aConnectionId) = 0; 36 | virtual void OnConnection(ConnectionId_t aHandle) = 0; 37 | virtual void OnDisconnection(ConnectionId_t aConnectionId, 38 | EDisconnectReason aReason) = 0; 39 | 40 | void SendToAll(Packet* apPacket, EPacketFlags aPacketFlags = kReliable) noexcept; 41 | void Send(ConnectionId_t aConnectionId, Packet* apPacket, EPacketFlags aPacketFlags = kReliable) const noexcept; 42 | void Kick(ConnectionId_t aConnectionId) noexcept; 43 | 44 | [[nodiscard]] uint16_t GetPort() const noexcept; 45 | [[nodiscard]] bool IsListening() const noexcept; 46 | [[nodiscard]] uint32_t GetClientCount() const noexcept; 47 | [[nodiscard]] uint32_t GetTickRate() const noexcept; 48 | [[nodiscard]] uint64_t GetTick() const noexcept; 49 | [[nodiscard]] SteamNetConnectionInfo_t GetConnectionInfo(ConnectionId_t aConnectionId) const noexcept; 50 | [[nodiscard]] bool IsAlive(ConnectionId_t aConnectionId) const noexcept; 51 | 52 | private: 53 | 54 | void Remove(ConnectionId_t aId) noexcept; 55 | 56 | void HandleMessage(const void* apData, uint32_t aSize, ConnectionId_t aConnectionId) noexcept; 57 | void HandleCompressedPayload(const void* apData, uint32_t aSize, ConnectionId_t aConnectionId) noexcept; 58 | 59 | void SynchronizeClientClocks(ConnectionId_t aSpecificConnection = k_HSteamNetConnection_Invalid) noexcept; 60 | 61 | static void SteamNetConnectionStatusChangedCallback(SteamNetConnectionStatusChangedCallback_t* apInfo); 62 | void OnSteamNetConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t* apInfo); 63 | 64 | HSteamListenSocket m_listenSock; 65 | HSteamNetPollGroup m_pollGroup; 66 | ISteamNetworkingSockets* m_pInterface; 67 | 68 | Vector m_connections; 69 | 70 | uint32_t m_tickRate; 71 | std::chrono::time_point m_lastClockSyncTime; 72 | std::chrono::time_point m_lastUpdateTime; 73 | std::chrono::time_point m_currentTick; 74 | std::chrono::milliseconds m_timeBetweenUpdates; 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /Code/connect/include/SteamInterface.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "steam/steamnetworkingsockets.h" 4 | 5 | namespace TiltedPhoques 6 | { 7 | enum EPacketFlags 8 | { 9 | kReliable, 10 | kUnreliable 11 | }; 12 | 13 | enum EConnectOpcode : uint8_t 14 | { 15 | kPayload = 0, 16 | kServerTime = 1, 17 | kCompressedPayload = 2 18 | }; 19 | 20 | struct SteamInterface 21 | { 22 | static void Acquire(); 23 | static void Release(); 24 | }; 25 | 26 | using ConnectionId_t = HSteamNetConnection; 27 | } 28 | -------------------------------------------------------------------------------- /Code/connect/include/SynchronizedClock.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace TiltedPhoques 6 | { 7 | struct SynchronizedClock 8 | { 9 | SynchronizedClock() noexcept; 10 | [[nodiscard]] uint64_t GetCurrentTick() const noexcept; 11 | [[nodiscard]] bool IsSynchronized() const noexcept; 12 | void Synchronize(uint64_t aServerTick, uint32_t aPing) noexcept; 13 | void Reset() noexcept; 14 | void Update() noexcept; 15 | 16 | private: 17 | 18 | uint64_t m_lastServerTick; 19 | uint64_t m_simulatedTick; 20 | std::chrono::nanoseconds m_previousSimulatedTick; 21 | std::chrono::nanoseconds m_tickDelta; 22 | std::chrono::time_point m_lastSynchronizationTime{}; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /Code/connect/src/Client.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "Client.hpp" 3 | #include "SteamInterface.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "Packet.hpp" 10 | #include 11 | #include 12 | 13 | namespace TiltedPhoques 14 | { 15 | static thread_local Client* s_pClient = nullptr; 16 | 17 | Client::Client() noexcept 18 | { 19 | SteamInterface::Acquire(); 20 | 21 | m_connection = k_HSteamNetConnection_Invalid; 22 | m_pInterface = SteamNetworkingSockets(); 23 | 24 | m_pLoop = Allocator::GetDefault()->Allocate(sizeof(uv_loop_t)); 25 | auto* pLoop = static_cast(m_pLoop); 26 | uv_loop_init(pLoop); 27 | pLoop->data = this; 28 | } 29 | 30 | Client::~Client() 31 | { 32 | uv_loop_close(static_cast(m_pLoop)); 33 | Allocator::Get()->Free(m_pLoop); 34 | SteamInterface::Release(); 35 | } 36 | 37 | Client::Client(Client&& aRhs) noexcept 38 | : m_connection(k_HSteamNetConnection_Invalid) 39 | , m_pInterface(nullptr) 40 | { 41 | SteamInterface::Acquire(); 42 | 43 | this->operator=(std::move(aRhs)); 44 | } 45 | 46 | Client& Client::operator=(Client&& aRhs) noexcept 47 | { 48 | std::swap(m_connection, aRhs.m_connection); 49 | std::swap(m_pInterface, aRhs.m_pInterface); 50 | 51 | return *this; 52 | } 53 | 54 | void Client::SteamNetConnectionStatusChangedCallback(SteamNetConnectionStatusChangedCallback_t* apInfo) 55 | { 56 | if (!apInfo || apInfo->m_hConn == k_HSteamNetConnection_Invalid) [[unlikely]] 57 | return; 58 | 59 | if (s_pClient) [[likely]] 60 | { 61 | s_pClient->OnSteamNetConnectionStatusChanged(apInfo); 62 | } 63 | } 64 | 65 | bool Client::Connect(const std::string& acEndpoint) noexcept 66 | { 67 | static auto GetAddrInfoCallback = [](uv_getaddrinfo_t* apHandle, int aStatus, struct addrinfo* apResult) 68 | { 69 | SteamNetworkingIPAddr remoteAddress{}; 70 | bool valid = false; 71 | 72 | auto* pClient = static_cast(apHandle->loop->data); 73 | pClient->m_pHandle = nullptr; 74 | 75 | if (aStatus == 0) 76 | { 77 | switch (apResult->ai_family) 78 | { 79 | case AF_INET: 80 | { 81 | const auto port = ntohs(reinterpret_cast(apResult->ai_addr)->sin_port); 82 | remoteAddress.SetIPv4(ntohl(reinterpret_cast(apResult->ai_addr)->sin_addr.s_addr), port); 83 | valid = true; 84 | } break; 85 | case AF_INET6: 86 | { 87 | const auto port = ntohs(reinterpret_cast(apResult->ai_addr)->sin6_port); 88 | remoteAddress.SetIPv6(reinterpret_cast(apResult->ai_addr)->sin6_addr.s6_addr, port); 89 | valid = true; 90 | } break; 91 | } 92 | } 93 | 94 | uv_freeaddrinfo(apResult); 95 | Allocator::GetDefault()->Free(apHandle); 96 | 97 | if(aStatus == UV_ECANCELED) 98 | { 99 | pClient->OnDisconnected(kAborted); 100 | return; 101 | } 102 | 103 | if (!valid) 104 | { 105 | pClient->OnDisconnected(kCannotResolve); 106 | return; 107 | } 108 | 109 | const auto cConnectResult = pClient->Connect(remoteAddress); 110 | if (!cConnectResult) 111 | { 112 | pClient->OnDisconnected(kLocalProblem); 113 | } 114 | }; 115 | 116 | const auto pos = acEndpoint.find_last_of(':'); 117 | auto* pHandle = static_cast(Allocator::GetDefault()->Allocate(sizeof(uv_getaddrinfo_t))); 118 | 119 | std::string endpoint = acEndpoint; 120 | std::string serviceName = "25681"; 121 | if(pos != std::string::npos) 122 | { 123 | serviceName = acEndpoint.c_str() + 1 + pos; 124 | endpoint = endpoint.substr(0, pos); 125 | } 126 | 127 | m_pHandle = pHandle; 128 | uv_getaddrinfo(static_cast(m_pLoop), pHandle, GetAddrInfoCallback, endpoint.c_str(), serviceName.c_str(), nullptr); 129 | 130 | return true; 131 | } 132 | 133 | bool Client::Connect(const SteamNetworkingIPAddr& acEndpoint) noexcept 134 | { 135 | SteamNetworkingConfigValue_t opt = {}; 136 | opt.SetPtr(k_ESteamNetworkingConfig_Callback_ConnectionStatusChanged, reinterpret_cast(&SteamNetConnectionStatusChangedCallback)); 137 | m_connection = m_pInterface->ConnectByIPAddress(acEndpoint, 1, &opt); 138 | 139 | return m_connection != k_HSteamNetConnection_Invalid; 140 | } 141 | 142 | bool Client::ConnectByIp(const std::string& acEndpoint) noexcept 143 | { 144 | SteamNetworkingIPAddr remoteAddress{}; 145 | remoteAddress.ParseString(acEndpoint.c_str()); 146 | 147 | return Connect(remoteAddress); 148 | } 149 | 150 | void Client::Close() noexcept 151 | { 152 | if (m_connection != k_HSteamNetConnection_Invalid) 153 | { 154 | m_pInterface->CloseConnection(m_connection, 0, nullptr, true); 155 | m_connection = k_HSteamNetConnection_Invalid; 156 | 157 | m_clock.Reset(); 158 | 159 | OnDisconnected(kAborted); 160 | } 161 | 162 | if(m_pHandle != nullptr) 163 | { 164 | uv_cancel(static_cast(m_pHandle)); 165 | } 166 | } 167 | 168 | void Client::Update() noexcept 169 | { 170 | m_clock.Update(); 171 | 172 | if (m_clock.GetCurrentTick() - m_lastStatisticsPoint >= 1000) 173 | { 174 | m_lastStatisticsPoint = m_clock.GetCurrentTick(); 175 | m_previousFrame = m_currentFrame; 176 | m_currentFrame = {}; 177 | } 178 | 179 | s_pClient = this; 180 | m_pInterface->RunCallbacks(); 181 | uv_run(static_cast(m_pLoop), UV_RUN_NOWAIT); 182 | s_pClient = nullptr; 183 | 184 | while (true) 185 | { 186 | ISteamNetworkingMessage* pIncomingMsg = nullptr; 187 | const auto cMessageCount = m_pInterface->ReceiveMessagesOnConnection(m_connection, &pIncomingMsg, 1); 188 | if (cMessageCount <= 0 || pIncomingMsg == nullptr) 189 | { 190 | // TODO: Handle error when messageCount < 0 191 | break; 192 | } 193 | 194 | m_currentFrame.RecvBytes += pIncomingMsg->GetSize(); 195 | m_currentFrame.UncompressedRecvBytes += pIncomingMsg->GetSize(); 196 | 197 | HandleMessage(pIncomingMsg->GetData(), pIncomingMsg->GetSize()); 198 | 199 | pIncomingMsg->Release(); 200 | } 201 | 202 | OnUpdate(); 203 | } 204 | 205 | void Client::Send(Packet* apPacket, const EPacketFlags acPacketFlags) const noexcept 206 | { 207 | m_currentFrame.UncompressedSentBytes += apPacket->m_size; 208 | 209 | if (apPacket->m_pData[0] == kPayload) 210 | { 211 | std::string data; 212 | snappy::Compress(apPacket->GetData(), apPacket->GetSize(), &data); 213 | 214 | if (data.size() < apPacket->GetSize()) 215 | { 216 | apPacket->m_pData[0] = kCompressedPayload; 217 | std::copy(std::begin(data), std::end(data), apPacket->GetData()); 218 | apPacket->m_size = (data.size() + 1) & 0xFFFFFFFF; 219 | } 220 | } 221 | 222 | m_currentFrame.SentBytes += apPacket->m_size; 223 | 224 | m_pInterface->SendMessageToConnection(m_connection, apPacket->m_pData, apPacket->m_size, 225 | acPacketFlags == kReliable ? k_nSteamNetworkingSend_Reliable : k_nSteamNetworkingSend_Unreliable, nullptr); 226 | } 227 | 228 | bool Client::IsConnected() const noexcept 229 | { 230 | if (m_connection != k_HSteamNetConnection_Invalid) 231 | { 232 | SteamNetConnectionInfo_t info{}; 233 | if (m_pInterface->GetConnectionInfo(m_connection, &info)) 234 | { 235 | if(info.m_eState == k_ESteamNetworkingConnectionState_Connected) 236 | { 237 | return GetClock().IsSynchronized(); 238 | } 239 | } 240 | } 241 | 242 | return false; 243 | } 244 | 245 | SteamNetConnectionRealTimeStatus_t Client::GetConnectionStatus() const noexcept 246 | { 247 | SteamNetConnectionRealTimeStatus_t status{}; 248 | 249 | if (m_connection != k_HSteamNetConnection_Invalid) 250 | { 251 | m_pInterface->GetConnectionRealTimeStatus(m_connection, &status, 0, nullptr); 252 | } 253 | 254 | return status; 255 | } 256 | 257 | Client::Statistics Client::GetStatistics() const noexcept 258 | { 259 | return m_previousFrame; 260 | } 261 | 262 | const SynchronizedClock& Client::GetClock() const noexcept 263 | { 264 | return m_clock; 265 | } 266 | 267 | void Client::OnSteamNetConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t* apInfo) 268 | { 269 | switch (apInfo->m_info.m_eState) 270 | { 271 | case k_ESteamNetworkingConnectionState_None: 272 | break; 273 | case k_ESteamNetworkingConnectionState_ClosedByPeer: 274 | case k_ESteamNetworkingConnectionState_ProblemDetectedLocally: 275 | { 276 | m_pInterface->CloseConnection(m_connection, 0, nullptr, false); 277 | m_connection = k_HSteamNetConnection_Invalid; 278 | 279 | m_clock.Reset(); 280 | 281 | if (apInfo->m_eOldState == k_ESteamNetworkingConnectionState_Connecting) 282 | { 283 | OnDisconnected(kTimeout); 284 | } 285 | else if (apInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ProblemDetectedLocally) 286 | { 287 | OnDisconnected(kLocalProblem); 288 | } 289 | else 290 | { 291 | OnDisconnected(kKicked); 292 | } 293 | 294 | break; 295 | } 296 | case k_ESteamNetworkingConnectionState_Connecting: 297 | break; 298 | case k_ESteamNetworkingConnectionState_Connected: 299 | // We don't notify here, wait for clock sync 300 | break; 301 | default: 302 | break; 303 | } 304 | } 305 | 306 | void Client::HandleMessage(const void* apData, uint32_t aSize) noexcept 307 | { 308 | // We handle the cases where packets target the current stack or the user stack 309 | if (aSize == 0) 310 | return; 311 | 312 | auto pData = static_cast(apData); 313 | 314 | const auto cOpcode = pData[0]; 315 | 316 | pData += 1; 317 | aSize -= 1; 318 | 319 | switch (cOpcode) 320 | { 321 | case kPayload: 322 | OnConsume(pData, aSize); 323 | break; 324 | case kServerTime: 325 | HandleServerTime(pData, aSize); 326 | break; 327 | case kCompressedPayload: 328 | HandleCompressedPayload(pData, aSize); 329 | break; 330 | default: 331 | assert(false); 332 | break; 333 | } 334 | } 335 | 336 | void Client::HandleServerTime(const void* apData, uint32_t aSize) noexcept 337 | { 338 | if (aSize < 8) 339 | return; 340 | 341 | const auto cConnectionStatus = GetConnectionStatus(); 342 | 343 | const auto cServerTime = google::protobuf::BigEndian::Load64(apData); 344 | const auto cWasSynchronized = GetClock().IsSynchronized(); 345 | 346 | m_clock.Synchronize(cServerTime, cConnectionStatus.m_nPing); 347 | 348 | if (!cWasSynchronized) 349 | OnConnected(); 350 | } 351 | 352 | void Client::HandleCompressedPayload(const void* apData, uint32_t aSize) noexcept 353 | { 354 | std::string data; 355 | snappy::Uncompress((const char*)apData, aSize, &data); 356 | 357 | m_currentFrame.UncompressedRecvBytes -= aSize; 358 | m_currentFrame.UncompressedRecvBytes += data.size() & 0xFFFFFFFF; 359 | 360 | if (!data.empty()) [[likely]] 361 | { 362 | OnConsume(data.data(), data.size() & 0xFFFFFFFF); 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /Code/connect/src/Packet.cpp: -------------------------------------------------------------------------------- 1 | #include "Packet.hpp" 2 | #include "SteamInterface.hpp" 3 | 4 | namespace TiltedPhoques 5 | { 6 | Packet::Packet() noexcept 7 | : m_pData(nullptr) 8 | , m_size(0) 9 | { 10 | } 11 | 12 | Packet::Packet(const uint32_t aSize) noexcept 13 | : m_pData(nullptr) 14 | , m_size(aSize + 1) 15 | { 16 | m_pData = static_cast(GetAllocator()->Allocate(m_size)); 17 | m_pData[0] = kPayload; 18 | } 19 | 20 | Packet::~Packet() noexcept 21 | { 22 | GetAllocator()->Free(m_pData); 23 | } 24 | 25 | char* Packet::GetData() const noexcept 26 | { 27 | return m_pData + 1; 28 | } 29 | 30 | uint32_t Packet::GetSize() const noexcept 31 | { 32 | return m_size - 1; 33 | } 34 | 35 | uint32_t Packet::GetTotalSize() const noexcept 36 | { 37 | return m_size; 38 | } 39 | 40 | bool Packet::IsValid() const noexcept 41 | { 42 | return m_pData != nullptr; 43 | } 44 | 45 | PacketView::PacketView(char* aPointer, uint32_t aSize) 46 | : Packet() 47 | { 48 | m_pData = aPointer; 49 | m_size = aSize; 50 | 51 | m_pData[0] = kPayload; 52 | } 53 | 54 | PacketView::~PacketView() 55 | { 56 | m_pData = nullptr; 57 | m_size = 0; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Code/connect/src/Server.cpp: -------------------------------------------------------------------------------- 1 | #include "Server.hpp" 2 | #include "SteamInterface.hpp" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | using namespace std::chrono; 15 | 16 | namespace TiltedPhoques 17 | { 18 | static thread_local Server* s_pServer = nullptr; 19 | 20 | Server::Server() noexcept 21 | : m_tickRate(10) 22 | , m_lastUpdateTime(0ns) 23 | , m_timeBetweenUpdates(100ms) 24 | , m_lastClockSyncTime(0ns) 25 | { 26 | SteamInterface::Acquire(); 27 | m_pInterface = SteamNetworkingSockets(); 28 | m_listenSock = k_HSteamListenSocket_Invalid; 29 | m_pollGroup = k_HSteamNetPollGroup_Invalid; 30 | } 31 | 32 | Server::~Server() 33 | { 34 | SteamInterface::Release(); 35 | } 36 | 37 | bool Server::Host(const uint16_t aPort, uint32_t aTickRate, bool bEnableDualStackIP) noexcept 38 | { 39 | Close(); 40 | 41 | SteamNetworkingIPAddr localAddress{}; // NOLINT(cppcoreguidelines-pro-type-member-init) 42 | if (bEnableDualStackIP) 43 | { 44 | localAddress.Clear(); 45 | localAddress.m_port = aPort; 46 | } 47 | else 48 | { 49 | localAddress.SetIPv4(0, aPort); 50 | } 51 | 52 | SteamNetworkingConfigValue_t opt = {}; 53 | opt.SetPtr(k_ESteamNetworkingConfig_Callback_ConnectionStatusChanged, reinterpret_cast(&SteamNetConnectionStatusChangedCallback)); 54 | m_listenSock = m_pInterface->CreateListenSocketIP(localAddress, 1, &opt); 55 | 56 | m_pollGroup = m_pInterface->CreatePollGroup(); 57 | 58 | if (m_tickRate == 0 && aTickRate == 0) 59 | { 60 | aTickRate = 10; 61 | } 62 | // If we pass 0, reuse the previously used tick rate 63 | else if (aTickRate == 0) 64 | { 65 | aTickRate = m_tickRate; 66 | } 67 | 68 | m_tickRate = aTickRate; 69 | 70 | // update time in MS 71 | m_timeBetweenUpdates = 1000ms / m_tickRate; 72 | return IsListening(); 73 | } 74 | 75 | void Server::Close() noexcept 76 | { 77 | m_pInterface->DestroyPollGroup(m_pollGroup); 78 | 79 | if (IsListening()) 80 | { 81 | m_pInterface->CloseListenSocket(m_listenSock); 82 | } 83 | 84 | m_pollGroup = k_HSteamNetPollGroup_Invalid; 85 | m_listenSock = k_HSteamListenSocket_Invalid; 86 | } 87 | 88 | void Server::Update() noexcept 89 | { 90 | m_currentTick = high_resolution_clock::now(); 91 | 92 | if (IsListening()) 93 | { 94 | s_pServer = this; 95 | m_pInterface->RunCallbacks(); 96 | s_pServer = nullptr; 97 | 98 | while (true) 99 | { 100 | ISteamNetworkingMessage* pIncomingMessage = nullptr; 101 | const auto messageCount = m_pInterface->ReceiveMessagesOnPollGroup(m_pollGroup, &pIncomingMessage, 1); 102 | if (messageCount <= 0 || pIncomingMessage == nullptr) 103 | { 104 | break; 105 | // TODO: Handle when messageCount is a negative number, it's an error 106 | } 107 | 108 | HandleMessage(pIncomingMessage->GetData(), pIncomingMessage->GetSize(), pIncomingMessage->GetConnection()); 109 | 110 | pIncomingMessage->Release(); 111 | } 112 | } 113 | 114 | // Sync clocks every 10 seconds 115 | if (m_currentTick - m_lastClockSyncTime >= 10s) 116 | { 117 | m_lastClockSyncTime = m_currentTick; 118 | SynchronizeClientClocks(); 119 | } 120 | 121 | if (m_currentTick - m_lastUpdateTime >= m_timeBetweenUpdates) 122 | { 123 | m_lastUpdateTime = m_currentTick; 124 | OnUpdate(); 125 | } 126 | 127 | std::this_thread::sleep_for(2ms); 128 | } 129 | 130 | void Server::SendToAll(Packet* apPacket, const EPacketFlags aPacketFlags) noexcept 131 | { 132 | for (const auto conn : m_connections) 133 | { 134 | Send(conn, apPacket, aPacketFlags); 135 | } 136 | } 137 | 138 | void Server::Send(const ConnectionId_t aConnectionId, Packet* apPacket, EPacketFlags aPacketFlags) const noexcept 139 | { 140 | if (apPacket->m_pData[0] == kPayload) 141 | { 142 | std::string data; 143 | snappy::Compress(apPacket->GetData(), apPacket->GetSize(), &data); 144 | 145 | if (data.size() < apPacket->GetSize()) 146 | { 147 | apPacket->m_pData[0] = kCompressedPayload; 148 | std::copy(std::begin(data), std::end(data), apPacket->GetData()); 149 | apPacket->m_size = (data.size() + 1) & 0xFFFFFFFF; 150 | } 151 | } 152 | 153 | m_pInterface->SendMessageToConnection(aConnectionId, apPacket->m_pData, apPacket->m_size, 154 | aPacketFlags == kReliable ? k_nSteamNetworkingSend_Reliable : k_nSteamNetworkingSend_Unreliable, nullptr); 155 | } 156 | 157 | void Server::Kick(const ConnectionId_t aConnectionId) noexcept 158 | { 159 | m_pInterface->CloseConnection(aConnectionId, 0, "Kick", true); 160 | Remove(aConnectionId); 161 | 162 | OnDisconnection(aConnectionId, EDisconnectReason::Kicked); 163 | } 164 | 165 | uint16_t Server::GetPort() const noexcept 166 | { 167 | SteamNetworkingIPAddr address{}; 168 | if (m_pInterface->GetListenSocketAddress(m_listenSock, &address)) 169 | { 170 | return address.m_port; 171 | } 172 | 173 | return 0; 174 | } 175 | 176 | bool Server::IsListening() const noexcept 177 | { 178 | return m_listenSock != k_HSteamListenSocket_Invalid; 179 | } 180 | 181 | uint32_t Server::GetClientCount() const noexcept 182 | { 183 | return m_connections.size() & 0xFFFFFFFF; 184 | } 185 | 186 | uint32_t Server::GetTickRate() const noexcept 187 | { 188 | return m_tickRate; 189 | } 190 | 191 | uint64_t Server::GetTick() const noexcept 192 | { 193 | return std::chrono::duration_cast(m_currentTick.time_since_epoch()).count(); 194 | } 195 | 196 | SteamNetConnectionInfo_t Server::GetConnectionInfo(ConnectionId_t aConnectionId) const noexcept 197 | { 198 | SteamNetConnectionInfo_t info{}; 199 | 200 | if (aConnectionId != k_HSteamNetConnection_Invalid) 201 | { 202 | m_pInterface->GetConnectionInfo(aConnectionId, &info); 203 | } 204 | 205 | return info; 206 | } 207 | 208 | bool Server::IsAlive(ConnectionId_t aConnectionId) const noexcept 209 | { 210 | const auto it = std::find(std::begin(m_connections), std::end(m_connections), aConnectionId); 211 | return it != std::end(m_connections); 212 | } 213 | 214 | void Server::Remove(const ConnectionId_t aId) noexcept 215 | { 216 | const auto it = std::find(std::begin(m_connections), std::end(m_connections), aId); 217 | if (it != std::end(m_connections) && !m_connections.empty()) 218 | { 219 | std::iter_swap(it, std::end(m_connections) - 1); 220 | m_connections.pop_back(); 221 | } 222 | } 223 | 224 | void Server::HandleMessage(const void* apData, const uint32_t aSize, const ConnectionId_t aConnectionId) noexcept 225 | { 226 | // We handle the cases where packets target the current stack or the user stack 227 | if (aSize == 0) 228 | return; 229 | 230 | const auto pData = static_cast(apData); 231 | switch (pData[0]) 232 | { 233 | case kPayload: 234 | OnConsume(pData + 1, aSize - 1, aConnectionId); 235 | break; 236 | case kCompressedPayload: 237 | HandleCompressedPayload(pData + 1, aSize - 1, aConnectionId); 238 | break; 239 | default: 240 | assert(false); 241 | break; 242 | } 243 | } 244 | 245 | void Server::HandleCompressedPayload(const void* apData, uint32_t aSize, ConnectionId_t aConnectionId) noexcept 246 | { 247 | std::string data; 248 | snappy::Uncompress((const char*)apData, aSize, &data); 249 | 250 | if (!data.empty()) 251 | { 252 | OnConsume(data.data(), data.size() & 0xFFFFFFFF, aConnectionId); 253 | } 254 | } 255 | 256 | void Server::SynchronizeClientClocks(const ConnectionId_t aSpecificConnection) noexcept 257 | { 258 | const auto time = GetTick(); 259 | 260 | StackAllocator<1 << 10> allocator; 261 | ScopedAllocator _{ &allocator }; 262 | 263 | const auto pBuffer = New(512); 264 | 265 | Buffer::Writer writer(pBuffer); 266 | writer.WriteBits(kServerTime, 8); 267 | writer.WriteBits(google::protobuf::BigEndian::FromHost64(time), 64); 268 | 269 | if(aSpecificConnection != k_HSteamNetConnection_Invalid) 270 | { 271 | // In this case we probably want it to arrive so send it reliably 272 | m_pInterface->SendMessageToConnection(aSpecificConnection, pBuffer->GetData(), writer.Size() & 0xFFFFFFFF, k_nSteamNetworkingSend_ReliableNoNagle, nullptr); 273 | } 274 | else 275 | { 276 | for (const auto cConnection : m_connections) 277 | { 278 | m_pInterface->SendMessageToConnection(cConnection, pBuffer->GetData(), writer.Size() & 0xFFFFFFFF, k_nSteamNetworkingSend_UnreliableNoDelay, nullptr); 279 | } 280 | } 281 | } 282 | 283 | void Server::SteamNetConnectionStatusChangedCallback(SteamNetConnectionStatusChangedCallback_t* apInfo) 284 | { 285 | if (!apInfo || apInfo->m_hConn == k_HSteamNetConnection_Invalid) [[unlikely]] 286 | return; 287 | 288 | if (s_pServer) 289 | { 290 | s_pServer->OnSteamNetConnectionStatusChanged(apInfo); 291 | } 292 | } 293 | 294 | void Server::OnSteamNetConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t* apInfo) 295 | { 296 | switch (apInfo->m_info.m_eState) 297 | { 298 | case k_ESteamNetworkingConnectionState_None: 299 | break; 300 | case k_ESteamNetworkingConnectionState_ProblemDetectedLocally: 301 | case k_ESteamNetworkingConnectionState_ClosedByPeer: 302 | { 303 | if (apInfo->m_eOldState == k_ESteamNetworkingConnectionState_Connected) 304 | { 305 | Remove(apInfo->m_hConn); 306 | 307 | const auto reason = apInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ClosedByPeer 308 | ? EDisconnectReason::Quit 309 | : EDisconnectReason::BadConnection; 310 | 311 | OnDisconnection(apInfo->m_hConn, reason); 312 | } 313 | 314 | m_pInterface->CloseConnection(apInfo->m_hConn, 0, nullptr, false); 315 | break; 316 | } 317 | case k_ESteamNetworkingConnectionState_Connecting: 318 | { 319 | if (m_pInterface->AcceptConnection(apInfo->m_hConn) != k_EResultOK) 320 | { 321 | m_pInterface->CloseConnection(apInfo->m_hConn, 0, nullptr, false); 322 | // TODO: Error handling 323 | break; 324 | } 325 | 326 | if(m_pInterface->SetConnectionPollGroup(apInfo->m_hConn, m_pollGroup) != k_EResultOK) 327 | { 328 | m_pInterface->CloseConnection(apInfo->m_hConn, 0, nullptr, false); 329 | // TODO: Error handling 330 | break; 331 | } 332 | 333 | m_connections.push_back(apInfo->m_hConn); 334 | 335 | SynchronizeClientClocks(apInfo->m_hConn); 336 | 337 | OnConnection(apInfo->m_hConn); 338 | break; 339 | } 340 | case k_ESteamNetworkingConnectionState_Connected: 341 | break; 342 | default: 343 | break; 344 | 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /Code/connect/src/SteamInterface.cpp: -------------------------------------------------------------------------------- 1 | #include "SteamInterface.hpp" 2 | #include "steam/steamnetworkingsockets.h" 3 | 4 | #include 5 | 6 | namespace TiltedPhoques 7 | { 8 | static std::atomic s_initCounter = 0; 9 | 10 | void SteamInterface::Acquire() 11 | { 12 | if (s_initCounter.fetch_add(1, std::memory_order_relaxed) == 0) 13 | { 14 | SteamDatagramErrMsg errorMessage; 15 | if (!GameNetworkingSockets_Init(nullptr, errorMessage)) 16 | { 17 | // TODO: Error management 18 | } 19 | } 20 | } 21 | 22 | void SteamInterface::Release() 23 | { 24 | if (s_initCounter.fetch_sub(1, std::memory_order_relaxed) == 1) 25 | { 26 | GameNetworkingSockets_Kill(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Code/connect/src/SynchronizedClock.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "SynchronizedClock.hpp" 3 | #include 4 | 5 | using namespace std::chrono_literals; 6 | 7 | namespace TiltedPhoques 8 | { 9 | SynchronizedClock::SynchronizedClock() noexcept 10 | : m_lastServerTick{ 0 } 11 | , m_simulatedTick{ 0 } 12 | , m_tickDelta{ 0 } 13 | , m_lastSynchronizationTime{ std::chrono::high_resolution_clock::now() } 14 | {} 15 | 16 | uint64_t SynchronizedClock::GetCurrentTick() const noexcept 17 | { 18 | return m_simulatedTick; 19 | } 20 | 21 | bool SynchronizedClock::IsSynchronized() const noexcept 22 | { 23 | return m_simulatedTick != 0; 24 | } 25 | 26 | void SynchronizedClock::Synchronize(uint64_t aServerTick, uint32_t aPing) noexcept 27 | { 28 | if (aServerTick <= m_lastServerTick) 29 | return; 30 | 31 | m_lastServerTick = aServerTick; 32 | 33 | const auto tripTime = aPing / 2; 34 | 35 | m_lastSynchronizationTime = std::chrono::high_resolution_clock::now(); 36 | 37 | if (IsSynchronized()) 38 | { 39 | m_previousSimulatedTick = std::chrono::milliseconds(m_simulatedTick); 40 | const auto estimatedServerTime = std::chrono::milliseconds(m_lastServerTick) + std::chrono::milliseconds(tripTime); 41 | m_tickDelta = estimatedServerTime - m_previousSimulatedTick; 42 | } 43 | else 44 | { 45 | m_previousSimulatedTick = std::chrono::milliseconds(m_lastServerTick) + std::chrono::milliseconds(tripTime); 46 | m_simulatedTick = std::chrono::duration_cast(m_previousSimulatedTick).count(); 47 | m_tickDelta = 0ns; 48 | } 49 | } 50 | 51 | void SynchronizedClock::Reset() noexcept 52 | { 53 | // Rebuild 54 | *this = SynchronizedClock{}; 55 | } 56 | 57 | void SynchronizedClock::Update() noexcept 58 | { 59 | const auto now = std::chrono::high_resolution_clock::now(); 60 | const auto delta = now - m_lastSynchronizationTime; 61 | const std::chrono::duration syncDelta = now - m_lastSynchronizationTime; 62 | 63 | if (!IsSynchronized()) 64 | return; 65 | 66 | const auto syncDeltaSeconds = syncDelta.count(); 67 | const auto serverTickEstimate = m_previousSimulatedTick + delta; 68 | 69 | const auto diff = std::chrono::nanoseconds(static_cast(m_tickDelta.count() * Min(1.0, (float)syncDeltaSeconds))); 70 | 71 | m_simulatedTick = std::chrono::duration_cast(serverTickEstimate + diff).count(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Code/tests/src/connect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Server.hpp" 4 | #include "Client.hpp" 5 | #include "Packet.hpp" 6 | #include 7 | 8 | using namespace TiltedPhoques; 9 | 10 | struct TestServer final : Server 11 | { 12 | void OnUpdate() override 13 | { 14 | const std::string data("Hello from server! 111111111111111111111110000000000000000000000000000"); 15 | 16 | Packet packet(data.size()); 17 | std::copy_n(data.begin(), data.size(), packet.GetData()); 18 | 19 | SendToAll(&packet); 20 | } 21 | 22 | void OnConsume(const void* apData, const uint32_t aSize, ConnectionId_t aId) override 23 | { 24 | std::string str((const char*)apData, aSize); 25 | std::cout << str << std::endl; 26 | } 27 | 28 | void OnConnection(ConnectionId_t aId) override 29 | { 30 | } 31 | 32 | void OnDisconnection(ConnectionId_t aId, EDisconnectReason aReason) override 33 | { 34 | } 35 | }; 36 | 37 | struct TestClient final : Client 38 | { 39 | void OnUpdate() override 40 | { 41 | } 42 | 43 | void OnConsume(const void* apData, const uint32_t aSize) override 44 | { 45 | //std::cout << static_cast(apData) << std::endl; 46 | 47 | const std::string data("Hello from client! 111111111111111111111110000000000000000000000000000"); 48 | 49 | Packet packet(data.size()); 50 | std::copy_n(data.begin(), data.size(), packet.GetData()); 51 | 52 | Send(&packet); 53 | } 54 | 55 | void OnConnected() override 56 | { 57 | std::cout << "Connected" << std::endl; 58 | } 59 | 60 | void OnDisconnected(EDisconnectReason aReason) override 61 | { 62 | std::cout << "Disconnected " << aReason << std::endl; 63 | } 64 | }; 65 | 66 | TEST_CASE("Connect tests", "[connect]") 67 | { 68 | #ifdef WIN32 69 | TestServer server; 70 | REQUIRE(server.Host(12547, 20) == true); 71 | while (true) 72 | server.Update(); 73 | #else 74 | TestClient client; 75 | REQUIRE(client.Connect("127.0.0.1:12547") == true); 76 | while (true) 77 | client.Update(); 78 | #endif 79 | 80 | } 81 | -------------------------------------------------------------------------------- /Code/tests/src/main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_RUNNER 2 | #include 3 | 4 | int main() 5 | { 6 | return Catch::Session().run(); 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Tilted Phoques. All Rights Reserved. 2 | 3 | All materials are made available on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. Do not remove or modify any license notices. 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECTS := Connect Tests protobuf SteamNet 2 | FORWARDED := clean help ${PROJECTS} 3 | 4 | ifeq ($(shell uname -m),x86_64) 5 | platform := x64 6 | else 7 | platform := x32 8 | endif 9 | 10 | config := debug 11 | premake_config = ${config}_${platform} 12 | 13 | .PHONY: all tests $(FORWARDED) 14 | 15 | all: Build/projects 16 | $(MAKE) -C Build/projects all config=${premake_config} 17 | 18 | tests: all 19 | Build/lib/${platform}/Tests 20 | 21 | Build/projects: 22 | cd Build && ./MakeGMakeProjects.sh 23 | 24 | $(PROJECTS): Build/projects 25 | $(MAKE) -C Build/projects $@ config=${premake_config} 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tilted Connect 2 | 3 | This library covers the serialization and deserialization of data along with a layer on top of the network library. 4 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: Linux 3 | displayName: Linux 4 | pool: 5 | vmImage: ubuntu-latest 6 | 7 | variables: 8 | platform: x64 9 | 10 | steps: 11 | - template: .ci/linux-build.yml 12 | 13 | - job: Windows_x32 14 | displayName: Windows x32 15 | pool: 16 | vmImage: windows-2019 17 | 18 | variables: 19 | platform: Win32 20 | 21 | steps: 22 | - template: .ci/windows-build.yml 23 | 24 | - job: Windows_x64 25 | displayName: Windows x64 26 | pool: 27 | vmImage: windows-2019 28 | 29 | variables: 30 | platform: x64 31 | 32 | steps: 33 | - template: .ci/windows-build.yml 34 | -------------------------------------------------------------------------------- /xmake.lua: -------------------------------------------------------------------------------- 1 | set_languages("cxx20") 2 | 3 | set_xmakever("2.5.1") 4 | 5 | -- direct dependency version pinning 6 | add_requires("tiltedcore v0.2.7", "hopscotch-map v2.3.1", "snappy 1.1.10", "gamenetworkingsockets v1.4.1", "catch2 2.13.9", "libuv v1.48.0") 7 | 8 | -- dependencies' dependencies version pinning 9 | add_requireconfs("*.mimalloc", { version = "2.1.7", override = true }) 10 | add_requireconfs("*.openssl", { version = "1.1.1-w", override = true }) 11 | add_requireconfs("*.cmake", { version = "3.30.2", override = true }) 12 | add_requireconfs("*.protobuf*", { version = "26.1", override = true }) 13 | add_requireconfs("**.abseil*", { version = "20250127.1", override = true }) 14 | 15 | add_rules("mode.debug","mode.releasedbg", "mode.release") 16 | add_rules("plugin.vsxmake.autoupdate") 17 | 18 | if is_mode("release") then 19 | add_defines("NDEBUG") 20 | 21 | set_optimize("fastest") 22 | end 23 | 24 | target("TiltedConnect") 25 | set_kind("static") 26 | set_group("Libraries") 27 | add_files("Code/connect/src/*.cpp") 28 | add_includedirs("Code/connect/include/", {public = true}) 29 | add_headerfiles("Code/connect/include/*.hpp", {prefixdir = "TiltedConnect"}) 30 | add_packages("tiltedcore", "hopscotch-map", "snappy", "gamenetworkingsockets", "libuv") 31 | add_cxflags("-fPIC") 32 | add_defines("STEAMNETWORKINGSOCKETS_STATIC_LINK") 33 | --------------------------------------------------------------------------------