Skip to content

Schwungus/nutpunch

Repository files navigation

NutPunch

Caution

NutPunch implements UDP-based peer-to-peer networking. Use only if you know what you're getting yourself into. Client-server architecture is a lot more commonplace in games, and arguably much easier to implement and understand. You have been warned. Think for yourself to make the right decision.

NutPunch is a UDP hole-punching library for REAL men (and women). Header-only. Brutal. Written in plain C.

Comes with a public instance for out-of-the-box integration.

Troubleshooting

UDP hole-punching can be finnicky at times, especially in non-standard networking environments. For that reason, this troubleshooting section comes before all else. If you're having connectivity issues in a game powered by NutPunch, please make sure the following conditions are met before you start throwing hooks and jabs at the developers:

  1. Your VPN client is off. Whatever that might be, it could be preventing UDP traffic from arriving to the correct destination (that being your computer). Software that verifiedly breaks NutPunch includes AmneziaVPN in certain configurations, and the Cloudflare WARP app (as opposed to using 1.1.1.1 as your DNS resolver server). More to be verified soon™.
  2. Your firewall allows the game executable to bind to and connect to any UDP port. This is the default behavior on Windows machines nowadays, but it's an edge case worth noting.

Introductory Lecture

This library implements P2P networking, where each peer communicates with all others. It's a complex model, and it could be counterproductive to use if you don't know what you're signing yourself up for. If you don't feel like reading the immediately following blanket of words and scribbles, you may skip to using premade integrations.

Before you can punch any holes in your peers' NAT, you will need a hole-punching server with a public IP address assigned. Querying a public server lets us bust a gateway open to the global network, all while the server relays the connection info for other peers to us. If you're just testing, you can use our public instance instead of hosting your own. The current server implementation uses a lobby-based approach, where each lobby supports up to 16 peers and is identified by a unique ASCII string.

In order to run your own hole-puncher server, you'll need to get the server binary from our reference implementation releases. A Docker image is also available for hosting a NutPuncher server on Linux. If you're in a pinch, don't have access to a public IP address, and your players reside on a LAN/virtual network such as Radmin VPN, you can actually run NutPuncher locally and use your LAN IP address to connect to it.

