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.
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:
- 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.1as your DNS resolver server). More to be verified soon™. - 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.
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:
- Host a lobby with
NutPunch_Host("lobby-id"), or join an existing one withNutPunch_Join("lobby-id"). - Optionally add metadata to the lobby:
- 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.
- After calling
NutPunch_Join(), you can set metadata in the lobby by callingNutPunch_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 toNutPunch_Update(), which will be covered later. - Just like
NutPunch_LobbySet(), you can useNutPunch_PeerSet()to set your very own peer-specific metadata such as nickname or skin spritesheet name. UseNutPunch_PeerGet()with a peer index to query your own or others' peer metadata.
- Listen for events:
- 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. - Check your status by matching the returned value against
NPS_*constants.NPS_Onlineis the only one you need to handle explicitly, as you can safely start retrieving metadata and player count with it. Optionally, you can also handleNPS_Errorand get clues as to what's wrong by callingNutPunch_GetLastError().
- Call
- Optionally read metadata from the lobby during
NPS_Onlinestatus. UseNutPunch_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 callingNutPunch_Update(), so if you need to use the gotten value more than once, cache it somewhere. - If all went well (i.e. you have enough metadata and player count is fulfilled), start your match.
- Run the game logic.
- Keep in sync with each peer: Send datagrams through
NutPunch_Send()and poll for incoming datagrams by looping withNutPunch_HasMessage()and retrieving them withNutPunch_NextMessage(). In scenarios where you need to cache packet data,memcpyit into a static array ofNUTPUNCH_BUFFER_SIZE1 bytes in order to fit the whole packet without overflowing and/or segfaulting. - Come back to step 6 the next frame. You're all Gucci!
Not documented yet.
TODO: Add a GekkoNet network adapter implementation. In the meantime, you can look into the code of a real usecase.
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.
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.
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");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>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>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: alwaysIf you're on MacOS, well, bad luck, buddy...
Footnotes
-
NUTPUNCH_BUFFER_SIZEis currently defined to be 8192 bytes. Should be small enough for WinSock not to signal aWSAEMSGSIZEerror on send. ↩
