Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
branches: [ main ]
pull_request:
workflow_dispatch:

jobs:
build-and-test:
name: ${{ matrix.os }} ${{ matrix.config }}
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ ubuntu-24.04, macos-15, windows-2025 ] # linux x64, apple silicon, windows x64
config: [ Debug, Release ]
steps:
- uses: actions/checkout@v4

- name: Configure
run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.config }}

- name: Build
run: cmake --build build --config ${{ matrix.config }} --parallel

- name: Test
run: ctest --test-dir build --build-config ${{ matrix.config }} --output-on-failure --timeout 600
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# CMake
build*/
CMakeUserPresets.json

# Visual Studio
*.db
*.opendb
Expand All @@ -14,6 +18,7 @@ Makefile

# Misc
*.txt
!CMakeLists.txt
*.7z
*.zip
*.tar.gz
Expand Down
46 changes: 22 additions & 24 deletions BUILDING.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,45 @@
How to build netcode
====================

## Building on Windows

Download [premake 5](https://premake.github.io/download.html) and copy the **premake5** executable somewhere in your path.
netcode builds with [CMake](https://cmake.org) (3.16 or later) on Windows, MacOS and Linux.

You need Visual Studio to build the source code. If you don't have Visual Studio you can [download the community edition for free](https://visualstudio.microsoft.com/downloads/).
libsodium is vendored in this repository, so there is nothing else to install.

Once you have Visual Studio installed, go to the command line under the netcode directory and type:

premake5 vs2019
## Building on MacOS and Linux

Open the generated netcode.sln file.
Go to the command line under the netcode directory and enter:

Now you can build the library and run individual test programs as you would for any other Visual Studio solution.
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel

## Building on MacOS and Linux
Run the unit tests:

First, download and install [premake 5](https://premake.github.io/download.html).
ctest --test-dir build --output-on-failure

Next, install libsodium.
Then you can run binaries like this:

Linux:
./build/bin/test
./build/bin/server
./build/bin/client

sudo apt install libsodium-dev
For a debug build, use `-DCMAKE_BUILD_TYPE=Debug` and a separate build directory, e.g. `-B build-debug`.

Mac:
## Building on Windows

brew install libsodium
You need Visual Studio to build the source code. If you don't have Visual Studio you can [download the community edition for free](https://visualstudio.microsoft.com/downloads/).

Now go to the command line under the netcode directory and enter:
Go to the command line under the netcode directory and type:

premake5 gmake
cmake -B build
cmake --build build --config Release

Which creates makefiles which you can use to build the source via:
Run the unit tests:

make -j
ctest --test-dir build --build-config Release --output-on-failure

Then you can run binaries like this:
Binaries are placed under `build\bin\Release` (or `build\bin\Debug` for `--config Debug`).

./bin/test
./bin/server
./bin/client
If you prefer working inside Visual Studio, open the generated `build\netcode.sln` and build and run the projects from there.

If you have questions please create an issue at https://github.com/mas-bandwidth/netcode and I'll do my best to help you out.

Expand Down
80 changes: 80 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
cmake_minimum_required(VERSION 3.16)

project(netcode LANGUAGES C CXX)

set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)

# static MSVC runtime, matching the previous premake configuration
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release)
endif()

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

# netcode.h selects debug/release behavior from these rather than NDEBUG
add_compile_definitions(
$<$<CONFIG:Debug>:NETCODE_DEBUG>
$<$<NOT:$<CONFIG:Debug>>:NETCODE_RELEASE>
)

if(MSVC)
add_compile_options(/fp:fast)
set(NETCODE_WARNINGS /W4)
else()
add_compile_options(-ffast-math)
set(NETCODE_WARNINGS -Wall -Wextra)
endif()

# vendored libsodium subset, amalgamated into a single header + source pair.
# see sodium/NOTES.md for how it is generated and validated.

add_library(sodium STATIC sodium/sodium.c sodium/sodium.h)
target_include_directories(sodium PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/sodium)
if(NOT MSVC)
target_compile_options(sodium PRIVATE
-Wall -Wextra
-Wno-unused-parameter
-Wno-unused-function
-Wno-unknown-pragmas
-Wno-unused-variable
-Wno-type-limits)
endif()

# the netcode library

add_library(netcode STATIC netcode.c netcode.h)
target_include_directories(netcode PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_options(netcode PRIVATE ${NETCODE_WARNINGS})
target_link_libraries(netcode PUBLIC sodium)
if(WIN32)
target_link_libraries(netcode PUBLIC ws2_32 iphlpapi)
endif()

# examples and long-running harnesses

foreach(example client server client_server soak profile)
add_executable(${example} ${example}.c)
target_compile_options(${example} PRIVATE ${NETCODE_WARNINGS})
target_link_libraries(${example} PRIVATE netcode)
endforeach()

# unit tests. test.cpp compiles netcode.c into itself with NETCODE_ENABLE_TESTS,
# so it links sodium directly, not the netcode library. the target cannot be
# named "test" (reserved by CTest) so the output name is set instead.

enable_testing()

add_executable(netcode_test test.cpp)
set_target_properties(netcode_test PROPERTIES OUTPUT_NAME test)
target_compile_options(netcode_test PRIVATE ${NETCODE_WARNINGS})
target_link_libraries(netcode_test PRIVATE sodium)
if(WIN32)
target_link_libraries(netcode_test PRIVATE ws2_32 iphlpapi)
endif()

add_test(NAME netcode_test COMMAND netcode_test)
73 changes: 67 additions & 6 deletions netcode.c
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,30 @@ void netcode_default_free_function( void * context, void * pointer )

#endif

static int netcode_parse_port( NETCODE_CONST char * string, uint16_t * port )
{
// the port must be all digits and fit in [0,65535]. anything else is an error,
// rather than whatever atoi truncation used to produce.

if ( string[0] == '\0' )
return NETCODE_ERROR;

int value = 0;
int i;
for ( i = 0; string[i] != '\0'; i++ )
{
if ( string[i] < '0' || string[i] > '9' )
return NETCODE_ERROR;
value = value * 10 + ( string[i] - '0' );
if ( value > 65535 )
return NETCODE_ERROR;
}

*port = (uint16_t) value;

return NETCODE_OK;
}

int netcode_parse_address( NETCODE_CONST char * address_string_in, struct netcode_address_t * address )
{
netcode_assert( address_string_in );
Expand Down Expand Up @@ -237,7 +261,8 @@ int netcode_parse_address( NETCODE_CONST char * address_string_in, struct netcod
break;
if ( address_string[index] == ':' && address_string[index-1] == ']' )
{
address->port = (uint16_t) ( atoi( &address_string[index + 1] ) );
if ( netcode_parse_port( &address_string[index+1], &address->port ) != NETCODE_OK )
return NETCODE_ERROR;
address_string[index-1] = '\0';
break;
}
Expand Down Expand Up @@ -277,7 +302,8 @@ int netcode_parse_address( NETCODE_CONST char * address_string_in, struct netcod
break;
if ( address_string[index] == ':' )
{
address->port = (uint16_t) atoi( &address_string[index+1] );
if ( netcode_parse_port( &address_string[index+1], &address->port ) != NETCODE_OK )
return NETCODE_ERROR;
address_string[index] = '\0';
break;
}
Expand Down Expand Up @@ -3317,11 +3343,16 @@ void netcode_client_send_packet( struct netcode_client_t * client, NETCODE_CONST
{
netcode_assert( client );
netcode_assert( packet_data );
netcode_assert( packet_bytes >= 0 );
netcode_assert( packet_bytes > 0 );
netcode_assert( packet_bytes <= NETCODE_MAX_PACKET_SIZE );

if ( packet_bytes < 0 || packet_bytes > NETCODE_MAX_PACKET_SIZE )
// zero byte payloads are not valid on the wire and would silently vanish at the receiver

if ( packet_bytes <= 0 || packet_bytes > NETCODE_MAX_PACKET_SIZE )
{
netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: payload packet size is out of range (%d)\n", packet_bytes );
return;
}

if ( client->state != NETCODE_CLIENT_STATE_CONNECTED )
return;
Expand Down Expand Up @@ -4881,11 +4912,16 @@ void netcode_server_send_packet( struct netcode_server_t * server, int client_in
{
netcode_assert( server );
netcode_assert( packet_data );
netcode_assert( packet_bytes >= 0 );
netcode_assert( packet_bytes > 0 );
netcode_assert( packet_bytes <= NETCODE_MAX_PACKET_SIZE );

if ( packet_bytes < 0 || packet_bytes > NETCODE_MAX_PACKET_SIZE )
// zero byte payloads are not valid on the wire and would silently vanish at the receiver

if ( packet_bytes <= 0 || packet_bytes > NETCODE_MAX_PACKET_SIZE )
{
netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: payload packet size is out of range (%d)\n", packet_bytes );
return;
}

if ( !server->running )
return;
Expand Down Expand Up @@ -5497,6 +5533,25 @@ static void test_address()
check( netcode_parse_address( ".....", &address ) == NETCODE_ERROR );
}

// ports must be all digits in [0,65535]. out of range and non-numeric ports must not silently truncate

{
struct netcode_address_t address;
check( netcode_parse_address( "127.0.0.1:65535", &address ) == NETCODE_OK );
check( address.type == NETCODE_ADDRESS_IPV4 );
check( address.port == 65535 );
check( netcode_parse_address( "[::1]:65535", &address ) == NETCODE_OK );
check( address.type == NETCODE_ADDRESS_IPV6 );
check( address.port == 65535 );
check( netcode_parse_address( "127.0.0.1:65536", &address ) == NETCODE_ERROR );
check( netcode_parse_address( "127.0.0.1:99999", &address ) == NETCODE_ERROR );
check( netcode_parse_address( "127.0.0.1:", &address ) == NETCODE_ERROR );
check( netcode_parse_address( "127.0.0.1:40k", &address ) == NETCODE_ERROR );
check( netcode_parse_address( "[::1]:65536", &address ) == NETCODE_ERROR );
check( netcode_parse_address( "[::1]:", &address ) == NETCODE_ERROR );
check( netcode_parse_address( "[::1]:40k", &address ) == NETCODE_ERROR );
}

{
struct netcode_address_t address;
check( netcode_parse_address( "107.77.207.77", &address ) == NETCODE_OK );
Expand Down Expand Up @@ -6873,6 +6928,12 @@ void client_server_socket_connect_to( NETCODE_CONST char * client_address, NETCO
if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
break;

// this test runs over real sockets while advancing virtual time, so it must yield
// real time each iteration or the virtual timeouts can expire before the OS delivers
// a single loopback packet

netcode_sleep( 0.01 );

time += delta_time;
}

Expand Down
22 changes: 18 additions & 4 deletions netcode.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@
#ifndef NETCODE_H
#define NETCODE_H

/*
IMPORTANT: netcode is single-threaded by design and is not thread safe.

The library uses global state (netcode_init/netcode_term, the log level, and the
printf and assert hooks) and performs no internal synchronization. Call all netcode
functions from the same thread, or provide your own locking around them. Each client
and server object must only be updated from one thread at a time.
*/

#include <stdint.h>
#include <stddef.h>

Expand Down Expand Up @@ -286,16 +295,21 @@ do
if ( !(condition) ) \
{ \
netcode_assert_function( #condition, __FUNCTION__, __FILE__, __LINE__ ); \
exit(1); \
} \
} while(0)
#else
#define netcode_assert( ignore ) ((void)0)
#endif

void netcode_set_assert_function( void (*function)( NETCODE_CONST char * /*condition*/,
NETCODE_CONST char * /*function*/,
NETCODE_CONST char * /*file*/,
/*
The default assert handler prints the failed condition, breaks into the debugger and
exits. A custom assert handler may return instead, in which case execution continues
past the failed assert -- that is the caller's choice and their responsibility.
*/

void netcode_set_assert_function( void (*function)( NETCODE_CONST char * /*condition*/,
NETCODE_CONST char * /*function*/,
NETCODE_CONST char * /*file*/,
int /*line*/ ) );

void netcode_random_bytes( uint8_t * data, int bytes );
Expand Down
Loading
Loading