Once you've figured out how the players are to connect to your hole-puncher server, you can start coding up your game. The complete example might be overwhelming at first, but make sure to skim through it before you do any heavy networking. Here's the general usage guide for the NutPunch library:

  1. Host a lobby with NutPunch_Host("lobby-id"), or join an existing one with NutPunch_Join("lobby-id").
  2. Optionally add metadata to the lobby:
    1. If you join an empty lobby, you will be considered its master. A lobby master has the authority from the matchmaking server to set lobby-specific metadata. This is usually needed to start games with specific parameters or to enforce an expected player count in a lobby. If you don't need metadata, you can just skip this entire step.
    2. After calling NutPunch_Join(), you can set metadata in the lobby by calling NutPunch_LobbySet() as a master. You don't have to finish "connecting" and handshaking with NutPuncher before setting metadata. A lobby can hold up to 16 fields, each identified by 8-byte strings and holding up to 32 bytes of data. Non-masters aren't allowed to set metadata, so calls are no-op for them. The actual metadata also won't be updated until the next call to NutPunch_Update(), which will be covered later.
    3. Just like NutPunch_LobbySet(), you can use NutPunch_PeerSet() to set your very own peer-specific metadata such as nickname or skin spritesheet name. Use NutPunch_PeerGet() with a peer index to query your own or others' peer metadata.
  3. Listen for events:
    1. Call NutPunch_Update() each frame, regardless of whether you're still joining or already playing with the boys. This will also automatically update lobby metadata back and forth.
    2. Check your status by matching the returned value against NPS_* constants. NPS_Online is the only one you need to handle explicitly, as you can safely start retrieving metadata and player count with it. Optionally, you can also handle NPS_Error and get clues as to what's wrong by calling NutPunch_GetLastError().
  4. Optionally read metadata from the lobby during NPS_Online status. Use NutPunch_Get() to get a pointer to a metadata field, which you can then read from (as long as it's valid and is the exact size that you expect it to be). These pointers are volatile, especially when calling NutPunch_Update(), so if you need to use the gotten value more than once, cache it somewhere.
  5. If all went well (i.e. you have enough metadata and player count is fulfilled), start your match.
  6. Run the game logic.
  7. Keep in sync with each peer: Send datagrams through NutPunch_Send() and poll for incoming datagrams by looping with NutPunch_HasMessage() and retrieving them with NutPunch_NextMessage(). In scenarios where you need to cache packet data, memcpy it into a static array of NUTPUNCH_BUFFER_SIZE1 bytes in order to fit the whole packet without overflowing and/or segfaulting.
  8. Come back to step 6 the next frame. You're all Gucci!

Premade Integrations

Not documented yet.

TODO: Add a GekkoNet network adapter implementation. In the meantime, you can look into the code of a real usecase.

Installation

If you're using CMake, you can include this library in your project by adding the following to your CMakeLists.txt:

include(FetchContent)
FetchContent_Declare(NutPunch
    GIT_REPOSITORY https://github.com/Schwungus/nutpunch.git
    GIT_TAG stable) # you can use a specific commit hash here
FetchContent_MakeAvailable(NutPunch)

add_executable(MyGame main.c) # your game's CMake target goes here
target_link_libraries(MyGame PRIVATE NutPunch)

For other build systems (or lack thereof), you only need to copy NutPunch.h into your include path. Make sure to link against ws2_32 on Windows though, or else you'll end up with scary linker errors related to Winsock.

Basic Usage

Once NutPunch.h is in your include-path, using it is straightforward, just like any header-only library. Select a source file where the library's function definitions will reside (it could be your main.c as well), tell the compiler to add NutPunch implementation details with a #define, and #include the library's main header inside it:

#define NUTPUNCH_IMPLEMENTATION
#include <NutPunch.h>

Then #include <NutPunch.h> wherever you need to use it. Here's a really simple example:

#include <stdlib.h> // for EXIT_SUCCESS
#include <NutPunch.h>

int main(int argc, char* argv[]) {
    NutPunch_Join("MyLobby");
    for (;;) // your game's mainloop goes here...
        NutPunch_Update();
    return EXIT_SUCCESS;
}

If you want to see all the juicy APIs in action, try reading test.c from this repo.

Take a look at advanced usage to discover things you can customize.

Public Instance

If you don't feel like hosting your own instance, you may use our public instance. It's used by default unless a different server is specified.

If you want to be explicit about using the public instance, call NutPunch_SetServerAddr:

NutPunch_SetServerAddr(NUTPUNCH_DEFAULT_SERVER);
NutPunch_Join("lobby-id");

Advanced Usage

Customize Memory Handling

You can #define custom memory handling functions for NutPunch to use, before including the header. They're only relevant to the implementation. If none are specified, C's standard library functions are used.

SDL3 example:

#include <SDL3/SDL_stdinc.h>

#define NUTPUNCH_IMPLEMENTATION
#define NutPunch_SNPrintF SDL_snprintf
#define NutPunch_Memcmp SDL_memcmp
#define NutPunch_Memset SDL_memset
#define NutPunch_Memcpy SDL_memcpy
#define NutPunch_Malloc SDL_malloc
#define NutPunch_Free SDL_free
#include <NutPunch.h>

Customize Logger Implementation

Just like in the example above, you can override NutPunch's logging facility before including NutPunch.h:

#define NUTPUNCH_IMPLEMENTATION
#define NutPunch_Log(...) printf(__VA_ARGS__)
#include <NutPunch.h>

Hosting your own NutPuncher

If you're dissatisfied with the public instance, whether from needing to stick to a specific build or fork or whatever, you can host your own. Make sure to read the introductory pamphlet before attempting this.

On Windows and Linux, use the provided server binary and make sure the UDP port 30000 is open to the public.

If you're crazy enough, you may also use our Docker image, e.g. with docker-compose:

services:
  main:
    image: ghcr.io/schwungus/nutpuncher
    container_name: nutpuncher
    ports: [30000:30000/udp]
    restart: always

If you're on MacOS, well, bad luck, buddy...

Footnotes

  1. NUTPUNCH_BUFFER_SIZE is currently defined to be 8192 bytes. Should be small enough for WinSock not to signal a WSAEMSGSIZE error on send.