diff --git a/Bindings/Netcode/IO.hsc b/Bindings/Netcode/IO.hsc
new file mode 100644
--- /dev/null
+++ b/Bindings/Netcode/IO.hsc
@@ -0,0 +1,191 @@
+{-|
+  Module      : Bindings.Netcode.IO
+  Description : Low-level bindings to the netcode.io library.
+  Copyright   : (c) Pavel Krajcevski, 2020
+  License     : BSD-3
+  Maintainer  : krajcevski@gmail.com
+  Stability   : experimental
+  Portability : Portable
+
+  This module contains the low-level bindings that represent the direct
+  interface between Haskell and the C library
+  <https://github.com/networkprotocol/netcode.io netcode.io>.
+
+  The bindings here are meant for advanced usage, as they are not particularly
+  idiomatic Haskell, and largely represent the types that we get from the
+  <https://hackage.haskell.org/package/bindings-DSL bindings-DSL> library. For
+  high level bindings (recommended), please refer to the "Netcode.IO" module.
+-}
+
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE NoImplicitPrelude  #-}
+{-# LANGUAGE StandaloneDeriving #-}
+--------------------------------------------------------------------------------
+
+#include <netcode.h>
+#include <bindings.dsl.h>
+
+--------------------------------------------------------------------------------
+
+module Bindings.Netcode.IO where
+
+import Data.Word              (Word8, Word16, Word64)
+import Foreign.C.String       (CString)
+import Foreign.C.Types        (CInt(..), CDouble(..))
+import Foreign.Marshal.Array  (peekArray, pokeArray)
+import Foreign.Ptr            (Ptr, FunPtr, plusPtr)
+import Foreign.Storable       (Storable(..))
+import Prelude                ( IO, Eq, Show, Num
+                              , ($)
+                              , div, undefined, return, take
+                              )
+
+--------------------------------------------------------------------------------
+
+#num NETCODE_CONNECT_TOKEN_BYTES
+#num NETCODE_KEY_BYTES
+#num NETCODE_MAC_BYTES
+#num NETCODE_USER_DATA_BYTES
+#num NETCODE_MAX_SERVERS_PER_CONNECT
+
+#num NETCODE_CLIENT_STATE_CONNECT_TOKEN_EXPIRED
+#num NETCODE_CLIENT_STATE_INVALID_CONNECT_TOKEN
+#num NETCODE_CLIENT_STATE_CONNECTION_TIMED_OUT
+#num NETCODE_CLIENT_STATE_CONNECTION_RESPONSE_TIMED_OUT
+#num NETCODE_CLIENT_STATE_CONNECTION_REQUEST_TIMED_OUT
+#num NETCODE_CLIENT_STATE_CONNECTION_DENIED
+#num NETCODE_CLIENT_STATE_DISCONNECTED
+#num NETCODE_CLIENT_STATE_SENDING_CONNECTION_REQUEST
+#num NETCODE_CLIENT_STATE_SENDING_CONNECTION_RESPONSE
+#num NETCODE_CLIENT_STATE_CONNECTED
+
+#num NETCODE_MAX_CLIENTS
+#num NETCODE_MAX_PACKET_SIZE
+
+#num NETCODE_LOG_LEVEL_NONE
+#num NETCODE_LOG_LEVEL_ERROR
+#num NETCODE_LOG_LEVEL_INFO
+#num NETCODE_LOG_LEVEL_DEBUG
+
+#num NETCODE_OK
+#num NETCODE_ERROR
+
+#num NETCODE_ADDRESS_NONE
+#num NETCODE_ADDRESS_IPV4
+#num NETCODE_ADDRESS_IPV6
+
+#ccall netcode_init, IO CInt
+#ccall netcode_term, IO ()
+
+#starttype struct netcode_address_t
+#array_field data.ipv4, Word8
+#array_field data.ipv6, Word16
+#field       port,      Word16
+#field       type,      Word8
+#stoptype
+
+#ccall netcode_parse_address, CString -> Ptr <netcode_address_t> -> IO CInt
+#ccall netcode_address_to_string, Ptr <netcode_address_t> -> CString -> IO CString
+#ccall netcode_address_equal, Ptr <netcode_address_t> -> Ptr <netcode_address_t> -> IO CInt
+
+#opaque_t netcode_network_simulator_t
+#opaque_t netcode_client_t
+#opaque_t netcode_server_t
+
+#callback_t allocate_function_t, Ptr () -> Word64 -> IO (Ptr ())
+#callback_t free_function_t, Ptr () -> Ptr () -> IO ()
+#callback_t state_change_callback_t, Ptr () -> CInt -> CInt -> IO ()
+#callback_t connect_disconnect_callback_t, Ptr () -> CInt -> CInt -> IO ()
+#callback_t send_loopback_packet_callback_t, Ptr () -> CInt -> Ptr Word8 -> CInt -> Word64 -> IO ()
+#callback_t send_packet_override_t, Ptr () -> Ptr <netcode_address_t> -> Ptr Word8 -> CInt -> IO ()
+#callback_t receive_packet_override_t, Ptr () -> Ptr <netcode_address_t> -> Ptr Word8 -> CInt -> IO CInt
+
+#starttype struct netcode_client_config_t
+#field allocator_context,             Ptr ()
+#field allocate_function,             <allocate_function_t>
+#field free_function,                 <free_function_t>
+#field network_simulator,             Ptr <netcode_network_simulator_t>
+#field callback_context,              Ptr ()
+#field state_change_callback,         <state_change_callback_t>
+#field send_loopback_packet_callback, <send_loopback_packet_callback_t>
+#field override_send_and_receive,     CInt
+#field send_packet_override,          <send_packet_override_t>
+#field receive_packet_override,       <receive_packet_override_t>
+#stoptype
+
+#ccall netcode_default_client_config, Ptr <netcode_client_config_t> -> IO ()
+#ccall netcode_client_create, CString -> Ptr <netcode_client_config_t> -> CDouble -> IO (Ptr <netcode_client_t>)
+#ccall netcode_client_destroy, Ptr <netcode_client_t> -> IO ()
+#ccall netcode_client_connect, Ptr <netcode_client_t> -> Ptr Word8 -> IO ()
+#ccall netcode_client_update, Ptr <netcode_client_t> -> CDouble -> IO ()
+#ccall netcode_client_next_packet_sequence, Ptr <netcode_client_t> -> IO Word64
+#ccall netcode_client_send_packet, Ptr <netcode_client_t> -> Ptr Word8 -> CInt -> IO ()
+#ccall netcode_client_receive_packet, Ptr <netcode_client_t> -> Ptr CInt -> Ptr Word64 -> IO (Ptr Word8)
+#ccall netcode_client_free_packet, Ptr <netcode_client_t> -> Ptr () -> IO ()
+#ccall netcode_client_disconnect, Ptr <netcode_client_t> -> IO ()
+#ccall netcode_client_state, Ptr <netcode_client_t> -> IO CInt
+#ccall netcode_client_index, Ptr <netcode_client_t> -> IO CInt
+#ccall netcode_client_max_clients, Ptr <netcode_client_t> -> IO CInt
+#ccall netcode_client_connect_loopback, Ptr <netcode_client_t> -> CInt -> CInt -> IO ()
+#ccall netcode_client_disconnect_loopback, Ptr <netcode_client_t> -> IO ()
+#ccall netcode_client_process_packet, Ptr <netcode_client_t> -> Ptr <netcode_address_t> -> Ptr Word8 -> CInt -> IO ()
+#ccall netcode_client_loopback, Ptr <netcode_client_t> -> IO CInt
+#ccall netcode_client_process_loopback_packet, Ptr <netcode_client_t> -> Ptr Word8 -> CInt -> Word64 -> IO ()
+#ccall netcode_client_get_port, Ptr <netcode_client_t> -> IO Word16
+#ccall netcode_client_server_address, Ptr <netcode_client_t> -> IO (Ptr <netcode_address_t>)
+#ccall netcode_generate_connect_token, CInt -> Ptr CString -> Ptr CString -> CInt -> CInt -> Word64 -> Word64 -> Ptr Word8 -> Ptr Word8 -> Ptr Word8 -> IO CInt
+
+#starttype struct netcode_server_config_t
+#field       protocol_id,                   Word64
+#array_field private_key,                   Word8
+#field       allocator_context,             Ptr ()
+#field       allocate_function,             <allocate_function_t>
+#field       free_function,                 <free_function_t>
+#field       network_simulator,             Ptr <netcode_network_simulator_t>
+#field       callback_context,              Ptr ()
+#field       connect_disconnect_callback,   <connect_disconnect_callback_t>
+#field       send_loopback_packet_callback, <send_loopback_packet_callback_t>
+#field       override_send_and_receive,     CInt
+#field       send_packet_override,          <send_packet_override_t>
+#field       receive_packet_override,       <receive_packet_override_t>
+#stoptype
+
+#ccall netcode_default_server_config, Ptr <netcode_server_config_t> -> IO ()
+#ccall netcode_server_create, CString -> Ptr <netcode_server_config_t> -> CDouble -> IO (Ptr <netcode_server_t>)
+#ccall netcode_server_destroy, Ptr <netcode_server_t> -> IO ()
+#ccall netcode_server_start, Ptr <netcode_server_t> -> CInt -> IO ()
+#ccall netcode_server_stop, Ptr <netcode_server_t> -> IO ()
+#ccall netcode_server_running, Ptr <netcode_server_t> -> IO CInt
+#ccall netcode_server_max_clients, Ptr <netcode_server_t> -> IO CInt
+#ccall netcode_server_update, Ptr <netcode_server_t> -> CDouble -> IO ()
+#ccall netcode_server_client_connected, Ptr <netcode_server_t> -> CInt -> IO CInt
+#ccall netcode_server_client_id, Ptr <netcode_server_t> -> CInt -> IO Word64
+#ccall netcode_server_client_address, Ptr <netcode_server_t> -> CInt -> IO (Ptr <netcode_address_t>)
+#ccall netcode_server_disconnect_client, Ptr <netcode_server_t> -> CInt -> IO ()
+#ccall netcode_server_disconnect_all_clients, Ptr <netcode_server_t> -> IO ()
+#ccall netcode_server_next_packet_sequence, Ptr <netcode_server_t> -> CInt -> IO Word64
+#ccall netcode_server_send_packet, Ptr <netcode_server_t> -> CInt -> Ptr Word8 -> CInt -> IO ()
+#ccall netcode_server_receive_packet, Ptr <netcode_server_t> -> CInt -> Ptr Word8 -> Ptr Word64 -> IO (Ptr Word8)
+#ccall netcode_server_free_packet, Ptr <netcode_server_t> -> Ptr () -> IO ()
+#ccall netcode_server_num_connected_clients, Ptr <netcode_server_t> -> IO CInt
+#ccall netcode_server_client_user_data, Ptr <netcode_server_t> -> CInt -> IO (Ptr ())
+#ccall netcode_server_process_packet, Ptr <netcode_server_t> -> Ptr <netcode_address_t> -> Ptr Word8 -> CInt -> IO ()
+#ccall netcode_server_connect_loopback_client, Ptr <netcode_server_t> -> CInt -> Word64 -> Ptr Word8 -> IO ()
+#ccall netcode_server_disconnect_loopback_client, Ptr <netcode_server_t> -> CInt -> IO ()
+#ccall netcode_server_client_loopback, Ptr <netcode_server_t> -> CInt -> IO CInt
+#ccall netcode_server_process_loopback_packet, Ptr <netcode_server_t> -> CInt -> Ptr Word8 -> CInt -> Word64 -> IO ()
+#ccall netcode_server_get_port, Ptr <netcode_server_t> -> IO Word16
+
+#ccall netcode_log_level, CInt -> IO ()
+
+#callback_t netcode_assert_function_ty, CString -> CString -> CString -> CInt -> IO ()
+#ccall netcode_set_assert_function, <netcode_assert_function_ty> -> IO ()
+
+-- void netcode_set_printf_function( int (*function)( NETCODE_CONST char *, ... ) );
+
+#ccall netcode_random_bytes, Ptr Word8 -> CInt -> IO ()
+#ccall netcode_sleep, CDouble -> IO ()
+#ccall netcode_time, IO CDouble
+
+-- | For testing only.
+#ccall netcode_test, IO ()
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2020, Pavel Krajcevski
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+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 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 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, 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.
diff --git a/Netcode/IO.hs b/Netcode/IO.hs
new file mode 100644
--- /dev/null
+++ b/Netcode/IO.hs
@@ -0,0 +1,126 @@
+{-|
+
+Module      : Netcode.IO
+Description : High-level bindings to the netcode.io library.
+Copyright   : (c) Pavel Krajcevski, 2020
+License     : BSD-3
+Maintainer  : krajcevski@gmail.com
+Stability   : experimental
+Portability : Portable
+
+This module contains the high-level bindings on top of the module
+"Bindings.Netcode.IO". These provide a cleaner interface to the
+<https://github.com/networkprotocol/netcode.io netcode.io> C library and are
+the recommended interface for application developers.
+
+These bindings have some limitations. Namely, they are not as performant as
+the "close to the metal" bindings provided in "Bindings.Netcode.IO". In the
+event that you need more performance, that module is available for use.
+
+The general architecture of a @netcode.io@ application is outlined in the C
+library
+<https://github.com/networkprotocol/netcode.io/blob/master/STANDARD.md#architecture documentation>. The jist is that we need three main entities:
+
+ * A server to connect to
+ * An authentication server to dispatch connection tokens
+ * A client that wants to connect to a server
+
+In this case, the client will request a connection token from the authentication
+server. The authentication server will know a-priori what the available servers
+are for the client to connect to. These servers (and other information) are
+stored in an encrypted 'ConnectToken', based on the 64-bit unique client ID. The
+authentication server will send a 'ConnectToken' to a client when that client
+looks for servers to connect to.
+
+Once a connection between server and client is established, they may exchange
+packets of information. These packets are sent over UDP and therefore are not
+guaranteed to arrive in order or even arrive at all. In order to create a
+reliable information channel between client and server, it is recommended to
+use this library in conjunction with the
+<https://github.com/networkprotocol/reliable.io reliable.io> library.
+-}
+
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+module Netcode.IO (
+    -- * Initialization
+      initialize
+    , terminate
+
+    -- * Addresses
+    , Address
+    , AddressMode(..)
+    , addressMode
+    , addressPort
+    , addressValues
+    , constructAddress
+    , parseAddress
+    , addressToString
+    , addressEqual
+
+    -- * Common callbacks
+    , SendPacketOverride
+    , ReceivePacketOverride
+
+    -- * Packets
+    , module Netcode.IO.Packet
+    , module Netcode.IO.Client
+    , module Netcode.IO.Server
+
+    -- * Utilities
+    , sleep
+
+    , LogLevel(..), logLevel
+) where
+--------------------------------------------------------------------------------
+
+import Control.Monad         (unless)
+import Data.Data             (Data)
+import Data.Typeable         (Typeable)
+import Foreign.C.Types       (CDouble(..))
+import GHC.Generics          (Generic)
+
+import Bindings.Netcode.IO
+import Netcode.IO.Address
+import Netcode.IO.Callbacks
+import Netcode.IO.Client
+import Netcode.IO.Packet
+import Netcode.IO.Server
+
+--------------------------------------------------------------------------------
+
+-- | Initializes the @netcode.io@ library runtime. This should be called before
+-- any additional functions in this library. Throws an
+-- t'Control.Exception.IOException' on failure.
+initialize :: IO ()
+initialize = do
+    result <- c'netcode_init
+    unless (result == c'NETCODE_OK) $
+      fail $ "Failed to initialize netcode.io. Result: " <> show result
+    return ()
+
+-- | Terminates the @netcode.io@ library runtime. This should be called only
+-- after all other library functions terminate.
+terminate :: IO ()
+terminate = c'netcode_term
+
+-- | Specifies the logging behavior of @netcode.io@. Note, this logging behavior
+-- is called from C calls to @printf@ and therefore might interfere with the
+-- Haskell runtime (such as 'putStrLn').
+data LogLevel = LogLevel'None
+              | LogLevel'Info
+              | LogLevel'Error
+              | LogLevel'Debug
+    deriving (Eq, Ord, Show, Read, Bounded, Enum, Generic, Data, Typeable)
+
+-- | Set the @netcode.io@ 'LogLevel'. The default is 'LogLevel'None'.
+logLevel :: LogLevel -> IO ()
+logLevel LogLevel'None  = c'netcode_log_level c'NETCODE_LOG_LEVEL_NONE
+logLevel LogLevel'Info  = c'netcode_log_level c'NETCODE_LOG_LEVEL_INFO
+logLevel LogLevel'Error = c'netcode_log_level c'NETCODE_LOG_LEVEL_ERROR
+logLevel LogLevel'Debug = c'netcode_log_level c'NETCODE_LOG_LEVEL_DEBUG
+
+-- | Sleep the current thread. This is usually only used in example programs.
+-- It's probably safer to use the built-in 'Control.Concurrent.threadDelay'.
+sleep :: Double -> IO ()
+sleep = c'netcode_sleep . CDouble
diff --git a/Netcode/IO/Address.hs b/Netcode/IO/Address.hs
new file mode 100644
--- /dev/null
+++ b/Netcode/IO/Address.hs
@@ -0,0 +1,134 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+module Netcode.IO.Address where
+
+--------------------------------------------------------------------------------
+
+import Data.Data             (Data)
+import Data.Typeable         (Typeable)
+import Data.Word             (Word8, Word16)
+import Control.Monad         (unless)
+import Foreign.C.String      (withCString, peekCString)
+import Foreign.ForeignPtr    (ForeignPtr, withForeignPtr, mallocForeignPtr)
+import Foreign.Marshal.Alloc (allocaBytes)
+import Foreign.Storable      (peek, poke)
+import GHC.Generics          (Generic)
+
+import Bindings.Netcode.IO
+
+--------------------------------------------------------------------------------
+
+-- | An opaque type that encapsulates an address that can be used with
+-- @netcode.io@. The address may be stored in memory in different ways (for
+-- example with encryption), and therefore needs the IO monad to interact with
+-- it.
+--
+-- <https://github.com/Mokosha/netcode-io/issues/1 TODO>: Use a high-level
+-- representation here to enable a more pure interface.
+newtype Address = Address (ForeignPtr C'netcode_address_t)
+                deriving (Show)
+
+-- | The address mode based on how the address is represented. Usually a
+-- consequence of how it was parsed.
+data AddressMode
+  = AddressMode'Unknown  -- ^ Usually when address is stored encrypted.
+  | AddressMode'IPv4
+  | AddressMode'IPv6
+    deriving (Eq, Ord, Show, Read, Enum, Bounded, Data, Typeable, Generic)
+
+typedAddressMode :: Word8 -> AddressMode
+typedAddressMode m
+  | m == c'NETCODE_ADDRESS_NONE = AddressMode'Unknown
+  | m == c'NETCODE_ADDRESS_IPV4 = AddressMode'IPv4
+  | m == c'NETCODE_ADDRESS_IPV6 = AddressMode'IPv6
+  | otherwise = error "Unknown address mode!"
+
+rawAddressMode :: AddressMode -> Word8
+rawAddressMode AddressMode'Unknown = c'NETCODE_ADDRESS_NONE
+rawAddressMode AddressMode'IPv4    = c'NETCODE_ADDRESS_IPV4
+rawAddressMode AddressMode'IPv6    = c'NETCODE_ADDRESS_IPV6
+
+-- | Returns the address mode for the given address. Note, this address may be
+-- stored in memory in different ways, so we must inspect the memory contents
+-- in order to retreive the addressing mode.
+addressMode :: Address -> IO AddressMode
+addressMode (Address addr) = withForeignPtr addr $
+  fmap (typedAddressMode . c'netcode_address_t'type) . peek
+
+-- | Returns the port associated with this address.
+addressPort :: Address -> IO Word16
+addressPort (Address addr) = withForeignPtr addr $
+  fmap c'netcode_address_t'port . peek
+
+-- | Returns the address as a sequence of values in its human readable format.
+-- For example:
+--
+-- >>> parseAddress "123.231.132.213" >>= addressValues
+-- [123, 231, 132, 213]
+--
+-- The length of the list is either 4 or 8 depending on the address mode. If
+-- the address mode is unknown, 'addressValues' returns the empty list.
+addressValues :: Address -> IO [Word16]
+addressValues (Address fptr) = withForeignPtr fptr $ \aptr -> do
+  addr <- peek aptr
+  case (typedAddressMode $ c'netcode_address_t'type addr) of
+    AddressMode'IPv4 ->
+      return $ map fromIntegral $ c'netcode_address_t'data'ipv4 addr
+    AddressMode'IPv6 ->  return $ c'netcode_address_t'data'ipv6 addr
+    _                ->  return []
+
+-- | Returns an address with the given values interpreted using the given mode.
+-- For IPv4 addresses, only the bottom 8 bits of each 16-bit word will be used.
+-- The list will be zero padded to contain enough values to fill the address as
+-- needed.
+constructAddress :: AddressMode
+                 -> Word16       -- ^ Port
+                 -> [Word16]
+                 -> IO Address
+constructAddress mode port vals =
+  let ipv4Vals = take 4 $ map fromIntegral $ vals <> repeat 0
+      ipv6Vals = take 8 $ vals <> repeat 0
+   in do
+    address <- mallocForeignPtr
+    withForeignPtr address $ \ptr -> do
+      addrVal <- peek ptr
+      case mode of
+        AddressMode'IPv4 -> poke ptr $ addrVal {
+            c'netcode_address_t'data'ipv4 = ipv4Vals
+          , c'netcode_address_t'port = port
+          , c'netcode_address_t'type = c'NETCODE_ADDRESS_IPV4
+          }
+        AddressMode'IPv6 -> poke ptr $ addrVal {
+            c'netcode_address_t'data'ipv6 = ipv6Vals
+          , c'netcode_address_t'port = port
+          , c'netcode_address_t'type = c'NETCODE_ADDRESS_IPV6
+          }
+        _ -> fail "Cannot construct address with unknown address mode"
+    return $ Address address
+
+-- | Takes a 'String' and parses it to create an 'Address'. The string should
+-- be formatted as either a valid IPv4 or IPv6 address. It does not, however,
+-- support dual address modes.
+parseAddress :: String -> IO Address
+parseAddress addrStr = do
+    address <- mallocForeignPtr
+    retVal <- withForeignPtr address $ \addressPtr ->
+                withCString addrStr (`c'netcode_parse_address` addressPtr)
+    unless (retVal == c'NETCODE_OK) $ fail "Unable to parse address"
+    return $ Address address
+
+-- | Returns a string that represents the given 'Address'.
+addressToString :: Address -> IO String
+addressToString (Address addrPtr) =
+    let maxAddrStringLen = 256
+     in allocaBytes maxAddrStringLen $ \addrStr -> do
+         _ <- withForeignPtr addrPtr (`c'netcode_address_to_string` addrStr)
+         peekCString addrStr
+
+-- | Returns True if two addresses are equal by examining their memory contents.
+addressEqual :: Address -> Address -> IO Bool
+addressEqual (Address addrPtrA) (Address addrPtrB) =
+    withForeignPtr addrPtrA $ \ptrA ->
+        withForeignPtr addrPtrB $ \ptrB ->
+            (/= 0) <$> c'netcode_address_equal ptrA ptrB
+
diff --git a/Netcode/IO/Callbacks.hs b/Netcode/IO/Callbacks.hs
new file mode 100644
--- /dev/null
+++ b/Netcode/IO/Callbacks.hs
@@ -0,0 +1,51 @@
+module Netcode.IO.Callbacks where
+
+-------------------------------------------------------------------------------
+import Data.Word             (Word8)
+import Foreign.C.Types       (CInt)
+import Foreign.ForeignPtr    (newForeignPtr_)
+import Foreign.Ptr           (Ptr)
+
+import Bindings.Netcode.IO
+import Netcode.IO.Address
+
+-------------------------------------------------------------------------------
+
+-- | Override that sends a packet to the given address. This can be used by
+-- both clients and servers. This is invoked after @netcode.io@ processes and
+-- encrypts the packet.
+type SendPacketOverride
+     = Address    -- ^ 'Address' to send the packet to
+    -> Ptr Word8  -- ^ A pointer to the memory that holds the packet data
+    -> CInt       -- ^ The size of the packet
+    -> IO ()
+
+mkSendPacketOverride :: SendPacketOverride -> IO C'send_packet_override_t
+mkSendPacketOverride sendFn =
+    mk'send_packet_override_t $ \_ aptr pkt pktSize -> do
+        addr <- Address <$> newForeignPtr_ aptr
+        sendFn addr pkt pktSize
+
+-- | Override that receives a packet from the given address. This can be used
+-- by both clients and servers. This is invoked before @netcode.io@ processes
+-- and decrypts the packet data.
+--
+-- Implementations of this callback are meant to fill the memory at the given
+-- pointer with the data from a packet received from the 'Address'. The maximum
+-- size of the buffer pointed to is also passed and the implementation is
+-- expected to return the actual size of the packet. In the event that there is
+-- no packet (or equivalently, no packet data), then the implementation should
+-- return zero.
+type ReceivePacketOverride
+     = Address    -- ^ Address from which to receive a packet
+    -> Ptr Word8  -- ^ Pointer to the buffer where to write packet data
+    -> CInt       -- ^ Maximum size of destination buffer in bytes
+    -> IO CInt    -- ^ Return value: should be size of packet data
+
+mkReceivePacketOverride :: ReceivePacketOverride
+                        -> IO C'receive_packet_override_t
+mkReceivePacketOverride recvFn =
+    mk'receive_packet_override_t $ \_ aptr pkt pktSize -> do
+        addr <- Address <$> newForeignPtr_ aptr
+        recvFn addr pkt pktSize
+    
diff --git a/Netcode/IO/Client.hs b/Netcode/IO/Client.hs
new file mode 100644
--- /dev/null
+++ b/Netcode/IO/Client.hs
@@ -0,0 +1,415 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+module Netcode.IO.Client (
+    -- * Clients
+
+    -- ** Client-specific callbacks
+      ClientStateChangeCallback
+
+    -- ** Client configs
+    , ClientConfig
+    , defaultClientConfig
+    , setClientStateChangeCallback, clearClientStateChangeCallback
+    , setClientSendReceiveOverrides, clearClientSendReceiveOverrides
+
+    -- ** Client objects
+    , Client
+    , createClient
+    , destroyClient
+    , generateClientID
+    , connectClient
+    , disconnectClient
+    , updateClient
+    , sendPacketFromClient
+    , receivePacketFromServer
+    , nextClientPacketSequence
+    , getClientPort
+    , withClientServerAddress
+
+    -- ** Client state
+    , ClientState(..)
+    , getClientState
+    , isClientDisconnected
+
+    -- ** Connect Tokens
+    , ConnectToken
+    , maximumServersPerConnect
+    , maximumUserDataSize
+    , privateKeySize
+    , generateConnectToken
+) where
+
+--------------------------------------------------------------------------------
+
+import Control.Monad         (when)
+import Data.Data             (Data)
+import Data.Typeable         (Typeable)
+import Data.Word             (Word8, Word16, Word64)
+import Foreign.C.String      (withCString)
+import Foreign.C.Types       (CDouble(..), CInt)
+import Foreign.Concurrent    (newForeignPtr)
+import Foreign.ForeignPtr    ( ForeignPtr, newForeignPtr_, mallocForeignPtrBytes
+                             , withForeignPtr
+                             )
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Marshal.Array (allocaArray, pokeArray)
+import Foreign.Ptr           ( Ptr, nullPtr, castPtr
+                             , FunPtr, nullFunPtr, freeHaskellFunPtr
+                             )
+import Foreign.Storable      (peek, poke, sizeOf, pokeElemOff)
+import GHC.Generics          (Generic)
+
+import Bindings.Netcode.IO
+import Netcode.IO.Address
+import Netcode.IO.Callbacks
+import Netcode.IO.Packet
+
+--------------------------------------------------------------------------------
+
+freeNullFunPtr :: FunPtr a -> IO ()
+freeNullFunPtr x
+    | x == nullFunPtr = return ()
+    | otherwise       = freeHaskellFunPtr x
+
+--------------------------------------------------------------------------------
+
+-- | The possible connection states of a 'Client'. The default state is
+-- 'ClientState'Disconnected'.
+data ClientState
+    = ClientState'ConnectTokenExpired
+    | ClientState'InvalidConnectToken
+    | ClientState'ConnectionTimedOut
+    | ClientState'ConnectionResponseTimedOut
+    | ClientState'ConnectionRequestTimedOut
+    | ClientState'ConnectionDenied
+    | ClientState'Disconnected
+    | ClientState'SendingConnectionRequest
+    | ClientState'SendingConnectionResponse
+    | ClientState'Connected
+    deriving (Eq, Ord, Show, Enum, Bounded, Data, Typeable, Generic)
+
+_rawClientState :: ClientState -> CInt
+_rawClientState ClientState'ConnectTokenExpired        = c'NETCODE_CLIENT_STATE_CONNECT_TOKEN_EXPIRED
+_rawClientState ClientState'InvalidConnectToken        = c'NETCODE_CLIENT_STATE_INVALID_CONNECT_TOKEN
+_rawClientState ClientState'ConnectionTimedOut         = c'NETCODE_CLIENT_STATE_CONNECTION_TIMED_OUT
+_rawClientState ClientState'ConnectionResponseTimedOut = c'NETCODE_CLIENT_STATE_CONNECTION_RESPONSE_TIMED_OUT
+_rawClientState ClientState'ConnectionRequestTimedOut  = c'NETCODE_CLIENT_STATE_CONNECTION_REQUEST_TIMED_OUT
+_rawClientState ClientState'ConnectionDenied           = c'NETCODE_CLIENT_STATE_CONNECTION_DENIED
+_rawClientState ClientState'Disconnected               = c'NETCODE_CLIENT_STATE_DISCONNECTED
+_rawClientState ClientState'SendingConnectionRequest   = c'NETCODE_CLIENT_STATE_SENDING_CONNECTION_REQUEST
+_rawClientState ClientState'SendingConnectionResponse  = c'NETCODE_CLIENT_STATE_SENDING_CONNECTION_RESPONSE
+_rawClientState ClientState'Connected                  = c'NETCODE_CLIENT_STATE_CONNECTED
+
+typedClientState :: CInt -> ClientState
+typedClientState raw
+  | raw == c'NETCODE_CLIENT_STATE_CONNECT_TOKEN_EXPIRED         = ClientState'ConnectTokenExpired
+  | raw == c'NETCODE_CLIENT_STATE_INVALID_CONNECT_TOKEN         = ClientState'InvalidConnectToken
+  | raw == c'NETCODE_CLIENT_STATE_CONNECTION_TIMED_OUT          = ClientState'ConnectionTimedOut
+  | raw == c'NETCODE_CLIENT_STATE_CONNECTION_RESPONSE_TIMED_OUT = ClientState'ConnectionResponseTimedOut
+  | raw == c'NETCODE_CLIENT_STATE_CONNECTION_REQUEST_TIMED_OUT  = ClientState'ConnectionRequestTimedOut
+  | raw == c'NETCODE_CLIENT_STATE_CONNECTION_DENIED             = ClientState'ConnectionDenied
+  | raw == c'NETCODE_CLIENT_STATE_DISCONNECTED                  = ClientState'Disconnected
+  | raw == c'NETCODE_CLIENT_STATE_SENDING_CONNECTION_REQUEST    = ClientState'SendingConnectionRequest
+  | raw == c'NETCODE_CLIENT_STATE_SENDING_CONNECTION_RESPONSE   = ClientState'SendingConnectionResponse
+  | raw == c'NETCODE_CLIENT_STATE_CONNECTED                     = ClientState'Connected
+  | otherwise = error "Unrecognized client state value"
+
+-- | A client object. This is an opaque type meant to be used in conjunction
+-- with this library.
+--
+-- A 'Client' is generally meant to connect to one of potentially many servers
+-- through a 'ConnectToken'. The main loop of the application that manages the
+-- lifetime of the client is expected to maintain a running timer with a
+-- resolution of at least seconds. This main loop is also expected to call
+-- 'updateClient' on a regular basis to allow the library to process incoming
+-- packets and send outgoing packets.
+data Client = Client 
+  { clientPtr :: Ptr C'netcode_client_t
+  , clientCallbacks :: ClientCallbacks
+  } deriving (Show)
+
+data ClientCallbacks = ClientCallbacks
+  { clientStateChange           :: C'state_change_callback_t
+  , clientSendPacketOverride    :: C'send_packet_override_t
+  , clientReceivePacketOverride :: C'receive_packet_override_t
+  } deriving (Show)
+
+defaultClientCallbacks :: ClientCallbacks
+defaultClientCallbacks = ClientCallbacks nullFunPtr nullFunPtr nullFunPtr
+
+-- | A 'ClientConfig' is a type that specifies the behavior of a 'Client'.
+-- Client configs are pretty spartan: the only options available at this time
+-- are setting callbacks.
+newtype ClientConfig = ClientConfig
+  (   Ptr C'netcode_client_config_t
+   -> ClientCallbacks
+   -> IO (Ptr C'netcode_client_config_t, ClientCallbacks)
+  )
+
+-- | A 'ClientConfig' with no callback overrides.
+defaultClientConfig :: ClientConfig
+defaultClientConfig = ClientConfig $ \clientConfig cbs -> do
+    c'netcode_default_client_config clientConfig
+    return (clientConfig, cbs)
+
+-- | A client-specific callback that gets invoked each time the underlying
+-- state of the client changes.
+type ClientStateChangeCallback
+   = ClientState   -- ^ Old state
+  -> ClientState   -- ^ New state
+  -> IO ()
+
+mkClientStateChangeCallback :: ClientStateChangeCallback
+                            -> IO C'state_change_callback_t
+mkClientStateChangeCallback cb = mk'state_change_callback_t $ \_ oldSt newSt ->
+    cb (typedClientState oldSt) (typedClientState newSt)
+
+-- | Creates a config that removes the existing 'ClientStateChangeCallback' and
+-- instead uses the given callback.
+setClientStateChangeCallback :: ClientStateChangeCallback
+                             -> ClientConfig -> ClientConfig
+setClientStateChangeCallback cb (ClientConfig mkConfig) =
+    ClientConfig $ \configPtr' callbacks' -> do
+        (configPtr, callbacks) <- mkConfig configPtr' callbacks'
+        freeNullFunPtr $ clientStateChange callbacks
+        fPtr <- mkClientStateChangeCallback cb
+        config <- peek configPtr
+        poke configPtr $
+            config { c'netcode_client_config_t'state_change_callback = fPtr }
+        return (configPtr, callbacks { clientStateChange = fPtr })
+
+-- | Clears the 'ClientStateChangeCallback' for the given config.
+clearClientStateChangeCallback :: ClientConfig -> ClientConfig
+clearClientStateChangeCallback (ClientConfig mkConfig) =
+    ClientConfig $ \configPtr' callbacks' -> do
+        (configPtr, callbacks) <- mkConfig configPtr' callbacks'
+        freeNullFunPtr $ clientStateChange callbacks
+        config <- peek configPtr
+        poke configPtr $
+            config { c'netcode_client_config_t'state_change_callback = nullFunPtr }
+        return (configPtr, callbacks { clientStateChange = nullFunPtr })
+
+-- | Removes the existing send and receive overrides for the given config, if
+-- set, and instead uses the ones given.
+setClientSendReceiveOverrides :: SendPacketOverride
+                              -> ReceivePacketOverride
+                              -> ClientConfig -> ClientConfig
+setClientSendReceiveOverrides sendFn recvFn (ClientConfig mkConfig) =
+    ClientConfig $ \configPtr' callbacks' -> do
+        (configPtr, callbacks) <- mkConfig configPtr' callbacks'
+        freeNullFunPtr $ clientSendPacketOverride callbacks
+        freeNullFunPtr $ clientReceivePacketOverride callbacks
+        config <- peek configPtr
+        sendOverride <- mkSendPacketOverride sendFn
+        recvOverride <- mkReceivePacketOverride recvFn
+        poke configPtr $ config
+            { c'netcode_client_config_t'send_packet_override = sendOverride
+            , c'netcode_client_config_t'receive_packet_override = recvOverride
+            , c'netcode_client_config_t'override_send_and_receive = 1
+            }
+        let newcbs = callbacks
+              { clientSendPacketOverride = sendOverride
+              , clientReceivePacketOverride = recvOverride
+              }
+        return (configPtr, newcbs)
+
+-- | Changes the config to use the default send and receive packet functions.
+clearClientSendReceiveOverrides :: ClientConfig -> ClientConfig
+clearClientSendReceiveOverrides (ClientConfig mkConfig) =
+    ClientConfig $ \configPtr' callbacks' -> do
+        (configPtr, callbacks) <- mkConfig configPtr' callbacks'
+        freeNullFunPtr $ clientSendPacketOverride callbacks
+        freeNullFunPtr $ clientReceivePacketOverride callbacks
+        config <- peek configPtr
+        poke configPtr $ config
+            { c'netcode_client_config_t'send_packet_override = nullFunPtr
+            , c'netcode_client_config_t'receive_packet_override = nullFunPtr
+            , c'netcode_client_config_t'override_send_and_receive = 0
+            }
+        let newcbs = callbacks
+              { clientSendPacketOverride = nullFunPtr
+              , clientReceivePacketOverride = nullFunPtr
+              }
+        return (configPtr, newcbs)
+
+-- | Creates a client at the given address using the provided config. Throws an
+-- IOException on failure.
+--
+-- Note, the address used here can be either formatted as an IPv4 address or an
+-- IPv6 address, similar to the arguments passed to 'parseAddress'. In the
+-- common case, you will likely want to use INADDR_ANY to bind to the
+-- underlying socket, which is represented by the address "0.0.0.0"
+--
+-- The time passed to this create function should be a measurement in seconds,
+-- such that when connecting in the future using 'updateClient', the same
+-- resolution timer is being passed. That allows the library to properly
+-- timeout in cases where connections are taking too long to establish.
+createClient :: String -> ClientConfig -> Double -> IO Client
+createClient s (ClientConfig mkConfig) time = alloca $ \clientConfig -> do
+    (config, callbacks) <- mkConfig clientConfig defaultClientCallbacks
+    ptr <- withCString s $ \cs ->
+        c'netcode_client_create cs config (CDouble time)
+    when (ptr == nullPtr) $ fail "Failed to create client!"
+    return (Client ptr callbacks)
+
+-- | Destroys the client and frees all of the Haskell-side function pointers
+-- that were registered as callbacks.
+destroyClient :: Client -> IO ()
+destroyClient (Client c cbs) = do
+    c'netcode_client_destroy c
+    freeNullFunPtr $ clientStateChange cbs
+    freeNullFunPtr $ clientSendPacketOverride cbs
+    freeNullFunPtr $ clientReceivePacketOverride cbs
+
+-- | Generates a random 64-bit client ID to be used with 'generateConnectToken'
+generateClientID :: IO Word64
+generateClientID = alloca $ \idPtr -> do
+    c'netcode_random_bytes (castPtr idPtr) $
+        fromIntegral $ sizeOf (undefined :: Word64)
+    peek idPtr
+
+-- | Begin the process to connect the client to a server stored in the given
+-- 'ConnectToken'. This does not connect the client immediately, but rather
+-- resets the client object and sets the state to
+-- 'ClientState'SendingConnectionRequest'. The client will attempt to connect
+-- on the next call to 'updateClient'.
+connectClient :: Client -> ConnectToken -> IO ()
+connectClient (Client c _) (ConnectToken ctPtr) =
+    withForeignPtr ctPtr (c'netcode_client_connect c)
+
+-- | Disconnects the client from anything it might be connected to.
+disconnectClient :: Client -> IO ()
+disconnectClient (Client c _) = c'netcode_client_disconnect c
+
+-- | Main processing call for clients with the current time in seconds (in the
+-- same domain as the time passed to 'createClient'). This flushes packet
+-- queues at the appropriate rate and updates connection statuses among other
+-- things. It is expected to be called in the main loop of the application.
+updateClient :: Client -> Double -> IO ()
+updateClient (Client c _) = c'netcode_client_update c . CDouble
+
+-- | Returns the current state of the 'Client'.
+getClientState :: Client -> IO ClientState
+getClientState (Client c _) = typedClientState <$> c'netcode_client_state c
+
+-- | Returns true if the 'Client' is in a state considered to be disconnected,
+-- as opposed to connected or connecting.
+isClientDisconnected :: Client -> IO Bool
+isClientDisconnected (Client c _) =
+    (<= c'NETCODE_CLIENT_STATE_DISCONNECTED) <$> c'netcode_client_state c
+
+-- | Returns the sequence number of the next packet that the 'Client' will
+-- send.
+nextClientPacketSequence :: Client -> IO Word64
+nextClientPacketSequence (Client c _) = c'netcode_client_next_packet_sequence c
+
+-- | Enqueues a packet to be sent during the next call to 'updateClient'.
+sendPacketFromClient :: Client -> Int -> Ptr Word8 -> IO ()
+sendPacketFromClient (Client c _) pktSz pktMem = do
+    let pktSize = min c'NETCODE_MAX_PACKET_SIZE (fromIntegral pktSz)
+    when (pktSz > c'NETCODE_MAX_PACKET_SIZE) $ putStrLn $
+        "WARNING: Sending packet that's too large: " <> show pktSz
+    c'netcode_client_send_packet c pktMem pktSize
+
+-- | Dequeues a received packet from the t'Netcode.IO.Server'. This function
+-- returns a @Just@ until the queue is empty, upon which it will return
+-- @Nothing@.
+receivePacketFromServer :: Client -> IO (Maybe Packet)
+receivePacketFromServer (Client c _) =
+    alloca $ \sequenceNumPtr ->
+    alloca $ \pktSzPtr -> do
+        packetMem <- c'netcode_client_receive_packet c pktSzPtr sequenceNumPtr
+        let finalizer = c'netcode_client_free_packet c (castPtr packetMem)
+        if packetMem == nullPtr
+            then return Nothing
+            else fmap Just $
+                Packet <$> peek sequenceNumPtr
+                       <*> (fromIntegral <$> peek pktSzPtr)
+                       <*> newForeignPtr packetMem finalizer
+
+-- | Returns the port assigned to this 'Client'.
+getClientPort :: Client -> IO Word16
+getClientPort (Client c _) = c'netcode_client_get_port c
+
+-- | Performs an action with the address of the server to which the given
+-- 'Client' is connected to. This is meant to minimize the chances that the
+-- 'Address' value will be used in a manner that outlives the given 'Client'.
+-- Callers should avoid storing the 'Address' value or returning it as a result
+-- of this function.
+--
+-- In the event that the client is not connected to a server, the address
+-- passed to the action will be @0.0.0.0@.
+withClientServerAddress :: Client -> (Address -> IO a) -> IO a
+withClientServerAddress (Client c _) fn = do
+    aptr <- c'netcode_client_server_address c
+    if aptr == nullPtr
+        then parseAddress "0.0.0.0" >>= fn
+        else Address <$> newForeignPtr_ aptr >>= fn
+
+-- | A 'ConnectToken' represents an encrypted set of data fields that describe
+-- both the client requesting to make a connection and the available servers to
+-- which that connection can be made. It is generated solely via the 
+newtype ConnectToken = ConnectToken (ForeignPtr Word8)
+
+-- | Gives the maximum size, in bytes, of user data stored in a 'ConnectToken'.
+maximumUserDataSize :: Num a => a
+maximumUserDataSize = c'NETCODE_USER_DATA_BYTES
+
+-- | Returns the maximum number of servers that can be stored in a
+-- 'ConnectToken'.
+maximumServersPerConnect :: Num a => a
+maximumServersPerConnect = c'NETCODE_MAX_SERVERS_PER_CONNECT
+
+-- | Returns the number of bytes expected in the private key used to generate a
+-- 'ConnectToken'
+privateKeySize :: Num a => a
+privateKeySize = c'NETCODE_KEY_BYTES
+
+-- | Creates a connect token for the given client (by clientID) with the list
+-- of associated addresses. User data may be at most 'maximumUserDataSize'
+-- values, otherwise is truncated or zero-padded to fill. The list of public
+-- and internal servers must not be empty and may contain at most
+-- 'maximumServersPerConnect' values, otherwise is truncated. Throws an
+-- IOException on failure.
+generateConnectToken :: [(String, String)] -- ^ Public and internal servers
+                     -> Int                -- ^ Token expiration in seconds
+                     -> Int                -- ^ Token timeout in seconds
+                     -> Word64             -- ^ Unique Client ID
+                     -> Word64             -- ^ Protocol ID
+                     -> [Word8]            -- ^ Private key
+                     -> [Word8]            -- ^ User data
+                     -> IO ConnectToken
+generateConnectToken [] _ _ _ _ _ _ = fail "Connect token server list is empty."
+generateConnectToken addrs expiry timeout clientID protocolID privateKey userData =
+    allocaArray (length addrs) $ \externalAddrs ->
+    allocaArray (length addrs) $ \internalAddrs ->
+        let checkServers =
+                when (length addrs > maximumServersPerConnect) $ putStrLn $
+                "Warning: Too many servers passed to connect token: " <> show (length addrs)
+
+            writeAddrsAndGo i ((s1, s2) : rest) = withCString s1 $ \cs1 -> do
+                pokeElemOff externalAddrs i cs1
+                withCString s2 $ \cs2 -> do
+                    pokeElemOff internalAddrs i cs2
+                    writeAddrsAndGo (i + 1) rest
+            writeAddrsAndGo _ [] =  -- go....
+                allocaArray c'NETCODE_USER_DATA_BYTES $ \userDataBytes ->
+                allocaArray c'NETCODE_KEY_BYTES $ \privateKeyBytes -> do
+                    connectTokenPtr <- mallocForeignPtrBytes c'NETCODE_CONNECT_TOKEN_BYTES
+                    pokeArray privateKeyBytes (take c'NETCODE_KEY_BYTES       $ privateKey <> repeat 0)
+                    pokeArray userDataBytes   (take c'NETCODE_USER_DATA_BYTES $ userData   <> repeat 0)
+                    result <- withForeignPtr connectTokenPtr $ \connectTokenBytes ->
+                        c'netcode_generate_connect_token (fromIntegral $ length addrs)
+                                                         externalAddrs
+                                                         internalAddrs
+                                                         (fromIntegral expiry)
+                                                         (fromIntegral timeout)
+                                                         clientID
+                                                         protocolID
+                                                         privateKeyBytes
+                                                         userDataBytes
+                                                         connectTokenBytes
+                    when (result == c'NETCODE_ERROR) $ fail "Error generating connect token"
+                    return $ ConnectToken connectTokenPtr
+          in checkServers >> writeAddrsAndGo 0 addrs
+    
diff --git a/Netcode/IO/Packet.hs b/Netcode/IO/Packet.hs
new file mode 100644
--- /dev/null
+++ b/Netcode/IO/Packet.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE DeriveGeneric #-}
+module Netcode.IO.Packet where
+
+--------------------------------------------------------------------------------
+
+import Data.Word (Word8, Word64)
+import Foreign.ForeignPtr (ForeignPtr)
+import GHC.Generics (Generic)
+
+import Bindings.Netcode.IO
+
+--------------------------------------------------------------------------------
+
+-- | A packet is a basic unit of data that is transferred between client and
+-- server. Sequence numbers indicate the order in which the packets were sent,
+-- and this library contains no guarantee that they will be received in a
+-- monotonically increasing order.
+data Packet = Packet {
+    -- | The sequence number for this packet.
+    packetSequenceNumber :: Word64,
+    -- | The size, in bytes, of the data stored at 'packetDataPtr'
+    packetSize :: Int,
+    -- | A pointer to the bytes that are contained in this packet. This can be
+    -- cast to any pointer type for the purposes of deseralizing, but this
+    -- pointer must outlive the amount of time that this library has been
+    -- initialized.
+    packetDataPtr :: ForeignPtr Word8
+} deriving (Show, Generic)
+
+-- | The maximum size, in bytes, of a packet. In other words, this is the
+-- maximum value that 'packetSize' can take.
+maximumPacketSize :: Num a => a
+maximumPacketSize = c'NETCODE_MAX_PACKET_SIZE
diff --git a/Netcode/IO/Server.hs b/Netcode/IO/Server.hs
new file mode 100644
--- /dev/null
+++ b/Netcode/IO/Server.hs
@@ -0,0 +1,403 @@
+module Netcode.IO.Server (
+    -- * Servers
+
+    -- ** Server-specific callbacks
+      ServerConnectDisconnectCallback
+
+    -- ** Server configs
+    , ServerConfig
+    , defaultServerConfig
+    , setProtocolID
+    , setPrivateKey
+    , setServerConnectDisconnectCallback, clearServerConnectDisconnectCallback
+    , setServerSendReceiveOverrides, clearServerSendReceiveOverrides
+
+    -- ** Server objects
+    , Server
+    , createServer
+    , destroyServer
+    , startServer
+    , maxNumClients
+    , stopServer
+    , updateServer
+    , clientConnectedAtIndex
+    , clientIdAtIndex
+    , withClientAddressAtIndex
+    , withClientUserDataAtIndex
+    , clientUserDataAtIndex
+    , maxClientsForServer
+    , numConnectedClients
+    , isServerRunning
+    , isServerFull
+    , getServerPort
+    , sendPacketFromServer
+    , broadcastPacketFromServer
+    , disconnectClientFromServer
+    , disconnectAllClientsFromServer
+    , receivePacketFromClient
+    , nextServerPacketSequence
+) where
+
+--------------------------------------------------------------------------------
+
+import Control.Applicative   (liftA2)
+import Control.Monad         (when, forM_)
+import Data.Word             (Word8, Word16, Word64)
+import Foreign.C.String      (withCString)
+import Foreign.C.Types       (CDouble(..))
+import Foreign.Concurrent    (newForeignPtr)
+import Foreign.ForeignPtr    (newForeignPtr_)
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Marshal.Array (peekArray)
+import Foreign.Ptr           ( Ptr, nullPtr, castPtr
+                             , FunPtr, nullFunPtr, freeHaskellFunPtr
+                             )
+import Foreign.Storable      (peek, poke)
+
+import Bindings.Netcode.IO
+import Netcode.IO.Address
+import Netcode.IO.Callbacks
+import Netcode.IO.Packet
+
+--------------------------------------------------------------------------------
+
+freeNullFunPtr :: FunPtr a -> IO ()
+freeNullFunPtr x
+    | x == nullFunPtr = return ()
+    | otherwise       = freeHaskellFunPtr x
+
+--------------------------------------------------------------------------------
+
+-- | A server object. This is an opaque type meant to be used in conjunction
+-- with this library.
+--
+-- A server is generally meant to represent and endpoint for one or more
+-- t'Network.IO.Client's to connect to. The server application is similar to
+-- that of the client in that it is expected to have a running timer with a
+-- resolution of at least seconds. The main loop of the server application is
+-- meant to call 'updateServer' to allow the library to process incoming
+-- packets and send outgoing packets to the clients.
+data Server = Server 
+    { serverPtr :: Ptr C'netcode_server_t
+    , serverCallbacks :: ServerCallbacks
+    } deriving (Show)
+
+data ServerCallbacks = ServerCallbacks
+    { serverConnectDisconnect     :: C'connect_disconnect_callback_t
+    , serverSendPacketOverride    :: C'send_packet_override_t
+    , serverReceivePacketOverride :: C'receive_packet_override_t
+    } deriving (Show)
+
+defaultServerCallbacks :: ServerCallbacks
+defaultServerCallbacks = ServerCallbacks nullFunPtr nullFunPtr nullFunPtr
+
+-- | A 'ServerConfig' is a type that specifies the behavior of a 'Server' and
+-- contains associated metadata.
+--
+-- At a minimum, the connection protocol ID and the private key for the
+-- application should be set for each server (via 'setProtocolID' and
+-- 'setPrivateKey', respectively).
+newtype ServerConfig = ServerConfig
+    (   Ptr C'netcode_server_config_t
+        -> ServerCallbacks
+        -> IO (Ptr C'netcode_server_config_t, ServerCallbacks)
+    )
+
+-- | The default 'ServerConfig' contains no callbacks or overrides, and
+-- contains empty values for the required fields needed to properly have
+-- a server respond to a connecting t'Netcode.IO.Client'.
+defaultServerConfig :: ServerConfig
+defaultServerConfig = ServerConfig $ \serverConfig cbs -> do
+    c'netcode_default_server_config serverConfig
+    return (serverConfig, cbs)
+
+-- | Sets the connection protocol ID used by this 'Server'. This is a unique
+-- ID that must match the protocol ID used in
+-- 'Netcode.IO.generateConnectToken'
+setProtocolID :: Word64 -> ServerConfig -> ServerConfig
+setProtocolID protocolID (ServerConfig mkServerPtr) =
+    ServerConfig $ \serverConfig cbs' -> do
+        (configPtr, cbs) <- mkServerPtr serverConfig cbs'
+        config <- peek configPtr
+        poke configPtr $
+            config { c'netcode_server_config_t'protocol_id = protocolID }
+        return (configPtr, cbs)
+
+-- | Sets the private key used by this 'Server'. This key must match the
+-- private key used in 'Netcode.IO.generateConnectToken'
+setPrivateKey :: [Word8] -> ServerConfig -> ServerConfig
+setPrivateKey key (ServerConfig mkServerPtr) =
+    ServerConfig $ \serverConfig cbs' -> do
+        (configPtr, cbs) <- mkServerPtr serverConfig cbs'
+        config <- peek configPtr
+        poke configPtr $    
+            config {
+                c'netcode_server_config_t'private_key = 
+                    take c'NETCODE_KEY_BYTES (key <> repeat 0)
+            }
+        return (configPtr, cbs)
+
+-- | A server-specific callback that gets invoked each time a client either
+-- connects to, or disconnects from, the server.
+type ServerConnectDisconnectCallback
+   = Int  -- ^ Client index connected
+  -> Bool -- ^ True if the client connected
+  -> IO ()
+
+mkServerConnectDisconnectCallback :: ServerConnectDisconnectCallback
+                                  -> IO C'connect_disconnect_callback_t
+mkServerConnectDisconnectCallback cb = mk'connect_disconnect_callback_t $
+    \_ clientIdx connected -> cb (fromIntegral clientIdx) (connected /= 0)
+
+-- | Replaces the existing 'ServerConnectDisconnectCallback' with the given one
+-- and frees any associated memory that may be allocated for the the existing
+-- callback.
+setServerConnectDisconnectCallback :: ServerConnectDisconnectCallback
+                                    -> ServerConfig -> ServerConfig
+setServerConnectDisconnectCallback cb (ServerConfig mkConfig) =
+    ServerConfig $ \configPtr' callbacks' -> do
+        (configPtr, callbacks) <- mkConfig configPtr' callbacks'
+        freeNullFunPtr $ serverConnectDisconnect callbacks
+        fPtr <- mkServerConnectDisconnectCallback cb
+        config <- peek configPtr
+        poke configPtr $ config
+            { c'netcode_server_config_t'connect_disconnect_callback = fPtr
+            }
+        return (configPtr, callbacks { serverConnectDisconnect = fPtr })
+
+-- | Removes the existing 'ServerConnectDisconnectCallback' and frees any
+-- associated memory that may be allocated for the the existing callback.
+clearServerConnectDisconnectCallback :: ServerConfig -> ServerConfig
+clearServerConnectDisconnectCallback (ServerConfig mkConfig) =
+    ServerConfig $ \configPtr' callbacks' -> do
+        (configPtr, callbacks) <- mkConfig configPtr' callbacks'
+        freeNullFunPtr $ serverConnectDisconnect callbacks
+        config <- peek configPtr
+        poke configPtr $ config
+            { c'netcode_server_config_t'connect_disconnect_callback = nullFunPtr
+            }
+        return (configPtr, callbacks { serverConnectDisconnect = nullFunPtr })
+
+-- | Removes the existing send and receive overrides for the given config, if
+-- set, and instead uses the ones given.
+setServerSendReceiveOverrides :: SendPacketOverride
+                            -> ReceivePacketOverride
+                            -> ServerConfig -> ServerConfig
+setServerSendReceiveOverrides sendFn recvFn (ServerConfig mkConfig) =
+    ServerConfig $ \configPtr' callbacks' -> do
+        (configPtr, callbacks) <- mkConfig configPtr' callbacks'
+        freeNullFunPtr $ serverSendPacketOverride callbacks
+        freeNullFunPtr $ serverReceivePacketOverride callbacks
+        config <- peek configPtr
+        sendOverride <- mkSendPacketOverride sendFn
+        recvOverride <- mkReceivePacketOverride recvFn
+        poke configPtr $ config
+            { c'netcode_server_config_t'send_packet_override = sendOverride
+            , c'netcode_server_config_t'receive_packet_override = recvOverride
+            , c'netcode_server_config_t'override_send_and_receive = 1
+            }
+        let newcbs = callbacks
+                { serverSendPacketOverride = sendOverride
+                , serverReceivePacketOverride = recvOverride
+                }
+        return (configPtr, newcbs)
+
+-- | Changes the config to use the default send and receive packet functions.
+clearServerSendReceiveOverrides :: ServerConfig -> ServerConfig
+clearServerSendReceiveOverrides (ServerConfig mkConfig) =
+    ServerConfig $ \configPtr' callbacks' -> do
+        (configPtr, callbacks) <- mkConfig configPtr' callbacks'
+        freeNullFunPtr $ serverSendPacketOverride callbacks
+        freeNullFunPtr $ serverReceivePacketOverride callbacks
+        config <- peek configPtr
+        poke configPtr $ config
+            { c'netcode_server_config_t'send_packet_override = nullFunPtr
+            , c'netcode_server_config_t'receive_packet_override = nullFunPtr
+            , c'netcode_server_config_t'override_send_and_receive = 0
+            }
+        let newcbs = callbacks
+                { serverSendPacketOverride = nullFunPtr
+                , serverReceivePacketOverride = nullFunPtr
+                }
+        return (configPtr, newcbs)
+
+-- | Creates a server at the given address using the provided config. Throws an
+-- IOException on failure.
+--
+-- Note, the address used here can be either formatted as an IPv4 address or an
+-- IPv6 address, similar to the arguments passed to 'parseAddress'. In the
+-- common case, you will likely want to use INADDR_ANY to bind to the
+-- underlying socket, which is represented by the address "0.0.0.0"
+--
+-- The time passed to this create function should be a measurement in seconds,
+-- such that when connecting in the future using 'updateServer', the same
+-- resolution timer is being passed. That allows the library to properly
+-- timeout in cases where connections are taking too long to establish.
+createServer :: String -> ServerConfig -> Double -> IO Server
+createServer s (ServerConfig mkConfig) time = alloca $ \serverConfig -> do
+    (config, cbs) <- mkConfig serverConfig defaultServerCallbacks
+    ptr <- withCString s (\cs -> c'netcode_server_create cs config (CDouble time))
+    when (ptr == nullPtr) $ fail "Failed to create server!"
+    return (Server ptr cbs)
+
+-- | Starts the server and specifies the maximum number of clients that can
+-- connect. Emits a warning when the maximum number of clients is more than
+-- 'maxNumClients'.
+startServer :: Server -> Int -> IO ()
+startServer (Server s _) n = do
+    when (n > maxNumClients) $ putStrLn $ concat [
+          "Warning: Can only start server with at most "
+        , show (maxNumClients :: Int), " clients. Requested: ", show n
+        ]
+    c'netcode_server_start s (min maxNumClients $ fromIntegral n)
+
+-- | Returns the maximum number of clients that a server can support.
+maxNumClients :: Num a => a
+maxNumClients = c'NETCODE_MAX_CLIENTS
+
+-- | Stops the server.
+stopServer :: Server -> IO ()
+stopServer (Server s _) = c'netcode_server_stop s
+
+-- | Destroys the server object and frees the associated Haskell-side callbacks
+-- and overrides
+destroyServer :: Server -> IO ()
+destroyServer (Server s cbs) = do
+    c'netcode_server_destroy s
+    freeNullFunPtr $ serverConnectDisconnect cbs
+    freeNullFunPtr $ serverSendPacketOverride cbs
+    freeNullFunPtr $ serverReceivePacketOverride cbs
+
+-- | Main processing call for a 'Server' with the current time in seconds (in
+-- the same domain as the time passed to 'createServer'). This flushes packet
+-- queues at the appropriate rate and updates connection statuses among other
+-- things. It is expected to be called in the main loop of the application.
+updateServer :: Server -> Double -> IO ()
+updateServer (Server s _) = c'netcode_server_update s . CDouble
+
+-- | Returns @True@ if the client at the given client index is connected to
+-- the server. Returns @False@ if not connected, if the server is not running,
+-- or if the client index is out of bounds.
+clientConnectedAtIndex :: Server -> Int -> IO Bool
+clientConnectedAtIndex (Server s _) =
+    fmap (/= 0) . c'netcode_server_client_connected s . fromIntegral
+
+-- | Returns the client ID of the client at the given client index. Returns @0@
+-- if not connected, the server is not running, or if the client index is out
+-- of bounds.
+clientIdAtIndex :: Server -> Int -> IO Word64
+clientIdAtIndex (Server s _) = c'netcode_server_client_id s . fromIntegral
+
+-- | Performs an action with the address of the client to which the given
+-- 'Server' is connected to. This is meant to minimize the chances that the
+-- 'Address' value will be used in a manner that outlives the given 'Server' or
+-- the connection lifetime of the client. Callers should avoid storing the
+-- 'Address' value or returning it as a result of this function.
+--
+-- In the event that the client index is out of bounds, or the client is not
+-- connected at that slot, the address passed to the action will be @0.0.0.0@.
+withClientAddressAtIndex :: Server -> Int -> (Address -> IO a) -> IO a
+withClientAddressAtIndex (Server s _) cidx fn = do
+    aptr <- c'netcode_server_client_address s (fromIntegral cidx)
+    if aptr == nullPtr
+        then parseAddress "0.0.0.0" >>= fn
+        else Address <$> newForeignPtr_ aptr >>= fn
+
+-- | Performs an action with the user data of the client to which the given
+-- 'Server' is connected to. This is meant to minimize the chances that the
+-- memory buffer will be used in a manner that outlives the given 'Server' or
+-- the connection lifetime of the client. Callers should avoid storing the
+-- @Ptr@ value or returning it as a result of this function.
+--
+-- In the event that the client index is out of bounds, or the client is not
+-- connected at that slot, the given action will receive @nullPtr@.
+withClientUserDataAtIndex :: Server -> Int -> (Ptr () -> IO a) -> IO a
+withClientUserDataAtIndex (Server s _) cidx fn =
+    c'netcode_server_client_user_data s (fromIntegral cidx) >>= fn
+
+-- | Returns the user data for the client connected at the given client index.
+--
+-- In the event that the client index is out of bounds, or the client is not
+-- connected at that slot, the result will be the empty list.
+clientUserDataAtIndex :: Server -> Int -> IO [Word8]
+clientUserDataAtIndex s i = withClientUserDataAtIndex s i $ \ptr ->
+    if ptr == nullPtr
+        then return []
+        else peekArray c'NETCODE_USER_DATA_BYTES (castPtr ptr)
+
+-- | Returns the maximum number of clients that can connect to this server, or
+-- zero if the server has not been started yet (via a call to 'startServer').
+maxClientsForServer :: Server -> IO Int
+maxClientsForServer (Server s _) =
+    fromIntegral <$> c'netcode_server_max_clients s
+
+-- | Returns the number of currently connected clients.
+numConnectedClients :: Server -> IO Int
+numConnectedClients (Server s _) =
+    fromIntegral <$> c'netcode_server_num_connected_clients s
+
+-- | Returns True if the server has been started, and is ready to accept
+-- incoming connections from clients.
+isServerRunning :: Server -> IO Bool
+isServerRunning (Server s _) = (/= 0) <$> c'netcode_server_running s
+
+-- | Returns true if the number of connected clients matches the maximum number
+-- of possibly connected clients.
+isServerFull :: Server -> IO Bool
+isServerFull (Server s _) =
+    liftA2 (==) (c'netcode_server_num_connected_clients s)
+                (c'netcode_server_max_clients s)
+
+-- | Returns the port assigned to the server's IP address.
+getServerPort :: Server -> IO Word16
+getServerPort (Server s _) = c'netcode_server_get_port s
+
+-- | Disconnects the client at the given index from the server.
+disconnectClientFromServer :: Server -> Int -> IO ()
+disconnectClientFromServer (Server s _) =
+    c'netcode_server_disconnect_client s . fromIntegral
+
+-- | Disconnects all clients from the server.
+disconnectAllClientsFromServer :: Server -> IO ()
+disconnectAllClientsFromServer (Server s _) =
+    c'netcode_server_disconnect_all_clients s
+
+-- | Returns the next sequence number of a packet destined for the client at
+-- the given client index.
+nextServerPacketSequence :: Server -> Int -> IO Word64
+nextServerPacketSequence (Server s _) =
+    c'netcode_server_next_packet_sequence s . fromIntegral
+
+-- | Enqueues a packet to be sent to the client at the given index during the
+-- next call to 'updateServer'.
+sendPacketFromServer :: Server
+                     -> Int        -- ^ Client index
+                     -> Int        -- ^ Size in bytes of packet data
+                     -> Ptr Word8  -- ^ Packet data buffer
+                     -> IO ()
+sendPacketFromServer (Server s _) clientIdx pktSz pktMem = do
+    let pktSize = min c'NETCODE_MAX_PACKET_SIZE (fromIntegral pktSz)
+    when (pktSz > c'NETCODE_MAX_PACKET_SIZE) $ putStrLn $
+            "WARNING: Sending packet that's too large: " <> show pktSz
+    c'netcode_server_send_packet s (fromIntegral clientIdx) pktMem pktSize
+
+-- | Enqueues a packet with the given size and data to all connected clients.
+broadcastPacketFromServer :: Server -> Int -> Ptr Word8 -> IO ()
+broadcastPacketFromServer s pktSz pktMem = do
+    numClients <- numConnectedClients s
+    forM_ [0..(numClients - 1)] $ \i -> sendPacketFromServer s i pktSz pktMem
+
+-- | Dequeues a received packet from the t'Netcode.IO.Client' at the given
+-- client index. This function returns a @Just@ until the queue is empty, upon
+-- which it will return a @Nothing@.
+receivePacketFromClient :: Server -> Int -> IO (Maybe Packet)
+receivePacketFromClient (Server s _) clientIdx =
+    alloca $ \sequenceNumPtr ->
+    alloca $ \pktSzPtr -> do
+        packetMem <- c'netcode_server_receive_packet s (fromIntegral clientIdx) pktSzPtr sequenceNumPtr
+        if packetMem == nullPtr
+            then return Nothing
+            else fmap Just $
+                Packet <$> peek sequenceNumPtr
+                        <*> (fromIntegral <$> peek pktSzPtr)
+                        <*> newForeignPtr packetMem (c'netcode_server_free_packet s (castPtr packetMem))
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,53 @@
+# netcode-io
+Haskell bindings to the
+[netcode.io](https://github.com/networkprotocol/netcode.io) library
+
+## Development
+
+These bindings were developed using `stack` in the "usual" way. We expect
+that anyone building from source to use the same workflow.
+
+### Windows
+
+The cabal file specifies that the `sodium` library is required for building
+this package. Unfortunately, this is a bit tricky on Windows since there's no
+native package manager. To use this with Windows, we recommend using
+[`MSYS2`](https://www.msys2.org/). `msys2.exe` has a built-in shell for using
+the `gcc` toolchain on Windows. This toolchain is compatible with the one `ghc`
+uses. Within this environment, you can install `sodium` using the `pacman`
+package manager:
+
+```
+$ pacman -S mingw64/mingw-w64-x86_64-libsodium
+```
+
+Once installed, you can add the following lines to your global `config.yaml`:
+
+```
+extra-include-dirs:
+  - D:\msys64\mingw64\include
+
+extra-lib-dirs:
+  - D:\msys64\mingw64\lib
+```
+
+Your `config.yaml` is located at your `%STACK_ROOT%` directory:
+
+```
+C:\> stack path --stack-root
+C:\path\to\stack\root
+```
+
+### OS X
+
+On MacOS, we can install `sodium` simply by using homebrew:
+
+```
+$ brew install libsodium
+```
+
+## LICENSING
+
+IANAL, but these bindings use
+[the same license](https://github.com/networkprotocol/netcode.io/blob/master/LICENCE)
+that the original library uses, so you must comply with both.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/examples/Client.hs b/examples/Client.hs
new file mode 100644
--- /dev/null
+++ b/examples/Client.hs
@@ -0,0 +1,114 @@
+-- This example serves as a counterpart to the example program of the same name
+-- in the C library.
+module Main (main) where
+
+import qualified Netcode.IO
+
+import Control.Exception (try, throw, AsyncException(..))
+import Control.Monad (forM_, when)
+import Data.Word (Word64, Word8)
+import Foreign.ForeignPtr (withForeignPtr)
+import Foreign.Marshal.Array (peekArray, withArrayLen)
+import System.Environment (getArgs)
+import System.Exit (ExitCode(..), exitSuccess)
+
+gProtocolID :: Word64
+gProtocolID = 0x1122334455667788
+
+gPrivateKey :: [Word8]
+gPrivateKey =
+    [ 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
+    ]
+
+gConnectTokenExpiry :: Int
+gConnectTokenExpiry = 30
+
+gConnectTokenTimeout :: Int
+gConnectTokenTimeout = 5
+
+main :: IO ()
+main = do
+    Netcode.IO.initialize
+
+    Netcode.IO.logLevel Netcode.IO.LogLevel'Info
+
+    putStrLn "[client]"
+    let stateChangeCallback old new =
+            putStrLn $ "Old state: " <> show old <> " new state: " <> show new
+        clientConfig = Netcode.IO.setClientStateChangeCallback stateChangeCallback
+                     $ Netcode.IO.defaultClientConfig
+    client <- Netcode.IO.createClient "0.0.0.0" clientConfig 0.0
+
+    args <- getArgs
+    let serverAddr = case length args of
+          1 -> head args
+          _ -> "127.0.0.1:40000"
+
+    -- This is expected to be performed on an HTTP server
+    clientID <- Netcode.IO.generateClientID
+    putStrLn $ "client id is " <> show clientID
+
+    connectToken <- Netcode.IO.generateConnectToken [(serverAddr, serverAddr)]
+                                                    gConnectTokenExpiry 
+                                                    gConnectTokenTimeout
+                                                    clientID
+                                                    gProtocolID
+                                                    gPrivateKey
+                                                    []
+
+    Netcode.IO.connectClient client connectToken
+
+    let untilM :: IO Bool -> IO ()
+        untilM cond = do
+            tf <- cond
+            if tf then return () else untilM cond
+
+        catchUserInterrupt :: IO a -> IO a
+        catchUserInterrupt prg = do
+            interruptResult <- try prg
+            case interruptResult of
+                (Left UserInterrupt) -> putStrLn "\nshutting down" >> exitSuccess
+                (Left e) -> throw e
+                (Right x) -> return x
+
+    quitResult <- try $ catchUserInterrupt $ forM_ [0.0, 0.016667 ..] $ \time -> do
+        Netcode.IO.updateClient client time
+
+        clientState <- Netcode.IO.getClientState client
+        when (clientState == Netcode.IO.ClientState'Connected) $
+            withArrayLen [0..(Netcode.IO.maximumPacketSize - 1)] $
+            Netcode.IO.sendPacketFromClient client
+
+        untilM $ do
+            mpkt <- Netcode.IO.receivePacketFromServer client
+            case mpkt of
+                Nothing -> return True
+                Just pkt -> do
+                    withForeignPtr (Netcode.IO.packetDataPtr pkt) $ \pktMem -> do
+                        pktData <- peekArray (Netcode.IO.packetSize pkt) pktMem
+                        case (and $ zipWith (==) pktData [0,1..]) of
+                            True -> return False
+                            False -> fail "Received garbled packet!"
+                    
+        clientState' <- Netcode.IO.getClientState client
+        when (any (== clientState') [ Netcode.IO.ClientState'ConnectTokenExpired
+                                    , Netcode.IO.ClientState'InvalidConnectToken
+                                    , Netcode.IO.ClientState'ConnectionTimedOut
+                                    , Netcode.IO.ClientState'ConnectionResponseTimedOut
+                                    , Netcode.IO.ClientState'ConnectionRequestTimedOut
+                                    , Netcode.IO.ClientState'ConnectionDenied
+                                    , Netcode.IO.ClientState'Disconnected
+                                    ]) exitSuccess
+
+        Netcode.IO.sleep 0.016667
+
+    case quitResult of
+        (Left ExitSuccess) -> return ()
+        (Right _) -> fail "forM_ with infinite list terminated?"
+        (Left e) -> fail $ show e
+
+    Netcode.IO.destroyClient client
+    Netcode.IO.terminate
diff --git a/examples/ClientServer.hs b/examples/ClientServer.hs
new file mode 100644
--- /dev/null
+++ b/examples/ClientServer.hs
@@ -0,0 +1,142 @@
+-- This example serves as a counterpart to the example program of the same name
+-- in the C library.
+module Main (main) where
+
+import qualified Netcode.IO
+
+import Control.Exception (try, throw, AsyncException(..))
+import Control.Monad (forM_, when)
+import Data.IORef (newIORef, readIORef, modifyIORef)
+import Data.Word (Word64, Word8)
+import Foreign.ForeignPtr (withForeignPtr)
+import Foreign.Marshal.Array (peekArray, withArrayLen)
+import System.Exit (ExitCode(..), exitSuccess)
+
+gProtocolID :: Word64
+gProtocolID = 0x1122334455667788
+
+gPrivateKey :: [Word8]
+gPrivateKey =
+    [ 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
+    ]
+
+gConnectTokenExpiry :: Int
+gConnectTokenExpiry = 30
+
+gConnectTokenTimeout :: Int
+gConnectTokenTimeout = 5
+
+main :: IO ()
+main = do
+    Netcode.IO.initialize
+
+    Netcode.IO.logLevel Netcode.IO.LogLevel'Info
+
+    putStrLn "[client/server]"
+    client <- Netcode.IO.createClient "::" Netcode.IO.defaultClientConfig 0.0
+
+    let serverAddrNoPort = "[::1]:00000"
+        serverConfig =
+            Netcode.IO.setPrivateKey gPrivateKey $
+            Netcode.IO.setProtocolID gProtocolID $
+            Netcode.IO.defaultServerConfig
+    server <- Netcode.IO.createServer serverAddrNoPort serverConfig 0.0
+
+    serverAddr <- ("[::1]:" <>) . show <$> Netcode.IO.getServerPort server
+    Netcode.IO.startServer server 1
+
+    -- This is expected to be performed on an HTTP server
+    clientID <- Netcode.IO.generateClientID
+    putStrLn $ "client id is " <> show clientID
+
+    connectToken <- Netcode.IO.generateConnectToken [(serverAddr, serverAddr)]
+                                                    gConnectTokenExpiry 
+                                                    gConnectTokenTimeout
+                                                    clientID
+                                                    gProtocolID
+                                                    gPrivateKey
+                                                    []
+
+    Netcode.IO.connectClient client connectToken
+
+    let untilM :: IO Bool -> IO ()
+        untilM cond = do
+            tf <- cond
+            if tf then return () else untilM cond
+
+        catchUserInterrupt :: IO a -> IO a
+        catchUserInterrupt prg = do
+            interruptResult <- try prg
+            case interruptResult of
+                (Left UserInterrupt) -> putStrLn "\nshutting down" >> exitSuccess
+                (Left e) -> throw e
+                (Right x) -> return x
+
+    numServerPacketsReceived <- newIORef (0 :: Int)
+    numClientPacketsReceived <- newIORef (0 :: Int)
+
+    quitResult <- try $ catchUserInterrupt $ forM_ [0.0, 0.016667 ..] $ \time -> do
+        Netcode.IO.updateClient client time
+        Netcode.IO.updateServer server time
+
+        clientState <- Netcode.IO.getClientState client
+        when (clientState == Netcode.IO.ClientState'Connected) $
+            withArrayLen [0..(Netcode.IO.maximumPacketSize - 1)] $
+            Netcode.IO.sendPacketFromClient client
+
+        clientConnected <- Netcode.IO.clientConnectedAtIndex server 0
+        when clientConnected $
+            withArrayLen [0..(Netcode.IO.maximumPacketSize - 1)] $
+            Netcode.IO.sendPacketFromServer server 0
+
+        untilM $ do
+            mpkt <- Netcode.IO.receivePacketFromServer client
+            case mpkt of
+                Nothing -> return True
+                Just pkt -> do
+                    modifyIORef numClientPacketsReceived (+ 1)
+                    withForeignPtr (Netcode.IO.packetDataPtr pkt) $ \pktMem -> do
+                        pktData <- peekArray (Netcode.IO.packetSize pkt) pktMem
+                        case (and $ zipWith (==) pktData [0,1..]) of
+                            True -> return False
+                            False -> fail "Received garbled packet!"
+
+        untilM $ do
+            mpkt <- Netcode.IO.receivePacketFromClient server 0
+            case mpkt of
+                Nothing -> return True
+                Just pkt -> do
+                    modifyIORef numServerPacketsReceived (+ 1)
+                    withForeignPtr (Netcode.IO.packetDataPtr pkt) $ \pktMem -> do
+                        pktData <- peekArray (Netcode.IO.packetSize pkt) pktMem
+                        case (and $ zipWith (==) pktData [0,1..]) of
+                            True -> return False
+                            False -> fail "Received garbled packet!"
+                    
+        numClientPackets <- readIORef numClientPacketsReceived
+        numServerPackets <- readIORef numServerPacketsReceived
+        if (numClientPackets >= 10 && numServerPackets >= 10)
+            then do
+                conn <- Netcode.IO.clientConnectedAtIndex server 0
+                when conn $ do
+                    putStrLn "client and server successfully exchanged packets"
+                    Netcode.IO.disconnectClientFromServer server 0
+                exitSuccess
+            else return ()
+
+        disconnected <- Netcode.IO.isClientDisconnected client
+        when disconnected exitSuccess
+
+        Netcode.IO.sleep 0.016667
+
+    case quitResult of
+        (Left ExitSuccess) -> return ()
+        (Right _) -> fail "forM_ with infinite list terminated?"
+        (Left e) -> fail $ show e
+
+    Netcode.IO.destroyServer server
+    Netcode.IO.destroyClient client
+    Netcode.IO.terminate
diff --git a/examples/RunCUnitTests.hs b/examples/RunCUnitTests.hs
new file mode 100644
--- /dev/null
+++ b/examples/RunCUnitTests.hs
@@ -0,0 +1,15 @@
+module Main (main) where
+
+import Bindings.Netcode.IO ( c'NETCODE_OK
+                           , c'netcode_init, c'netcode_test, c'netcode_term
+                           )
+
+main :: IO ()
+main = do
+    initResult <- c'netcode_init
+    if initResult == c'NETCODE_OK
+      then return ()
+      else fail "Failed to initialize netcode.io"
+    c'netcode_test
+    c'netcode_term
+    putStrLn "Success!"
diff --git a/examples/Server.hs b/examples/Server.hs
new file mode 100644
--- /dev/null
+++ b/examples/Server.hs
@@ -0,0 +1,82 @@
+-- This example serves as a counterpart to the example program of the same name
+-- in the C library.
+module Main (main) where
+
+import qualified Netcode.IO
+
+import Control.Exception (try, throw, AsyncException(..))
+import Control.Monad (forM_, when)
+import Data.Word (Word64, Word8)
+import Foreign.ForeignPtr (withForeignPtr)
+import Foreign.Marshal.Array (peekArray, withArrayLen)
+import System.Environment (getArgs)
+import System.Exit (ExitCode(..), exitSuccess)
+
+gProtocolID :: Word64
+gProtocolID = 0x1122334455667788
+
+gPrivateKey :: [Word8]
+gPrivateKey =
+    [ 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
+    ]
+
+main :: IO ()
+main = do
+    Netcode.IO.initialize
+
+    Netcode.IO.logLevel Netcode.IO.LogLevel'Info
+
+    putStrLn "[server]"
+    args <- getArgs
+    let serverAddr = if length args == 1 then head args else "127.0.0.1:40000"
+        serverConfig =
+            Netcode.IO.setPrivateKey gPrivateKey $
+            Netcode.IO.setProtocolID gProtocolID $
+            Netcode.IO.defaultServerConfig
+    server <- Netcode.IO.createServer serverAddr serverConfig 0.0
+
+    Netcode.IO.startServer server Netcode.IO.maxNumClients
+
+    let untilM :: IO Bool -> IO ()
+        untilM cond = do
+            tf <- cond
+            if tf then return () else untilM cond
+
+        catchUserInterrupt :: IO a -> IO a
+        catchUserInterrupt prg = do
+            interruptResult <- try prg
+            case interruptResult of
+                (Left UserInterrupt) -> putStrLn "\nshutting down" >> exitSuccess
+                (Left e) -> throw e
+                (Right x) -> return x
+
+    quitResult <- try $ catchUserInterrupt $ forM_ [0.0, 0.016667 ..] $ \time -> do
+        Netcode.IO.updateServer server time
+
+        clientConnected <- Netcode.IO.clientConnectedAtIndex server 0
+        when clientConnected $
+            withArrayLen [0..(Netcode.IO.maximumPacketSize - 1)] $
+            Netcode.IO.sendPacketFromServer server 0
+
+        forM_ [0..(Netcode.IO.maxNumClients - 1)] $ \client -> untilM $ do
+          mpkt <- Netcode.IO.receivePacketFromClient server client
+          case mpkt of
+            Nothing -> return True
+            Just pkt -> withForeignPtr (Netcode.IO.packetDataPtr pkt) $ \pktMem -> do
+                pktData <- peekArray (Netcode.IO.packetSize pkt) pktMem
+                case (and $ zipWith (==) pktData [0,1..]) of
+                  True -> return False
+                  False -> fail "Received garbled packet!"
+
+        Netcode.IO.sleep 0.016667
+
+    case quitResult of
+        (Left ExitSuccess) -> return ()
+        (Right _) -> fail "forM_ with infinite list terminated?"
+        (Left e) -> fail $ show e
+
+    Netcode.IO.destroyServer server
+    Netcode.IO.terminate
diff --git a/examples/Soak.hs b/examples/Soak.hs
new file mode 100644
--- /dev/null
+++ b/examples/Soak.hs
@@ -0,0 +1,293 @@
+{-# LANGUAGE TupleSections #-}
+module Main (main) where
+
+--------------------------------------------------------------------------------
+
+import Control.Exception (try, AsyncException(..))
+import Control.Monad (forM_, foldM, filterM, when)
+import Data.Bifunctor (second)
+import Data.Bool (bool)
+import Data.Function (on)
+import Data.List (delete, deleteBy)
+import Data.Maybe (fromJust)
+import Data.IORef (IORef, newIORef, readIORef, writeIORef)
+import Data.Word (Word8, Word64)
+import Foreign.ForeignPtr (withForeignPtr)
+import Foreign.Marshal.Array (peekArray, withArrayLen)
+import System.Environment (getArgs)
+import System.IO (hSetBuffering, stdin, BufferMode(..))
+
+import qualified Netcode.IO as Netcode
+
+--------------------------------------------------------------------------------
+
+data Activated a = Activated
+                   { inactive :: [Int]
+                   , active :: [(Int, a)]
+                   } deriving (Eq, Show, Ord)
+
+instance Functor Activated where
+    fmap f (Activated i xs) = Activated i (fmap (second f) xs)
+
+instance Foldable Activated where
+    foldMap f (Activated _ xs) = foldMap (f . snd) xs
+
+instance Traversable Activated where
+    traverse f (Activated i' xs) =
+        Activated i' <$> traverse (\(i, x) -> (i,) <$> f x) xs
+
+mkActivated :: Int -> Activated a
+mkActivated n = Activated (take n [0..]) []
+
+activate :: Int -> a -> Activated a -> Maybe (Activated a)
+activate _ _ (Activated [] _) = Nothing
+activate i x (Activated is xs)
+    | i `notElem` is = Nothing
+    | otherwise      = Just (Activated (delete i is) ((i, x):xs))
+
+deactivate :: Int -> Activated a -> Maybe (Activated a)
+deactivate i a@(Activated is xs)
+    | isActive i a =
+        Just $ Activated (i:is) $ deleteBy ((==) `on` fst) (i, undefined) xs
+    | otherwise = 
+        Nothing
+
+isActive :: Int -> Activated a -> Bool
+isActive i (Activated _ xs) = i `elem` (map fst xs)
+
+--------------------------------------------------------------------------------
+
+newtype RandomInts = RandomInts (IORef [Int])
+
+mkRandomInts :: IO RandomInts
+mkRandomInts = do
+    let randInts = 4 : map ((`mod` 134456) . (+ 28411) . (* 8121)) randInts
+    RandomInts <$> newIORef randInts
+
+getNextInt :: RandomInts -> IO Int
+getNextInt (RandomInts ints) = do
+    (i:is) <- readIORef ints
+    writeIORef ints is
+    return i
+
+filterRand :: RandomInts -> Int -> [a] -> IO [a]
+filterRand ints modVal = (map snd <$>)
+                        . filterM (fmap ((== 0) . (`mod` modVal)) . fst)
+                        . zip (repeat $ getNextInt ints)
+
+whenRandMod :: RandomInts -> Int -> IO () -> IO ()
+whenRandMod ints modVal = whenM ((== 0) . (`mod` modVal) <$> getNextInt ints)
+
+--------------------------------------------------------------------------------
+
+gPrivateKey :: [Word8]
+gPrivateKey =
+    [ 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
+    ]
+
+gProtocolID :: Word64
+gProtocolID = 0x1122334455667788
+
+gPacketData :: [Word8]
+gPacketData = [0..(Netcode.maximumPacketSize - 1)]
+
+gMaxNumClients :: Int
+gMaxNumClients = 1024
+
+gMaxNumServers :: Int
+gMaxNumServers = 64
+
+gServerBasePort :: Int
+gServerBasePort = 40000
+
+gConnectTokenExpiry :: Int
+gConnectTokenExpiry = 30
+
+gConnectTokenTimeout :: Int
+gConnectTokenTimeout = 5
+
+-------------------------------------------------------------------------------
+whenM :: Monad m => m Bool -> m () -> m ()
+whenM c p = do { b <- c; when b p }
+
+untilM :: Monad m => m Bool -> m ()
+untilM p = p >>= bool (untilM p) (return ())
+-------------------------------------------------------------------------------
+
+data Soak = Soak (Activated Netcode.Client) (Activated Netcode.Server)
+
+initializeSoak :: IO Soak
+initializeSoak = do
+    putStrLn "initializing"
+    Netcode.initialize
+    Netcode.logLevel Netcode.LogLevel'Info
+
+    return $ Soak (mkActivated gMaxNumClients) (mkActivated gMaxNumServers)
+
+shutdownSoak :: Soak -> IO ()
+shutdownSoak (Soak clients servers) = do
+    putStrLn "shutdown"
+
+    forM_ (map snd $ active clients) Netcode.destroyClient
+    forM_ (map snd $ active servers) Netcode.destroyServer
+
+    Netcode.terminate
+
+decodePacket :: Maybe Netcode.Packet -> IO Bool
+decodePacket Nothing = return True
+decodePacket (Just pkt) =
+    withForeignPtr (Netcode.packetDataPtr pkt) $ \pktMem -> do
+        pktData <- peekArray (Netcode.packetSize pkt) pktMem
+        case (and $ zipWith (==) pktData gPacketData) of
+            True -> return False
+            False -> fail "Received garbled packet!"
+
+iterateSoak :: RandomInts -> IORef Soak -> Double -> IO ()
+iterateSoak ints sr t = do
+    (Soak clients servers) <- readIORef sr
+    let serverConfig =
+            Netcode.setPrivateKey gPrivateKey $
+            Netcode.setProtocolID gProtocolID $
+            Netcode.defaultServerConfig
+
+        mkServer ss serverID = do
+            let addr = "127.0.0.1:" ++ show (gServerBasePort + serverID)
+            s <- Netcode.createServer addr serverConfig t
+            putStrLn $ "created server " ++ show s
+            return $ fromJust $ activate serverID s ss
+
+    servers' <- filterRand ints 10 (inactive servers)
+            >>= foldM mkServer servers
+
+    let killServer ss (serverID, s) = do
+            putStrLn $ "destroy server " ++ show s
+            Netcode.destroyServer s
+            return $ fromJust $ deactivate serverID ss
+
+    servers'' <- filterM (Netcode.isServerFull . snd) (active servers)
+             >>= filterRand ints 10000
+             >>= foldM killServer servers'
+
+    let mkClient cs clientID = do
+            c <- Netcode.createClient "0.0.0.0" Netcode.defaultClientConfig t
+            putStrLn $ "created client " ++ show c
+            return $ fromJust $ activate clientID c cs
+
+    clients' <- filterRand ints 10 (inactive clients)
+            >>= foldM mkClient clients
+
+    let killClient cs (clientID, c) = do
+            putStrLn $ "destroy client " ++ show c
+            Netcode.destroyClient c
+            return $ fromJust $ deactivate clientID cs
+
+    clients'' <- filterRand ints 1000 (active clients)
+             >>= foldM killClient clients'
+
+    forM_ servers'' $ \s -> do
+        _ <- getNextInt ints
+        whenRandMod ints 10 $
+            whenM (not <$> Netcode.isServerRunning s) $ do
+                numClients <- (`mod` Netcode.maxNumClients) <$> getNextInt ints
+                Netcode.startServer s (1 + numClients)
+
+        maxClients <- Netcode.maxClientsForServer s
+        whenRandMod ints 1000 $
+            whenM ((== maxClients) <$> Netcode.numConnectedClients s) $
+            whenM (Netcode.isServerRunning s) $
+            Netcode.stopServer s
+
+        whenM (Netcode.isServerRunning s) $ do
+            forM_ [0..(maxClients - 1)] $ \clientIndex ->
+                whenM (Netcode.clientConnectedAtIndex s clientIndex) $ do
+                    pktSz <-
+                        ((+ 1) . (`mod` Netcode.maximumPacketSize)) <$>
+                        getNextInt ints
+                    withArrayLen (take pktSz gPacketData) $
+                        Netcode.sendPacketFromServer s clientIndex
+
+            forM_ [0..(maxClients - 1)] $ \clientIndex ->
+                whenM (Netcode.clientConnectedAtIndex s clientIndex) $
+                untilM $ Netcode.receivePacketFromClient s clientIndex
+                     >>= decodePacket
+
+        Netcode.updateServer s t
+
+    forM_ clients'' $ \c -> do
+        _ <- getNextInt ints
+        whenRandMod ints 10 $ whenM (Netcode.isClientDisconnected c) $ do
+            clientID <- Netcode.generateClientID
+            userData <-
+                mapM (fmap fromIntegral)
+                     (replicate Netcode.maximumUserDataSize $ getNextInt ints)
+
+            connectServers <-
+                filterM (Netcode.isServerRunning . snd) (active servers'')
+
+            let maxServers = Netcode.maximumServersPerConnect
+                mkAddrPair i = 
+                    let addr = "127.0.0.1:" <> show (gServerBasePort + i)
+                     in (addr, addr)
+                serverAddrs = take maxServers $
+                              map (mkAddrPair . fst) connectServers
+
+            when (not $ null serverAddrs) $
+                Netcode.connectClient c =<<
+                Netcode.generateConnectToken serverAddrs
+                                             gConnectTokenExpiry 
+                                             gConnectTokenTimeout
+                                             clientID
+                                             gProtocolID
+                                             gPrivateKey
+                                             userData
+
+        let isConnected = (== Netcode.ClientState'Connected)
+        whenRandMod ints 100 $
+            whenM (isConnected <$> Netcode.getClientState c) $
+            Netcode.disconnectClient c
+
+        whenM (isConnected <$> Netcode.getClientState c) $ do
+            pktSz <-
+                ((+ 1) . (`mod` Netcode.maximumPacketSize)) <$>
+                getNextInt ints
+            withArrayLen (take pktSz gPacketData)
+                         (Netcode.sendPacketFromClient c)
+
+            untilM $ Netcode.receivePacketFromServer c >>= decodePacket
+
+        Netcode.updateClient c t
+
+    writeIORef sr $ Soak clients'' servers''
+
+main :: IO ()
+main = do
+    hSetBuffering stdin LineBuffering
+    args <- getArgs
+    let numIters :: Maybe Int
+        numIters = case length args of
+            1 -> Just $ read (head args)
+            _ -> Nothing
+
+        times = case numIters of
+            Just x -> take x [0.0,0.1..]
+            Nothing -> [0.0,0.1..]
+
+    putStrLn "[soak]"
+    putStrLn $ "num_iterations = " <> show numIters
+
+    soak <- initializeSoak
+    soakRef <- newIORef soak
+
+    ints <- mkRandomInts
+
+    soakResult <- try $ forM_ times $ iterateSoak ints soakRef
+
+    case soakResult of
+        (Left UserInterrupt) -> return ()
+        (Left e) -> fail (show e)
+        (Right ()) -> return ()
+
+    readIORef soakRef >>= shutdownSoak
diff --git a/netcode-io.cabal b/netcode-io.cabal
new file mode 100644
--- /dev/null
+++ b/netcode-io.cabal
@@ -0,0 +1,124 @@
+cabal-version:       >= 1.10
+name:                netcode-io
+version:             0.0.1
+synopsis:            Bindings to the low-level netcode.io library.
+description:         Bindings to the low-level netcode.io library, which come
+                     in two flavors: c-level bindings and a high-level
+                     interface to the library.
+
+                     For the low level interface, refer to the
+                     Bindings.Netcode.IO module.
+
+                     For the high-level interface, which is a bit closer to
+                     idiomatic Haskell, refer to the Netcode.IO module.
+homepage:            http://www.github.com/Mokosha/netcode-io
+bug-reports:         http://www.github.com/Mokosha/netcode-io 
+license:             BSD3
+license-file:        LICENSE
+author:              Pavel Krajcevski
+maintainer:          krajcevski@gmail.com
+copyright:           Pavel Krajcevski, 2020
+category:            Network
+build-type:          Simple
+extra-source-files:  README.md
+                  ,  netcode.io/netcode.h
+                  ,  netcode.io/netcode.c
+
+source-repository head
+  type:      git
+  location:  https://www.github.com/Mokosha/netcode-io.git
+
+library
+  ghc-options:         -Wall -fPIC
+  include-dirs:        netcode.io
+  c-sources:           netcode.io/netcode.c
+  cc-options:          -std=c99 -DNDEBUG -DNETCODE_ENABLE_TESTS=1
+  extra-libraries:     sodium
+
+  if os(mingw32)
+    -- To make life easy on windows, we encourage using static libraries where
+    -- possible. That way we don't need to go around copying .dll files.
+    ld-options:        -static
+    extra-libraries:   ws2_32
+
+  exposed-modules:     Bindings.Netcode.IO
+                     , Netcode.IO
+  other-modules:       Netcode.IO.Server
+                     , Netcode.IO.Client
+                     , Netcode.IO.Address
+                     , Netcode.IO.Packet
+                     , Netcode.IO.Callbacks
+  -- other-extensions:
+  build-tools:         hsc2hs
+  build-depends:       base >= 4.12 && < 5
+                     , bindings-DSL
+  default-language:    Haskell2010
+
+flag examples
+  description: If true, build the examples
+  default:     False
+
+executable netcode-io-c-unit-tests
+  default-language:    Haskell2010
+  main-is:             RunCUnitTests.hs
+  hs-source-dirs:      examples
+  ghc-options:         -Wall -static -fPIC
+  build-depends:       base > 4
+                     , netcode-io
+
+  if flag(examples)
+    buildable:         True
+  else
+    buildable:         False
+
+executable netcode-io-client
+  default-language:    Haskell2010
+  main-is:             Client.hs
+  hs-source-dirs:      examples
+  ghc-options:         -Wall -static -fPIC
+  build-depends:       base > 4
+                     , netcode-io
+
+  if flag(examples)
+    buildable:         True
+  else
+    buildable:         False
+
+executable netcode-io-server
+  default-language:    Haskell2010
+  main-is:             Server.hs
+  hs-source-dirs:      examples
+  ghc-options:         -Wall -static -fPIC
+  build-depends:       base > 4
+                     , netcode-io
+
+  if flag(examples)
+    buildable:         True
+  else
+    buildable:         False
+
+executable netcode-io-clientserver
+  default-language:    Haskell2010
+  main-is:             ClientServer.hs
+  hs-source-dirs:      examples
+  ghc-options:         -Wall -static -fPIC
+  build-depends:       base > 4
+                     , netcode-io
+
+  if flag(examples)
+    buildable:         True
+  else
+    buildable:         False
+
+executable netcode-io-soak
+  default-language:    Haskell2010
+  main-is:             Soak.hs
+  hs-source-dirs:      examples
+  ghc-options:         -Wall -static -fPIC
+  build-depends:       base > 4
+                     , netcode-io
+
+  if flag(examples)
+    buildable:         True
+  else
+    buildable:         False
diff --git a/netcode.io/netcode.c b/netcode.io/netcode.c
new file mode 100644
--- /dev/null
+++ b/netcode.io/netcode.c
@@ -0,0 +1,8971 @@
+/*
+    netcode.io reference implementation
+
+    Copyright © 2017 - 2019, The Network Protocol Company, Inc.
+
+    Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+        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 
+           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 
+           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, 
+    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.
+*/
+
+#include "netcode.h"
+#include <stdlib.h>
+#include <memory.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <inttypes.h>
+#include <math.h>
+#include <time.h>
+
+#ifdef _MSC_VER
+#define SODIUM_STATIC
+#pragma warning(disable:4996)
+#endif // #ifdef _MSC_VER
+
+#include <sodium.h>
+
+#define NETCODE_SOCKET_IPV6         1
+#define NETCODE_SOCKET_IPV4         2
+
+#define NETCODE_CONNECT_TOKEN_NONCE_BYTES 24
+#define NETCODE_CONNECT_TOKEN_PRIVATE_BYTES 1024
+#define NETCODE_CHALLENGE_TOKEN_BYTES 300
+#define NETCODE_VERSION_INFO_BYTES 13
+#define NETCODE_MAX_PACKET_BYTES 1300
+#define NETCODE_MAX_PAYLOAD_BYTES 1200
+#define NETCODE_MAX_ADDRESS_STRING_LENGTH 256
+#define NETCODE_PACKET_QUEUE_SIZE 256
+#define NETCODE_REPLAY_PROTECTION_BUFFER_SIZE 256
+#define NETCODE_CLIENT_MAX_RECEIVE_PACKETS 64
+#define NETCODE_SERVER_MAX_RECEIVE_PACKETS ( 64 * NETCODE_MAX_CLIENTS )
+#define NETCODE_CLIENT_SOCKET_SNDBUF_SIZE ( 256 * 1024 )
+#define NETCODE_CLIENT_SOCKET_RCVBUF_SIZE ( 256 * 1024 )
+#define NETCODE_SERVER_SOCKET_SNDBUF_SIZE ( 4 * 1024 * 1024 )
+#define NETCODE_SERVER_SOCKET_RCVBUF_SIZE ( 4 * 1024 * 1024 )
+
+#define NETCODE_VERSION_INFO ( (uint8_t*) "NETCODE 1.02" )
+#define NETCODE_PACKET_SEND_RATE 10.0
+#define NETCODE_NUM_DISCONNECT_PACKETS 10
+
+#ifndef NETCODE_ENABLE_TESTS
+#define NETCODE_ENABLE_TESTS 0
+#endif // #ifndef NETCODE_ENABLE_TESTS
+
+#ifndef NETCODE_ENABLE_LOGGING
+#define NETCODE_ENABLE_LOGGING 1
+#endif // #ifndef NETCODE_ENABLE_LOGGING
+
+// ------------------------------------------------------------------
+
+static void netcode_default_assert_handler( NETCODE_CONST char * condition, NETCODE_CONST char * function, NETCODE_CONST char * file, int line )
+{
+    printf( "assert failed: ( %s ), function %s, file %s, line %d\n", condition, function, file, line );
+    #if defined( __GNUC__ )
+    __builtin_trap();
+    #elif defined( _MSC_VER )
+    __debugbreak();
+    #endif
+    exit( 1 );
+}
+
+static int log_level = 0;
+static int (*printf_function)( NETCODE_CONST char *, ... ) = ( int (*)( NETCODE_CONST char *, ... ) ) printf;
+void (*netcode_assert_function)( NETCODE_CONST char *, NETCODE_CONST char *, NETCODE_CONST char * file, int line ) = netcode_default_assert_handler;
+
+void netcode_log_level( int level )
+{
+    log_level = level;
+}
+
+void netcode_set_printf_function( int (*function)( NETCODE_CONST char *, ... ) )
+{
+    netcode_assert( function );
+    printf_function = function;
+}
+
+void netcode_set_assert_function( void (*function)( NETCODE_CONST char *, NETCODE_CONST char *, NETCODE_CONST char * file, int line ) )
+{
+    netcode_assert_function = function;
+}
+
+#if NETCODE_ENABLE_LOGGING
+
+void netcode_printf( int level, NETCODE_CONST char * format, ... ) 
+{
+    if ( level > log_level )
+        return;
+    va_list args;
+    va_start( args, format );
+    char buffer[4*1024];
+    vsprintf( buffer, format, args );
+    printf_function( "%s", buffer );
+    va_end( args );
+}
+
+#else // #if NETCODE_ENABLE_LOGGING
+
+void netcode_printf( int level, NETCODE_CONST char * format, ... ) 
+{
+    (void) level;
+    (void) format;
+}
+
+#endif // #if NETCODE_ENABLE_LOGGING
+
+void * netcode_default_allocate_function( void * context, uint64_t bytes )
+{
+    (void) context;
+    return malloc( bytes );
+}
+
+void netcode_default_free_function( void * context, void * pointer )
+{
+    (void) context;
+    free( pointer );
+}
+
+// ------------------------------------------------------------------
+
+#if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+
+    #define NOMINMAX
+    #define _WINSOCK_DEPRECATED_NO_WARNINGS
+    #include <winsock2.h>
+    #include <ws2tcpip.h>
+    #include <ws2ipdef.h>
+    #include <iphlpapi.h>
+    #pragma comment( lib, "WS2_32.lib" )
+    #pragma comment( lib, "IPHLPAPI.lib" )
+
+    #ifdef SetPort
+    #undef SetPort
+    #endif // #ifdef SetPort
+
+    #include <iphlpapi.h>
+    #pragma comment( lib, "IPHLPAPI.lib" )
+    
+#elif NETCODE_PLATFORM == NETCODE_PLATFORM_MAC || NETCODE_PLATFORM == NETCODE_PLATFORM_UNIX
+
+    #include <netdb.h>
+    #include <sys/types.h>
+    #include <sys/socket.h>
+    #include <netinet/in.h>
+    #include <ifaddrs.h>
+    #include <net/if.h>
+    #include <fcntl.h>
+    #include <netdb.h>
+    #include <arpa/inet.h>
+    #include <unistd.h>
+    #include <errno.h>
+
+#else
+
+    #error netcode.io - unknown platform!
+
+#endif
+
+// ----------------------------------------------------------------
+
+#ifdef __MINGW32__
+    const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt)
+    {
+        if ( af == AF_INET )
+        {
+            struct sockaddr_in in;
+            memset(&in, 0, sizeof(in));
+            in.sin_family = AF_INET;
+            memcpy(&in.sin_addr, src, sizeof(struct in_addr));
+            getnameinfo((struct sockaddr *)&in, sizeof(struct sockaddr_in), dst, cnt, NULL, 0, NI_NUMERICHOST);
+            return dst;
+        } 
+        else if ( af == AF_INET6 )
+        {
+            struct sockaddr_in6 in;
+            memset(&in, 0, sizeof(in));
+            in.sin6_family = AF_INET6;
+            memcpy(&in.sin6_addr, src, sizeof(struct in_addr6));
+            getnameinfo((struct sockaddr *)&in, sizeof(struct sockaddr_in6), dst, cnt, NULL, 0, NI_NUMERICHOST);
+            return dst;
+        }
+
+        return NULL;
+    }
+
+    #define NS_INADDRSZ  4
+    #define NS_IN6ADDRSZ 16
+    #define NS_INT16SZ   2
+
+    int inet_pton4(const char *src, char *dst)
+    {
+        uint8_t tmp[NS_INADDRSZ], *tp;
+        int saw_digit = 0;
+        int octets = 0;
+        *(tp = tmp) = 0;
+        int ch;
+
+        while ((ch = *src++) != '\0')
+        {
+            if ( ch >= '0' && ch <= '9' )
+            {
+                uint32_t n = *tp * 10 + (ch - '0');
+
+                if ( saw_digit && *tp == 0 )
+                    return 0;
+                if ( n > 255 )
+                    return 0;
+                *tp = n;
+                if ( !saw_digit )
+                {
+                    if ( ++octets > 4 )
+                        return 0;
+                    saw_digit = 1;
+                }
+            }
+            else if ( ch == '.' && saw_digit )
+            {
+                if ( octets == 4 )
+                    return 0;
+                *++tp = 0;
+                saw_digit = 0;
+            }
+            else
+                return 0;
+        }
+        if ( octets < 4 )
+            return 0;
+
+        memcpy(dst, tmp, NS_INADDRSZ);
+
+        return 1;
+    }
+
+    int inet_pton6(const char *src, char *dst)
+    {
+        static const char xdigits[] = "0123456789abcdef";
+        uint8_t tmp[NS_IN6ADDRSZ];
+        uint8_t *tp = (uint8_t*) memset(tmp, '\0', NS_IN6ADDRSZ);
+        uint8_t *endp = tp + NS_IN6ADDRSZ;
+        uint8_t *colonp = NULL;
+
+        // Leading :: requires some special handling
+        if ( *src == ':' )
+        {
+            if ( *++src != ':' )
+                return 0;
+        }
+
+        const char *curtok = src;
+        int saw_xdigit = 0;
+        uint32_t val = 0;
+        int ch;
+
+        while ( (ch = tolower(*src++)) != '\0' )
+        {
+            const char *pch = strchr(xdigits, ch);
+            if ( pch != NULL )
+            {
+                val <<= 4;
+                val |= (pch - xdigits);
+                if ( val > 0xffff )
+                    return 0;
+                saw_xdigit = 1;
+                continue;
+            }
+            if ( ch == ':' )
+            {
+                curtok = src;
+                if ( !saw_xdigit )
+                {
+                    if ( colonp )
+                        return 0;
+                    colonp = tp;
+                    continue;
+                }
+                else if ( *src == '\0' )
+                {
+                    return 0;
+                }
+                if ( tp + NS_INT16SZ > endp )
+                    return 0;
+                *tp++ = (uint8_t) (val >> 8) & 0xff;
+                *tp++ = (uint8_t) val & 0xff;
+                saw_xdigit = 0;
+                val = 0;
+                continue;
+            }
+            if ( ch == '.' && ((tp + NS_INADDRSZ) <= endp) && inet_pton4(curtok, (char*) tp) > 0 )
+            {
+                tp += NS_INADDRSZ;
+                saw_xdigit = 0;
+                break;
+            }
+            return 0;
+        }
+
+        if ( saw_xdigit )
+        {
+            if ( tp + NS_INT16SZ > endp )
+                return 0;
+            *tp++ = (uint8_t) (val >> 8) & 0xff;
+            *tp++ = (uint8_t) val & 0xff;
+        }
+
+        if ( colonp != NULL )
+        {
+            const int n = tp - colonp;
+            if ( tp == endp )
+                return 0;
+            for (int i = 1; i <= n; i++)
+            {
+                endp[-i] = colonp[n - i];
+                colonp[n - i] = 0;
+            }
+            tp = endp;
+        }
+        if ( tp != endp )
+            return 0;
+
+        memcpy(dst, tmp, NS_IN6ADDRSZ);
+
+        return 1;
+    }
+
+    int inet_pton(int af, const char *src, void *dst)
+    {
+        switch ( af )
+        {
+            case AF_INET:
+                return inet_pton4(src, (char *) dst);
+
+            case AF_INET6:
+                return inet_pton6(src, (char *) dst);
+
+            default:
+                return -1;
+        }
+    }
+#endif
+
+// ----------------------------------------------------------------
+
+int netcode_parse_address( NETCODE_CONST char * address_string_in, struct netcode_address_t * address )
+{
+    netcode_assert( address_string_in );
+    netcode_assert( address );
+
+    memset( address, 0, sizeof( struct netcode_address_t ) );
+
+    // first try to parse the string as an IPv6 address:
+    // 1. if the first character is '[' then it's probably an ipv6 in form "[addr6]:portnum"
+    // 2. otherwise try to parse as a raw IPv6 address using inet_pton
+
+    #define NETCODE_ADDRESS_BUFFER_SAFETY 32
+
+    char buffer[NETCODE_MAX_ADDRESS_STRING_LENGTH + NETCODE_ADDRESS_BUFFER_SAFETY*2];
+
+    char * address_string = buffer + NETCODE_ADDRESS_BUFFER_SAFETY;
+    strncpy( address_string, address_string_in, NETCODE_MAX_ADDRESS_STRING_LENGTH - 1 );
+    address_string[NETCODE_MAX_ADDRESS_STRING_LENGTH-1] = '\0';
+
+    int address_string_length = (int) strlen( address_string );
+
+    if ( address_string[0] == '[' )
+    {
+        int base_index = address_string_length - 1;
+        
+        int i;
+        for ( i = 0; i < 6; ++i )         // note: no need to search past 6 characters as ":65535" is longest possible port value
+        {
+            int index = base_index - i;
+            if ( index < 3 )
+                return NETCODE_ERROR;
+            if ( address_string[index] == ':' )
+            {
+                address->port = (uint16_t) ( atoi( &address_string[index + 1] ) );
+                address_string[index-1] = '\0';
+            }
+        }
+        address_string += 1;
+    }
+
+    struct in6_addr sockaddr6;
+    if ( inet_pton( AF_INET6, address_string, &sockaddr6 ) == 1 )
+    {
+        address->type = NETCODE_ADDRESS_IPV6;
+        int i;
+        for ( i = 0; i < 8; ++i )
+        {
+            address->data.ipv6[i] = ntohs( ( (uint16_t*) &sockaddr6 ) [i] );
+        }
+        return NETCODE_OK;
+    }
+
+    // otherwise it's probably an IPv4 address:
+    // 1. look for ":portnum", if found save the portnum and strip it out
+    // 2. parse remaining ipv4 address via inet_pton
+
+    address_string_length = (int) strlen( address_string );
+    int base_index = address_string_length - 1;
+    int i;
+    for ( i = 0; i < 6; ++i )
+    {
+        int index = base_index - i;
+        if ( index < 0 )
+            break;
+        if ( address_string[index] == ':' )
+        {
+            address->port = (uint16_t) atoi( &address_string[index+1] );
+            address_string[index] = '\0';
+        }
+    }
+
+    struct sockaddr_in sockaddr4;
+    if ( inet_pton( AF_INET, address_string, &sockaddr4.sin_addr ) == 1 )
+    {
+        address->type = NETCODE_ADDRESS_IPV4;
+        address->data.ipv4[3] = (uint8_t) ( ( sockaddr4.sin_addr.s_addr & 0xFF000000 ) >> 24 );
+        address->data.ipv4[2] = (uint8_t) ( ( sockaddr4.sin_addr.s_addr & 0x00FF0000 ) >> 16 );
+        address->data.ipv4[1] = (uint8_t) ( ( sockaddr4.sin_addr.s_addr & 0x0000FF00 ) >> 8  );
+        address->data.ipv4[0] = (uint8_t) ( ( sockaddr4.sin_addr.s_addr & 0x000000FF )       );
+        return NETCODE_OK;
+    }
+
+    return NETCODE_ERROR;
+}
+
+char * netcode_address_to_string( struct netcode_address_t * address, char * buffer )
+{
+    netcode_assert( address );
+    netcode_assert( buffer );
+
+    if ( address->type == NETCODE_ADDRESS_IPV6 )
+    {
+        if ( address->port == 0 )
+        {
+            uint16_t ipv6_network_order[8];
+            int i;
+            for ( i = 0; i < 8; ++i )
+                ipv6_network_order[i] = htons( address->data.ipv6[i] );
+            inet_ntop( AF_INET6, (void*) ipv6_network_order, buffer, NETCODE_MAX_ADDRESS_STRING_LENGTH );
+            return buffer;
+        }
+        else
+        {
+            char address_string[INET6_ADDRSTRLEN];
+            uint16_t ipv6_network_order[8];
+            int i;
+            for ( i = 0; i < 8; ++i )
+                ipv6_network_order[i] = htons( address->data.ipv6[i] );
+            inet_ntop( AF_INET6, (void*) ipv6_network_order, address_string, INET6_ADDRSTRLEN );
+            snprintf( buffer, NETCODE_MAX_ADDRESS_STRING_LENGTH, "[%s]:%d", address_string, address->port );
+            return buffer;
+        }
+    }
+    else if ( address->type == NETCODE_ADDRESS_IPV4 )
+    {
+        if ( address->port != 0 )
+        {
+            snprintf( buffer, NETCODE_MAX_ADDRESS_STRING_LENGTH, "%d.%d.%d.%d:%d", 
+                address->data.ipv4[0], 
+                address->data.ipv4[1], 
+                address->data.ipv4[2], 
+                address->data.ipv4[3], 
+                address->port );
+        }
+        else
+        {
+            snprintf( buffer, NETCODE_MAX_ADDRESS_STRING_LENGTH, "%d.%d.%d.%d", 
+                address->data.ipv4[0], 
+                address->data.ipv4[1], 
+                address->data.ipv4[2], 
+                address->data.ipv4[3] );
+        }
+        return buffer;
+    }
+    else
+    {
+        snprintf( buffer, NETCODE_MAX_ADDRESS_STRING_LENGTH, "%s", "NONE" );
+        return buffer;
+    }
+}
+
+int netcode_address_is_any_address(struct netcode_address_t *a) {
+    if (a->type == NETCODE_ADDRESS_IPV4) {
+        // 0.0.0.0
+        if (a->data.ipv4[0] == 0 &&
+            a->data.ipv4[1] == 0 &&
+            a->data.ipv4[2] == 0 &&
+            a->data.ipv4[3] == 0)
+            return 1;
+
+    } else if ( a->type == NETCODE_ADDRESS_IPV6 ) {
+        // [::] aka [0:0:0:0:0:0:0:0]
+        if (a->data.ipv6[0] == 0 &&
+            a->data.ipv6[1] == 0 &&
+            a->data.ipv6[2] == 0 &&
+            a->data.ipv6[3] == 0 &&
+            a->data.ipv6[4] == 0 &&
+            a->data.ipv6[5] == 0 &&
+            a->data.ipv6[6] == 0 &&
+            a->data.ipv6[7] == 0)
+            return 1;
+    }
+
+    return 0;
+}
+
+int netcode_address_equal( struct netcode_address_t * a, struct netcode_address_t * b )
+{
+    netcode_assert( a );
+    netcode_assert( b );
+
+    if ( a->type != b->type )
+        return 0;
+
+    if ( a->port != b->port )
+        return 0;
+
+    // At this point, ports match. If either address is 0.0.0.0 or [::], then mark them as matching. These addresses are never routed, they're only used when listening.
+    if (netcode_address_is_any_address(a) || netcode_address_is_any_address(b))
+        return 1;
+
+    if ( a->type == NETCODE_ADDRESS_IPV4 )
+    {
+        int i;
+        for ( i = 0; i < 4; ++i )
+        {
+            if ( a->data.ipv4[i] != b->data.ipv4[i] )
+                return 0;
+        }
+    }
+    else if ( a->type == NETCODE_ADDRESS_IPV6 )
+    {
+        int i;
+        for ( i = 0; i < 8; ++i )
+        {
+            if ( a->data.ipv6[i] != b->data.ipv6[i] )
+                return 0;
+        }
+    }
+    else
+    {
+        return 0;
+    }
+
+    return 1;
+}
+
+// ----------------------------------------------------------------
+
+struct netcode_t
+{
+    int initialized;
+};
+
+static struct netcode_t netcode;
+
+int netcode_init()
+{
+    netcode_assert( !netcode.initialized );
+
+#if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+    WSADATA WsaData;         
+    if ( WSAStartup( MAKEWORD(2,2), &WsaData ) != NO_ERROR )
+        return NETCODE_ERROR;
+#endif // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+
+    if ( sodium_init() == -1 )
+        return NETCODE_ERROR;
+
+    netcode.initialized = 1;
+
+    return NETCODE_OK;
+}
+
+void netcode_term()
+{
+    netcode_assert( netcode.initialized );
+
+#if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+    WSACleanup();
+#endif // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+
+    netcode.initialized = 0;
+}
+
+// ----------------------------------------------------------------
+
+#if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+typedef uint64_t netcode_socket_handle_t;
+#else // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+typedef int netcode_socket_handle_t;
+#endif // #if NETCODE_PLATFORM == NETCODe_PLATFORM_WINDOWS
+
+struct netcode_socket_t
+{
+    struct netcode_address_t address;
+    netcode_socket_handle_t handle;
+};
+
+
+struct netcode_socket_holder_t
+{
+    struct netcode_socket_t ipv4;
+    struct netcode_socket_t ipv6;
+};
+
+#define NETCODE_SOCKET_ERROR_NONE                               0
+#define NETCODE_SOCKET_ERROR_CREATE_FAILED                      1
+#define NETCODE_SOCKET_ERROR_SET_NON_BLOCKING_FAILED            2
+#define NETCODE_SOCKET_ERROR_SOCKOPT_IPV6_ONLY_FAILED           3
+#define NETCODE_SOCKET_ERROR_SOCKOPT_RCVBUF_FAILED              4
+#define NETCODE_SOCKET_ERROR_SOCKOPT_SNDBUF_FAILED              5
+#define NETCODE_SOCKET_ERROR_BIND_IPV4_FAILED                   6
+#define NETCODE_SOCKET_ERROR_BIND_IPV6_FAILED                   7
+#define NETCODE_SOCKET_ERROR_GET_SOCKNAME_IPV4_FAILED           8
+#define NETCODE_SOCKET_ERROR_GET_SOCKNAME_IPV6_FAILED           7
+
+void netcode_socket_destroy( struct netcode_socket_t * socket )
+{
+    netcode_assert( socket );
+    netcode_assert( netcode.initialized );
+
+    if ( socket->handle != 0 )
+    {
+        #if NETCODE_PLATFORM == NETCODE_PLATFORM_MAC || NETCODE_PLATFORM == NETCODE_PLATFORM_UNIX
+        close( socket->handle );
+        #elif NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+        closesocket( socket->handle );
+        #else
+        #error unsupported platform
+        #endif
+        socket->handle = 0;
+    }
+}
+
+int netcode_socket_create( struct netcode_socket_t * s, struct netcode_address_t * address, int send_buffer_size, int receive_buffer_size )
+{
+    netcode_assert( s );
+    netcode_assert( address );
+    netcode_assert( netcode.initialized );
+
+    netcode_assert( address->type != NETCODE_ADDRESS_NONE );
+
+    s->address = *address;
+
+    // create socket
+
+    s->handle = socket( ( address->type == NETCODE_ADDRESS_IPV6 ) ? AF_INET6 : AF_INET, SOCK_DGRAM, IPPROTO_UDP );
+
+#if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+    if ( s->handle == INVALID_SOCKET )
+#else // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+    if ( s->handle <= 0 )
+#endif // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to create socket\n" );
+        return NETCODE_SOCKET_ERROR_CREATE_FAILED;
+    }
+
+    // force IPv6 only if necessary
+
+    if ( address->type == NETCODE_ADDRESS_IPV6 )
+    {
+        int yes = 1;
+        if ( setsockopt( s->handle, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&yes, sizeof(yes) ) != 0 )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to set socket ipv6 only\n" );
+            netcode_socket_destroy( s );
+            return NETCODE_SOCKET_ERROR_SOCKOPT_IPV6_ONLY_FAILED;
+        }
+    }
+
+    // increase socket send and receive buffer sizes
+
+    if ( setsockopt( s->handle, SOL_SOCKET, SO_SNDBUF, (char*)&send_buffer_size, sizeof(int) ) != 0 )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to set socket send buffer size\n" );
+        netcode_socket_destroy( s );
+        return NETCODE_SOCKET_ERROR_SOCKOPT_SNDBUF_FAILED;
+    }
+
+    if ( setsockopt( s->handle, SOL_SOCKET, SO_RCVBUF, (char*)&receive_buffer_size, sizeof(int) ) != 0 )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to set socket receive buffer size\n" );
+        netcode_socket_destroy( s );
+        return NETCODE_SOCKET_ERROR_SOCKOPT_RCVBUF_FAILED;
+    }
+
+    // bind to port
+
+    if ( address->type == NETCODE_ADDRESS_IPV6 )
+    {
+        struct sockaddr_in6 socket_address;
+        memset( &socket_address, 0, sizeof( struct sockaddr_in6 ) );
+        socket_address.sin6_family = AF_INET6;
+        int i;
+        for ( i = 0; i < 8; ++i )
+        {
+            ( (uint16_t*) &socket_address.sin6_addr ) [i] = htons( address->data.ipv6[i] );
+        }
+        socket_address.sin6_port = htons( address->port );
+
+        if ( bind( s->handle, (struct sockaddr*) &socket_address, sizeof( socket_address ) ) < 0 )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to bind socket (ipv6)\n" );
+            netcode_socket_destroy( s );
+            return NETCODE_SOCKET_ERROR_BIND_IPV6_FAILED;
+        }
+    }
+    else
+    {
+        struct sockaddr_in socket_address;
+        memset( &socket_address, 0, sizeof( socket_address ) );
+        socket_address.sin_family = AF_INET;
+        socket_address.sin_addr.s_addr = ( ( (uint32_t) address->data.ipv4[0] ) )       | 
+                                         ( ( (uint32_t) address->data.ipv4[1] ) << 8 )  | 
+                                         ( ( (uint32_t) address->data.ipv4[2] ) << 16 ) | 
+                                         ( ( (uint32_t) address->data.ipv4[3] ) << 24 );
+        socket_address.sin_port = htons( address->port );
+
+        if ( bind( s->handle, (struct sockaddr*) &socket_address, sizeof( socket_address ) ) < 0 )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to bind socket (ipv4)\n" );
+            netcode_socket_destroy( s );
+            return NETCODE_SOCKET_ERROR_BIND_IPV4_FAILED;
+        }
+    }
+
+    // if bound to port 0 find the actual port we got
+
+    if ( address->port == 0 )
+    {
+        if ( address->type == NETCODE_ADDRESS_IPV6 )
+        {
+            struct sockaddr_in6 sin;
+            socklen_t len = sizeof( sin );
+            if ( getsockname( s->handle, (struct sockaddr*)&sin, &len ) == -1 )
+            {
+                netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to get socket port (ipv6)\n" );
+                netcode_socket_destroy( s );
+                return NETCODE_SOCKET_ERROR_GET_SOCKNAME_IPV6_FAILED;
+            }
+            s->address.port = ntohs( sin.sin6_port );
+        }
+        else
+        {
+            struct sockaddr_in sin;
+            socklen_t len = sizeof( sin );
+            if ( getsockname( s->handle, (struct sockaddr*)&sin, &len ) == -1 )
+            {
+                netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to get socket port (ipv4)\n" );
+                netcode_socket_destroy( s );
+                return NETCODE_SOCKET_ERROR_GET_SOCKNAME_IPV4_FAILED;
+            }
+            s->address.port = ntohs( sin.sin_port );
+        }
+    }
+
+    // set non-blocking io
+
+#if NETCODE_PLATFORM == NETCODE_PLATFORM_MAC || NETCODE_PLATFORM == NETCODE_PLATFORM_UNIX
+
+    int non_blocking = 1;
+    if ( fcntl( s->handle, F_SETFL, O_NONBLOCK, non_blocking ) == -1 )
+    {
+        netcode_socket_destroy( s );
+        return NETCODE_SOCKET_ERROR_SET_NON_BLOCKING_FAILED;
+    }
+
+#elif NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+
+    DWORD nonBlocking = 1;
+    if ( ioctlsocket( s->handle, FIONBIO, &nonBlocking ) != 0 )
+    {
+        netcode_socket_destroy( s );
+        return NETCODE_SOCKET_ERROR_SET_NON_BLOCKING_FAILED;
+    }
+
+#else
+
+    #error unsupported platform
+
+#endif
+
+    return NETCODE_SOCKET_ERROR_NONE;
+}
+
+void netcode_socket_send_packet( struct netcode_socket_t * socket, struct netcode_address_t * to, void * packet_data, int packet_bytes )
+{
+    netcode_assert( socket );
+    netcode_assert( socket->handle != 0 );
+    netcode_assert( to );
+    netcode_assert( to->type == NETCODE_ADDRESS_IPV6 || to->type == NETCODE_ADDRESS_IPV4 );
+    netcode_assert( packet_data );
+    netcode_assert( packet_bytes > 0 );
+
+    if ( to->type == NETCODE_ADDRESS_IPV6 )
+    {
+        struct sockaddr_in6 socket_address;
+        memset( &socket_address, 0, sizeof( socket_address ) );
+        socket_address.sin6_family = AF_INET6;
+        int i;
+        for ( i = 0; i < 8; ++i )
+        {
+            ( (uint16_t*) &socket_address.sin6_addr ) [i] = htons( to->data.ipv6[i] );
+        }
+        socket_address.sin6_port = htons( to->port );
+        int result = sendto( socket->handle, (char*) packet_data, packet_bytes, 0, (struct sockaddr*) &socket_address, sizeof( struct sockaddr_in6 ) );
+        (void) result;
+    }
+    else if ( to->type == NETCODE_ADDRESS_IPV4 )
+    {
+        struct sockaddr_in socket_address;
+        memset( &socket_address, 0, sizeof( socket_address ) );
+        socket_address.sin_family = AF_INET;
+        socket_address.sin_addr.s_addr = ( ( (uint32_t) to->data.ipv4[0] ) )        | 
+                                         ( ( (uint32_t) to->data.ipv4[1] ) << 8 )   | 
+                                         ( ( (uint32_t) to->data.ipv4[2] ) << 16 )  | 
+                                         ( ( (uint32_t) to->data.ipv4[3] ) << 24 );
+        socket_address.sin_port = htons( to->port );
+        int result = sendto( socket->handle, (NETCODE_CONST char*) packet_data, packet_bytes, 0, (struct sockaddr*) &socket_address, sizeof( struct sockaddr_in ) );
+        (void) result;
+    }
+}
+
+int netcode_socket_receive_packet( struct netcode_socket_t * socket, struct netcode_address_t * from, void * packet_data, int max_packet_size )
+{
+    netcode_assert( socket );
+    netcode_assert( socket->handle != 0 );
+    netcode_assert( from );
+    netcode_assert( packet_data );
+    netcode_assert( max_packet_size > 0 );
+
+#if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+    typedef int socklen_t;
+#endif // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+    
+    struct sockaddr_storage sockaddr_from;
+    socklen_t from_length = sizeof( sockaddr_from );
+
+    int result = recvfrom( socket->handle, (char*) packet_data, max_packet_size, 0, (struct sockaddr*) &sockaddr_from, &from_length );
+
+#if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+    if ( result == SOCKET_ERROR )
+    {
+        int error = WSAGetLastError();
+
+        if ( error == WSAEWOULDBLOCK || error == WSAECONNRESET )
+            return 0;
+
+        netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: recvfrom failed with error %d\n", error );
+
+        return 0;
+    }
+#else // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+    if ( result <= 0 )
+    {
+        if ( errno == EAGAIN )
+            return 0;
+
+        netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: recvfrom failed with error %d\n", errno );
+
+        return 0;
+    }
+#endif // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
+
+    if ( sockaddr_from.ss_family == AF_INET6 )
+    {
+        struct sockaddr_in6 * addr_ipv6 = (struct sockaddr_in6*) &sockaddr_from;
+        from->type = NETCODE_ADDRESS_IPV6;
+        int i;
+        for ( i = 0; i < 8; ++i )
+        {
+            from->data.ipv6[i] = ntohs( ( (uint16_t*) &addr_ipv6->sin6_addr ) [i] );
+        }
+        from->port = ntohs( addr_ipv6->sin6_port );
+    }
+    else if ( sockaddr_from.ss_family == AF_INET )
+    {
+        struct sockaddr_in * addr_ipv4 = (struct sockaddr_in*) &sockaddr_from;
+        from->type = NETCODE_ADDRESS_IPV4;
+        from->data.ipv4[0] = (uint8_t) ( ( addr_ipv4->sin_addr.s_addr & 0x000000FF ) );
+        from->data.ipv4[1] = (uint8_t) ( ( addr_ipv4->sin_addr.s_addr & 0x0000FF00 ) >> 8 );
+        from->data.ipv4[2] = (uint8_t) ( ( addr_ipv4->sin_addr.s_addr & 0x00FF0000 ) >> 16 );
+        from->data.ipv4[3] = (uint8_t) ( ( addr_ipv4->sin_addr.s_addr & 0xFF000000 ) >> 24 );
+        from->port = ntohs( addr_ipv4->sin_port );
+    }
+    else
+    {
+        netcode_assert( 0 );
+        return 0;
+    }
+  
+    netcode_assert( result >= 0 );
+
+    int bytes_read = result;
+
+    return bytes_read;
+}
+
+// ----------------------------------------------------------------
+
+void netcode_write_uint8( uint8_t ** p, uint8_t value )
+{
+    **p = value;
+    ++(*p);
+}
+
+void netcode_write_uint16( uint8_t ** p, uint16_t value )
+{
+    (*p)[0] = value & 0xFF;
+    (*p)[1] = value >> 8;
+    *p += 2;
+}
+
+void netcode_write_uint32( uint8_t ** p, uint32_t value )
+{
+    (*p)[0] = value & 0xFF;
+    (*p)[1] = ( value >> 8  ) & 0xFF;
+    (*p)[2] = ( value >> 16 ) & 0xFF;
+    (*p)[3] = value >> 24;
+    *p += 4;
+}
+
+void netcode_write_uint64( uint8_t ** p, uint64_t value )
+{
+    (*p)[0] = value & 0xFF;
+    (*p)[1] = ( value >> 8  ) & 0xFF;
+    (*p)[2] = ( value >> 16 ) & 0xFF;
+    (*p)[3] = ( value >> 24 ) & 0xFF;
+    (*p)[4] = ( value >> 32 ) & 0xFF;
+    (*p)[5] = ( value >> 40 ) & 0xFF;
+    (*p)[6] = ( value >> 48 ) & 0xFF;
+    (*p)[7] = value >> 56;
+    *p += 8;
+}
+
+void netcode_write_bytes( uint8_t ** p, uint8_t * byte_array, int num_bytes )
+{
+    int i;
+    for ( i = 0; i < num_bytes; ++i )
+    {
+        netcode_write_uint8( p, byte_array[i] );
+    }
+}
+
+uint8_t netcode_read_uint8( uint8_t ** p )
+{
+    uint8_t value = **p;
+    ++(*p);
+    return value;
+}
+
+uint16_t netcode_read_uint16( uint8_t ** p )
+{
+    uint16_t value;
+    value = (*p)[0];
+    value |= ( ( (uint16_t)( (*p)[1] ) ) << 8 );
+    *p += 2;
+    return value;
+}
+
+uint32_t netcode_read_uint32( uint8_t ** p )
+{
+    uint32_t value;
+    value  = (*p)[0];
+    value |= ( ( (uint32_t)( (*p)[1] ) ) << 8 );
+    value |= ( ( (uint32_t)( (*p)[2] ) ) << 16 );
+    value |= ( ( (uint32_t)( (*p)[3] ) ) << 24 );
+    *p += 4;
+    return value;
+}
+
+uint64_t netcode_read_uint64( uint8_t ** p )
+{
+    uint64_t value;
+    value  = (*p)[0];
+    value |= ( ( (uint64_t)( (*p)[1] ) ) << 8  );
+    value |= ( ( (uint64_t)( (*p)[2] ) ) << 16 );
+    value |= ( ( (uint64_t)( (*p)[3] ) ) << 24 );
+    value |= ( ( (uint64_t)( (*p)[4] ) ) << 32 );
+    value |= ( ( (uint64_t)( (*p)[5] ) ) << 40 );
+    value |= ( ( (uint64_t)( (*p)[6] ) ) << 48 );
+    value |= ( ( (uint64_t)( (*p)[7] ) ) << 56 );
+    *p += 8;
+    return value;
+}
+
+void netcode_read_bytes( uint8_t ** p, uint8_t * byte_array, int num_bytes )
+{
+    int i;
+    for ( i = 0; i < num_bytes; ++i )
+    {
+        byte_array[i] = netcode_read_uint8( p );
+    }
+}
+
+// ----------------------------------------------------------------
+
+void netcode_generate_key( uint8_t * key )
+{
+    netcode_assert( key );
+    randombytes_buf( key, NETCODE_KEY_BYTES );
+}
+
+void netcode_generate_nonce( uint8_t * nonce )
+{
+    netcode_assert( nonce );
+    randombytes_buf( nonce, NETCODE_CONNECT_TOKEN_NONCE_BYTES );
+}
+
+void netcode_random_bytes( uint8_t * data, int bytes )
+{
+    netcode_assert( data );
+    netcode_assert( bytes > 0 );
+    randombytes_buf( data, bytes );
+}
+
+int netcode_encrypt_aead_bignonce( uint8_t * message, uint64_t message_length, 
+                          uint8_t * additional, uint64_t additional_length,
+                          NETCODE_CONST uint8_t * nonce,
+                          NETCODE_CONST uint8_t * key )
+{
+    unsigned long long encrypted_length;
+
+    int result = crypto_aead_xchacha20poly1305_ietf_encrypt( message, &encrypted_length,
+                                                            message, (unsigned long long) message_length,
+                                                            additional, (unsigned long long) additional_length,
+                                                            NULL, nonce, key );
+    
+    if ( result != 0 )
+        return NETCODE_ERROR;
+
+    netcode_assert( encrypted_length == message_length + NETCODE_MAC_BYTES );
+
+    return NETCODE_OK;
+}
+
+int netcode_decrypt_aead_bignonce( uint8_t * message, uint64_t message_length, 
+                          uint8_t * additional, uint64_t additional_length,
+                          uint8_t * nonce,
+                          uint8_t * key )
+{
+    unsigned long long decrypted_length;
+
+    int result = crypto_aead_xchacha20poly1305_ietf_decrypt( message, &decrypted_length,
+                                                            NULL,
+                                                            message, (unsigned long long) message_length,
+                                                            additional, (unsigned long long) additional_length,
+                                                            nonce, key );
+
+    if ( result != 0 )
+        return NETCODE_ERROR;
+
+    netcode_assert( decrypted_length == message_length - NETCODE_MAC_BYTES );
+
+    return NETCODE_OK;
+}
+
+int netcode_encrypt_aead( uint8_t * message, uint64_t message_length, 
+                          uint8_t * additional, uint64_t additional_length,
+                          NETCODE_CONST uint8_t * nonce,
+                          NETCODE_CONST uint8_t * key )
+{
+    unsigned long long encrypted_length;
+
+    int result = crypto_aead_chacha20poly1305_ietf_encrypt( message, &encrypted_length,
+                                                            message, (unsigned long long) message_length,
+                                                            additional, (unsigned long long) additional_length,
+                                                            NULL, nonce, key );
+    
+    if ( result != 0 )
+        return NETCODE_ERROR;
+
+    netcode_assert( encrypted_length == message_length + NETCODE_MAC_BYTES );
+
+    return NETCODE_OK;
+}
+
+int netcode_decrypt_aead( uint8_t * message, uint64_t message_length, 
+                          uint8_t * additional, uint64_t additional_length,
+                          uint8_t * nonce,
+                          uint8_t * key )
+{
+    unsigned long long decrypted_length;
+
+    int result = crypto_aead_chacha20poly1305_ietf_decrypt( message, &decrypted_length,
+                                                            NULL,
+                                                            message, (unsigned long long) message_length,
+                                                            additional, (unsigned long long) additional_length,
+                                                            nonce, key );
+
+    if ( result != 0 )
+        return NETCODE_ERROR;
+
+    netcode_assert( decrypted_length == message_length - NETCODE_MAC_BYTES );
+
+    return NETCODE_OK;
+}
+
+// ----------------------------------------------------------------
+
+struct netcode_connect_token_private_t
+{
+    uint64_t client_id;
+    int timeout_seconds;
+    int num_server_addresses;
+    struct netcode_address_t server_addresses[NETCODE_MAX_SERVERS_PER_CONNECT];
+    uint8_t client_to_server_key[NETCODE_KEY_BYTES];
+    uint8_t server_to_client_key[NETCODE_KEY_BYTES];
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+};
+
+void netcode_generate_connect_token_private( struct netcode_connect_token_private_t * connect_token, 
+                                             uint64_t client_id, 
+                                             int timeout_seconds,
+                                             int num_server_addresses, 
+                                             struct netcode_address_t * server_addresses, 
+                                             uint8_t * user_data )
+{
+    netcode_assert( connect_token );
+    netcode_assert( num_server_addresses > 0 );
+    netcode_assert( num_server_addresses <= NETCODE_MAX_SERVERS_PER_CONNECT );
+    netcode_assert( server_addresses );
+    netcode_assert( user_data );
+
+    connect_token->client_id = client_id;
+    connect_token->timeout_seconds = timeout_seconds;
+    connect_token->num_server_addresses = num_server_addresses;
+    
+    int i;
+    for ( i = 0; i < num_server_addresses; ++i )
+    {
+        memcpy( &connect_token->server_addresses[i], &server_addresses[i], sizeof( struct netcode_address_t ) );
+    }
+
+    netcode_generate_key( connect_token->client_to_server_key );
+    netcode_generate_key( connect_token->server_to_client_key );
+
+    if ( user_data != NULL )
+    {
+        memcpy( connect_token->user_data, user_data, NETCODE_USER_DATA_BYTES );
+    }
+    else
+    {
+        memset( connect_token->user_data, 0, NETCODE_USER_DATA_BYTES );
+    }
+}
+
+void netcode_write_connect_token_private( struct netcode_connect_token_private_t * connect_token, uint8_t * buffer, int buffer_length )
+{
+    (void) buffer_length;
+
+    netcode_assert( connect_token );
+    netcode_assert( connect_token->num_server_addresses > 0 );
+    netcode_assert( connect_token->num_server_addresses <= NETCODE_MAX_SERVERS_PER_CONNECT );
+    netcode_assert( buffer );
+    netcode_assert( buffer_length >= NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+    uint8_t * start = buffer;
+
+    (void) start;
+
+    netcode_write_uint64( &buffer, connect_token->client_id );
+
+    netcode_write_uint32( &buffer, connect_token->timeout_seconds );
+
+    netcode_write_uint32( &buffer, connect_token->num_server_addresses );
+
+    int i,j;
+
+    for ( i = 0; i < connect_token->num_server_addresses; ++i )
+    {
+        // todo: should really have a function to write an address
+        if ( connect_token->server_addresses[i].type == NETCODE_ADDRESS_IPV4 )
+        {
+            netcode_write_uint8( &buffer, NETCODE_ADDRESS_IPV4 );
+            for ( j = 0; j < 4; ++j )
+            {
+                netcode_write_uint8( &buffer, connect_token->server_addresses[i].data.ipv4[j] );
+            }
+            netcode_write_uint16( &buffer, connect_token->server_addresses[i].port );
+        }
+        else if ( connect_token->server_addresses[i].type == NETCODE_ADDRESS_IPV6 )
+        {
+            netcode_write_uint8( &buffer, NETCODE_ADDRESS_IPV6 );
+            for ( j = 0; j < 8; ++j )
+            {
+                netcode_write_uint16( &buffer, connect_token->server_addresses[i].data.ipv6[j] );
+            }
+            netcode_write_uint16( &buffer, connect_token->server_addresses[i].port );
+        }
+        else
+        {
+            netcode_assert( 0 );
+        }
+    }
+
+    netcode_write_bytes( &buffer, connect_token->client_to_server_key, NETCODE_KEY_BYTES );
+
+    netcode_write_bytes( &buffer, connect_token->server_to_client_key, NETCODE_KEY_BYTES );
+
+    netcode_write_bytes( &buffer, connect_token->user_data, NETCODE_USER_DATA_BYTES );
+
+    netcode_assert( buffer - start <= NETCODE_CONNECT_TOKEN_PRIVATE_BYTES - NETCODE_MAC_BYTES );
+
+    memset( buffer, 0, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES - ( buffer - start ) );
+}
+
+int netcode_encrypt_connect_token_private( uint8_t * buffer, 
+                                           int buffer_length, 
+                                           uint8_t * version_info, 
+                                           uint64_t protocol_id, 
+                                           uint64_t expire_timestamp, 
+                                           NETCODE_CONST uint8_t * nonce, 
+                                           NETCODE_CONST uint8_t * key )
+{
+    netcode_assert( buffer );
+    netcode_assert( buffer_length == NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+    netcode_assert( key );
+
+    (void) buffer_length;
+
+    uint8_t additional_data[NETCODE_VERSION_INFO_BYTES+8+8];
+    {
+        uint8_t * p = additional_data;
+        netcode_write_bytes( &p, version_info, NETCODE_VERSION_INFO_BYTES );
+        netcode_write_uint64( &p, protocol_id );
+        netcode_write_uint64( &p, expire_timestamp );
+    }
+
+    return netcode_encrypt_aead_bignonce( buffer, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES - NETCODE_MAC_BYTES, additional_data, sizeof( additional_data ), nonce, key );
+}
+
+int netcode_decrypt_connect_token_private( uint8_t * buffer, 
+                                           int buffer_length, 
+                                           uint8_t * version_info, 
+                                           uint64_t protocol_id, 
+                                           uint64_t expire_timestamp, 
+                                           uint8_t * nonce, 
+                                           uint8_t * key )
+{
+    netcode_assert( buffer );
+    netcode_assert( buffer_length == NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+    netcode_assert( key );
+
+    (void) buffer_length;
+
+    uint8_t additional_data[NETCODE_VERSION_INFO_BYTES+8+8];
+    {
+        uint8_t * p = additional_data;
+        netcode_write_bytes( &p, version_info, NETCODE_VERSION_INFO_BYTES );
+        netcode_write_uint64( &p, protocol_id );
+        netcode_write_uint64( &p, expire_timestamp );
+    }
+    return netcode_decrypt_aead_bignonce( buffer, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES, additional_data, sizeof( additional_data ), nonce, key );
+}
+
+int netcode_read_connect_token_private( uint8_t * buffer, int buffer_length, struct netcode_connect_token_private_t * connect_token )
+{
+    netcode_assert( buffer );
+    netcode_assert( connect_token );
+
+    if ( buffer_length < NETCODE_CONNECT_TOKEN_PRIVATE_BYTES )
+        return NETCODE_ERROR;
+    
+    connect_token->client_id = netcode_read_uint64( &buffer );
+
+    connect_token->timeout_seconds = (int) netcode_read_uint32( &buffer );
+
+    connect_token->num_server_addresses = netcode_read_uint32( &buffer );
+
+    if ( connect_token->num_server_addresses <= 0 )
+        return NETCODE_ERROR;
+
+    if ( connect_token->num_server_addresses > NETCODE_MAX_SERVERS_PER_CONNECT )
+        return NETCODE_ERROR;
+
+    int i,j;
+
+    for ( i = 0; i < connect_token->num_server_addresses; ++i )
+    {
+        // todo: should really have a function to read an address
+        connect_token->server_addresses[i].type = netcode_read_uint8( &buffer );
+
+        if ( connect_token->server_addresses[i].type == NETCODE_ADDRESS_IPV4 )
+        {
+            for ( j = 0; j < 4; ++j )
+            {
+                connect_token->server_addresses[i].data.ipv4[j] = netcode_read_uint8( &buffer );
+            }
+            connect_token->server_addresses[i].port = netcode_read_uint16( &buffer );
+        }
+        else if ( connect_token->server_addresses[i].type == NETCODE_ADDRESS_IPV6 )
+        {
+            for ( j = 0; j < 8; ++j )
+            {
+                connect_token->server_addresses[i].data.ipv6[j] = netcode_read_uint16( &buffer );
+            }
+            connect_token->server_addresses[i].port = netcode_read_uint16( &buffer );
+        }
+        else
+        {
+            return NETCODE_ERROR;
+        }
+    }
+
+    netcode_read_bytes( &buffer, connect_token->client_to_server_key, NETCODE_KEY_BYTES );
+
+    netcode_read_bytes( &buffer, connect_token->server_to_client_key, NETCODE_KEY_BYTES );
+
+    netcode_read_bytes( &buffer, connect_token->user_data, NETCODE_USER_DATA_BYTES );
+
+    return NETCODE_OK;
+}
+
+// -----------------------------------------------
+
+struct netcode_challenge_token_t
+{
+    uint64_t client_id;
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+};
+
+void netcode_write_challenge_token( struct netcode_challenge_token_t * challenge_token, uint8_t * buffer, int buffer_length )
+{
+    (void) buffer_length;
+
+    netcode_assert( challenge_token );
+    netcode_assert( buffer );
+    netcode_assert( buffer_length >= NETCODE_CHALLENGE_TOKEN_BYTES );
+
+    memset( buffer, 0, NETCODE_CHALLENGE_TOKEN_BYTES );
+
+    uint8_t * start = buffer;
+
+    (void) start;
+
+    netcode_write_uint64( &buffer, challenge_token->client_id );
+
+    netcode_write_bytes( &buffer, challenge_token->user_data, NETCODE_USER_DATA_BYTES ); 
+
+    netcode_assert( buffer - start <= NETCODE_CHALLENGE_TOKEN_BYTES - NETCODE_MAC_BYTES );
+}
+
+int netcode_encrypt_challenge_token( uint8_t * buffer, int buffer_length, uint64_t sequence, uint8_t * key )
+{
+    netcode_assert( buffer );
+    netcode_assert( buffer_length >= NETCODE_CHALLENGE_TOKEN_BYTES );
+    netcode_assert( key );
+
+    (void) buffer_length;
+
+    uint8_t nonce[12];
+    {
+        uint8_t * p = nonce;
+        netcode_write_uint32( &p, 0 );
+        netcode_write_uint64( &p, sequence );
+    }
+
+    return netcode_encrypt_aead( buffer, NETCODE_CHALLENGE_TOKEN_BYTES - NETCODE_MAC_BYTES, NULL, 0, nonce, key );
+}
+
+int netcode_decrypt_challenge_token( uint8_t * buffer, int buffer_length, uint64_t sequence, uint8_t * key )
+{
+    netcode_assert( buffer );
+    netcode_assert( buffer_length >= NETCODE_CHALLENGE_TOKEN_BYTES );
+    netcode_assert( key );
+
+    (void) buffer_length;
+
+    uint8_t nonce[12];
+    {
+        uint8_t * p = nonce;
+        netcode_write_uint32( &p, 0 );
+        netcode_write_uint64( &p, sequence );
+    }
+
+    return netcode_decrypt_aead( buffer, NETCODE_CHALLENGE_TOKEN_BYTES, NULL, 0, nonce, key );
+}
+
+int netcode_read_challenge_token( uint8_t * buffer, int buffer_length, struct netcode_challenge_token_t * challenge_token )
+{
+    netcode_assert( buffer );
+    netcode_assert( challenge_token );
+
+    if ( buffer_length < NETCODE_CHALLENGE_TOKEN_BYTES )
+        return NETCODE_ERROR;
+
+    uint8_t * start = buffer;
+
+    (void) start;
+    
+    challenge_token->client_id = netcode_read_uint64( &buffer );
+
+    netcode_read_bytes( &buffer, challenge_token->user_data, NETCODE_USER_DATA_BYTES );
+
+    netcode_assert( buffer - start == 8 + NETCODE_USER_DATA_BYTES );
+
+    return NETCODE_OK;
+}
+
+// ----------------------------------------------------------------
+
+#define NETCODE_CONNECTION_REQUEST_PACKET           0
+#define NETCODE_CONNECTION_DENIED_PACKET            1
+#define NETCODE_CONNECTION_CHALLENGE_PACKET         2
+#define NETCODE_CONNECTION_RESPONSE_PACKET          3
+#define NETCODE_CONNECTION_KEEP_ALIVE_PACKET        4
+#define NETCODE_CONNECTION_PAYLOAD_PACKET           5
+#define NETCODE_CONNECTION_DISCONNECT_PACKET        6
+#define NETCODE_CONNECTION_NUM_PACKETS              7
+
+struct netcode_connection_request_packet_t
+{
+    uint8_t packet_type;
+    uint8_t version_info[NETCODE_VERSION_INFO_BYTES];
+    uint64_t protocol_id;
+    uint64_t connect_token_expire_timestamp;
+    uint8_t connect_token_nonce[NETCODE_CONNECT_TOKEN_NONCE_BYTES];
+    uint8_t connect_token_data[NETCODE_CONNECT_TOKEN_PRIVATE_BYTES];
+};
+
+struct netcode_connection_denied_packet_t
+{
+    uint8_t packet_type;
+};
+
+struct netcode_connection_challenge_packet_t
+{
+    uint8_t packet_type;
+    uint64_t challenge_token_sequence;
+    uint8_t challenge_token_data[NETCODE_CHALLENGE_TOKEN_BYTES];
+};
+
+struct netcode_connection_response_packet_t
+{
+    uint8_t packet_type;
+    uint64_t challenge_token_sequence;
+    uint8_t challenge_token_data[NETCODE_CHALLENGE_TOKEN_BYTES];
+};
+
+struct netcode_connection_keep_alive_packet_t
+{
+    uint8_t packet_type;
+    int client_index;
+    int max_clients;
+};
+
+struct netcode_connection_payload_packet_t
+{
+    uint8_t packet_type;
+    uint32_t payload_bytes;
+    uint8_t payload_data[1];
+};
+
+struct netcode_connection_disconnect_packet_t
+{
+    uint8_t packet_type;
+};
+
+struct netcode_connection_payload_packet_t * netcode_create_payload_packet( int payload_bytes, void * allocator_context, void* (*allocate_function)(void*,uint64_t) )
+{
+    netcode_assert( payload_bytes >= 0 );
+    netcode_assert( payload_bytes <= NETCODE_MAX_PAYLOAD_BYTES );
+
+    if ( allocate_function == NULL )
+    {
+        allocate_function = netcode_default_allocate_function;
+    }
+
+    struct netcode_connection_payload_packet_t * packet = (struct netcode_connection_payload_packet_t*) 
+        allocate_function( allocator_context, sizeof( struct netcode_connection_payload_packet_t ) + payload_bytes );
+
+    if ( !packet )
+        return NULL;
+    
+    packet->packet_type = NETCODE_CONNECTION_PAYLOAD_PACKET;
+    packet->payload_bytes = payload_bytes;
+
+    return packet;
+}
+
+struct netcode_context_t
+{
+    uint8_t write_packet_key[NETCODE_KEY_BYTES];
+    uint8_t read_packet_key[NETCODE_KEY_BYTES];
+};
+
+int netcode_sequence_number_bytes_required( uint64_t sequence )
+{
+    int i;
+    uint64_t mask = 0xFF00000000000000UL;
+    for ( i = 0; i < 7; ++i )
+    {
+        if ( sequence & mask )
+            break;
+        mask >>= 8;
+    }
+    return 8 - i;
+}
+
+int netcode_write_packet( void * packet, uint8_t * buffer, int buffer_length, uint64_t sequence, uint8_t * write_packet_key, uint64_t protocol_id )
+{
+    netcode_assert( packet );
+    netcode_assert( buffer );
+    netcode_assert( write_packet_key );
+
+    (void) buffer_length;
+
+    uint8_t packet_type = ((uint8_t*)packet)[0];
+
+    if ( packet_type == NETCODE_CONNECTION_REQUEST_PACKET )
+    {
+        // connection request packet: first byte is zero
+
+        netcode_assert( buffer_length >= 1 + 13 + 8 + 8 + NETCODE_CONNECT_TOKEN_NONCE_BYTES + NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+        struct netcode_connection_request_packet_t * p = (struct netcode_connection_request_packet_t*) packet;
+
+        uint8_t * start = buffer;
+
+        netcode_write_uint8( &buffer, NETCODE_CONNECTION_REQUEST_PACKET );
+        netcode_write_bytes( &buffer, p->version_info, NETCODE_VERSION_INFO_BYTES );
+        netcode_write_uint64( &buffer, p->protocol_id );
+        netcode_write_uint64( &buffer, p->connect_token_expire_timestamp );
+        netcode_write_bytes( &buffer, p->connect_token_nonce, NETCODE_CONNECT_TOKEN_NONCE_BYTES );
+        netcode_write_bytes( &buffer, p->connect_token_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+        netcode_assert( buffer - start == 1 + 13 + 8 + 8 + NETCODE_CONNECT_TOKEN_NONCE_BYTES + NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+        return (int) ( buffer - start );
+    }
+    else
+    {
+        // *** encrypted packets ***
+
+        // write the prefix byte (this is a combination of the packet type and number of sequence bytes)
+
+        uint8_t * start = buffer;
+
+        uint8_t sequence_bytes = (uint8_t) netcode_sequence_number_bytes_required( sequence );
+
+        netcode_assert( sequence_bytes >= 1 );
+        netcode_assert( sequence_bytes <= 8 );
+
+        netcode_assert( packet_type <= 0xF );
+
+        uint8_t prefix_byte = packet_type | ( sequence_bytes << 4 );
+
+        netcode_write_uint8( &buffer, prefix_byte );
+
+        // write the variable length sequence number [1,8] bytes.
+
+        uint64_t sequence_temp = sequence;
+
+        int i;
+        for ( i = 0; i < sequence_bytes; ++i )
+        {
+            netcode_write_uint8( &buffer, (uint8_t) ( sequence_temp & 0xFF ) );
+            sequence_temp >>= 8;
+        }
+
+        // write packet data according to type. this data will be encrypted.
+
+        uint8_t * encrypted_start = buffer;
+
+        switch ( packet_type )
+        {
+            case NETCODE_CONNECTION_DENIED_PACKET:
+            {
+                // ...
+            }
+            break;
+
+            case NETCODE_CONNECTION_CHALLENGE_PACKET:
+            {
+                struct netcode_connection_challenge_packet_t * p = (struct netcode_connection_challenge_packet_t*) packet;
+                netcode_write_uint64( &buffer, p->challenge_token_sequence );
+                netcode_write_bytes( &buffer, p->challenge_token_data, NETCODE_CHALLENGE_TOKEN_BYTES );
+            }
+            break;
+
+            case NETCODE_CONNECTION_RESPONSE_PACKET:
+            {
+                struct netcode_connection_response_packet_t * p = (struct netcode_connection_response_packet_t*) packet;
+                netcode_write_uint64( &buffer, p->challenge_token_sequence );
+                netcode_write_bytes( &buffer, p->challenge_token_data, NETCODE_CHALLENGE_TOKEN_BYTES );
+            }
+            break;
+
+            case NETCODE_CONNECTION_KEEP_ALIVE_PACKET:
+            {
+                struct netcode_connection_keep_alive_packet_t * p = (struct netcode_connection_keep_alive_packet_t*) packet;
+                netcode_write_uint32( &buffer, p->client_index );
+                netcode_write_uint32( &buffer, p->max_clients );
+            }
+            break;
+
+            case NETCODE_CONNECTION_PAYLOAD_PACKET:
+            {
+                struct netcode_connection_payload_packet_t * p = (struct netcode_connection_payload_packet_t*) packet;
+
+                netcode_assert( p->payload_bytes <= NETCODE_MAX_PAYLOAD_BYTES );
+
+                netcode_write_bytes( &buffer, p->payload_data, p->payload_bytes );
+            }
+            break;
+
+            case NETCODE_CONNECTION_DISCONNECT_PACKET:
+            {
+                // ...
+            }
+            break;
+
+            default:
+                netcode_assert( 0 );
+        }
+
+        netcode_assert( buffer - start <= buffer_length - NETCODE_MAC_BYTES );
+
+        uint8_t * encrypted_finish = buffer;
+
+        // encrypt the per-packet packet written with the prefix byte, protocol id and version as the associated data. this must match to decrypt.
+
+        uint8_t additional_data[NETCODE_VERSION_INFO_BYTES+8+1];
+        {
+            uint8_t * p = additional_data;
+            netcode_write_bytes( &p, NETCODE_VERSION_INFO, NETCODE_VERSION_INFO_BYTES );
+            netcode_write_uint64( &p, protocol_id );
+            netcode_write_uint8( &p, prefix_byte );
+        }
+
+        uint8_t nonce[12];
+        {
+            uint8_t * p = nonce;
+            netcode_write_uint32( &p, 0 );
+            netcode_write_uint64( &p, sequence );
+        }
+
+        if ( netcode_encrypt_aead( encrypted_start, 
+                                   encrypted_finish - encrypted_start, 
+                                   additional_data, sizeof( additional_data ), 
+                                   nonce, write_packet_key ) != NETCODE_OK )
+        {
+            return NETCODE_ERROR;
+        }
+
+        buffer += NETCODE_MAC_BYTES;
+
+        netcode_assert( buffer - start <= buffer_length );
+
+        return (int) ( buffer - start );
+    }
+}
+
+struct netcode_replay_protection_t
+{
+    uint64_t most_recent_sequence;
+    uint64_t received_packet[NETCODE_REPLAY_PROTECTION_BUFFER_SIZE];
+};
+
+void netcode_replay_protection_reset( struct netcode_replay_protection_t * replay_protection )
+{
+    netcode_assert( replay_protection );
+    replay_protection->most_recent_sequence = 0;
+    memset( replay_protection->received_packet, 0xFF, sizeof( replay_protection->received_packet ) );
+}
+
+int netcode_replay_protection_already_received( struct netcode_replay_protection_t * replay_protection, uint64_t sequence )
+{
+    netcode_assert( replay_protection );
+
+    if ( sequence + NETCODE_REPLAY_PROTECTION_BUFFER_SIZE <= replay_protection->most_recent_sequence )
+        return 1;
+    
+    int index = (int) ( sequence % NETCODE_REPLAY_PROTECTION_BUFFER_SIZE );
+
+    if ( replay_protection->received_packet[index] == 0xFFFFFFFFFFFFFFFFLL )
+        return 0;
+
+    if ( replay_protection->received_packet[index] >= sequence )
+        return 1;
+
+    return 0;
+}
+
+void netcode_replay_protection_advance_sequence( struct netcode_replay_protection_t * replay_protection, uint64_t sequence )
+{
+    netcode_assert( replay_protection );
+
+    if ( sequence > replay_protection->most_recent_sequence )
+        replay_protection->most_recent_sequence = sequence;
+
+    int index = (int) ( sequence % NETCODE_REPLAY_PROTECTION_BUFFER_SIZE );
+
+    replay_protection->received_packet[index] = sequence;
+}
+
+void * netcode_read_packet( uint8_t * buffer, 
+                            int buffer_length, 
+                            uint64_t * sequence, 
+                            uint8_t * read_packet_key, 
+                            uint64_t protocol_id, 
+                            uint64_t current_timestamp, 
+                            uint8_t * private_key, 
+                            uint8_t * allowed_packets, 
+                            struct netcode_replay_protection_t * replay_protection, 
+                            void * allocator_context, 
+                            void* (*allocate_function)(void*,uint64_t) )
+{
+    netcode_assert( sequence );
+    netcode_assert( allowed_packets );
+
+    *sequence = 0;
+
+    if ( allocate_function == NULL )
+    {
+        allocate_function = netcode_default_allocate_function;
+    }
+
+    if ( buffer_length < 1 )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored packet. buffer length is less than 1\n" );
+        return NULL;
+    }
+
+    uint8_t * start = buffer;
+
+    uint8_t prefix_byte = netcode_read_uint8( &buffer );
+
+    if ( prefix_byte == NETCODE_CONNECTION_REQUEST_PACKET )
+    {
+        // connection request packet: first byte is zero
+
+        if ( !allowed_packets[NETCODE_CONNECTION_REQUEST_PACKET] )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection request packet. packet type is not allowed\n" );
+            return NULL;
+        }
+
+        if ( buffer_length != 1 + NETCODE_VERSION_INFO_BYTES + 8 + 8 + NETCODE_CONNECT_TOKEN_NONCE_BYTES + NETCODE_CONNECT_TOKEN_PRIVATE_BYTES )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection request packet. bad packet length (expected %d, got %d)\n", 1 + NETCODE_VERSION_INFO_BYTES + 8 + 8 + 8 + NETCODE_CONNECT_TOKEN_PRIVATE_BYTES, buffer_length );
+            return NULL;
+        }
+
+        if ( !private_key )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection request packet. no private key\n" );
+            return NULL;
+        }
+
+        uint8_t version_info[NETCODE_VERSION_INFO_BYTES];
+        netcode_read_bytes( &buffer, version_info, NETCODE_VERSION_INFO_BYTES );
+        if ( version_info[0]  != 'N' || 
+             version_info[1]  != 'E' || 
+             version_info[2]  != 'T' || 
+             version_info[3]  != 'C' || 
+             version_info[4]  != 'O' ||
+             version_info[5]  != 'D' ||
+             version_info[6]  != 'E' ||
+             version_info[7]  != ' ' || 
+             version_info[8]  != '1' ||
+             version_info[9]  != '.' ||
+             version_info[10] != '0' ||
+             version_info[11] != '2' ||
+             version_info[12] != '\0' )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection request packet. bad version info\n" );
+            return NULL;
+        }
+
+        uint64_t packet_protocol_id = netcode_read_uint64( &buffer );
+        if ( packet_protocol_id != protocol_id )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection request packet. wrong protocol id. expected %.16" PRIx64 ", got %.16" PRIx64 "\n", 
+                protocol_id, packet_protocol_id );
+            return NULL;
+        }
+
+        uint64_t packet_connect_token_expire_timestamp = netcode_read_uint64( &buffer );
+        if ( packet_connect_token_expire_timestamp <= current_timestamp )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection request packet. connect token expired\n" );
+            return NULL;
+        }
+
+        uint8_t packet_connect_token_nonce[NETCODE_CONNECT_TOKEN_NONCE_BYTES];
+        netcode_read_bytes(&buffer, packet_connect_token_nonce, sizeof(packet_connect_token_nonce));
+
+        netcode_assert( buffer - start == 1 + NETCODE_VERSION_INFO_BYTES + 8 + 8 + NETCODE_CONNECT_TOKEN_NONCE_BYTES );
+
+        if ( netcode_decrypt_connect_token_private( buffer, 
+                                                    NETCODE_CONNECT_TOKEN_PRIVATE_BYTES, 
+                                                    version_info, 
+                                                    protocol_id, 
+                                                    packet_connect_token_expire_timestamp, 
+                                                    packet_connect_token_nonce, 
+                                                    private_key ) != NETCODE_OK )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection request packet. connect token failed to decrypt\n" );
+            return NULL;
+        }
+
+        struct netcode_connection_request_packet_t * packet = (struct netcode_connection_request_packet_t*) 
+            allocate_function( allocator_context, sizeof( struct netcode_connection_request_packet_t ) );
+
+        if ( !packet )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection request packet. failed to allocate packet\n" );
+            return NULL;
+        }
+
+        packet->packet_type = NETCODE_CONNECTION_REQUEST_PACKET;
+        memcpy( packet->version_info, version_info, NETCODE_VERSION_INFO_BYTES );
+        packet->protocol_id = packet_protocol_id;
+        packet->connect_token_expire_timestamp = packet_connect_token_expire_timestamp;
+        memcpy( packet->connect_token_nonce, packet_connect_token_nonce, NETCODE_CONNECT_TOKEN_NONCE_BYTES );
+        netcode_read_bytes( &buffer, packet->connect_token_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+        netcode_assert( buffer - start == 1 + NETCODE_VERSION_INFO_BYTES + 8 + 8 + NETCODE_CONNECT_TOKEN_NONCE_BYTES + NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+        return packet;
+    }
+    else
+    {
+        // *** encrypted packets ***
+
+        if ( !read_packet_key )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored encrypted packet. no read packet key for this address\n" );
+            return NULL;
+        }
+
+        if ( buffer_length < 1 + 1 + NETCODE_MAC_BYTES )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored encrypted packet. packet is too small to be valid (%d bytes)\n", buffer_length );
+            return NULL;
+        }
+
+        // extract the packet type and number of sequence bytes from the prefix byte
+
+        int packet_type = prefix_byte & 0xF;
+
+        if ( packet_type >= NETCODE_CONNECTION_NUM_PACKETS )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored encrypted packet. packet type %d is invalid\n", packet_type );
+            return NULL;
+        }
+
+        if ( !allowed_packets[packet_type] )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored encrypted packet. packet type %d is not allowed\n", packet_type );
+            return NULL;
+        }
+
+        int sequence_bytes = prefix_byte >> 4;
+
+        if ( sequence_bytes < 1 || sequence_bytes > 8 )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored encrypted packet. sequence bytes %d is out of range [1,8]\n", sequence_bytes );
+            return NULL;
+        }
+
+        if ( buffer_length < 1 + sequence_bytes + NETCODE_MAC_BYTES )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored encrypted packet. buffer is too small for sequence bytes + encryption mac\n" );
+            return NULL;
+        }
+
+        // read variable length sequence number [1,8]
+
+        int i;
+        for ( i = 0; i < sequence_bytes; ++i )
+        {
+            uint8_t value = netcode_read_uint8( &buffer );
+            (*sequence) |= ( uint64_t) ( value ) << ( 8 * i );
+        }
+
+        // ignore the packet if it has already been received
+
+        if ( replay_protection && packet_type >= NETCODE_CONNECTION_KEEP_ALIVE_PACKET )
+        {
+            if ( netcode_replay_protection_already_received( replay_protection, *sequence ) )
+            {
+                netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored packet. sequence %.16" PRIx64 " already received (replay protection)\n", *sequence );
+                return NULL;
+            }
+        }
+
+        // decrypt the per-packet type data
+
+        uint8_t additional_data[NETCODE_VERSION_INFO_BYTES+8+1];
+        {
+            uint8_t * p = additional_data;
+            netcode_write_bytes( &p, NETCODE_VERSION_INFO, NETCODE_VERSION_INFO_BYTES );
+            netcode_write_uint64( &p, protocol_id );
+            netcode_write_uint8( &p, prefix_byte );
+        }
+
+        uint8_t nonce[12];
+        {
+            uint8_t * p = nonce;
+            netcode_write_uint32( &p, 0 );
+            netcode_write_uint64( &p, *sequence );
+        }
+
+        int encrypted_bytes = (int) ( buffer_length - ( buffer - start ) );
+
+        if ( encrypted_bytes < NETCODE_MAC_BYTES )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored encrypted packet. encrypted payload is too small\n" );
+            return NULL;
+        }
+
+        if ( netcode_decrypt_aead( buffer, encrypted_bytes, additional_data, sizeof( additional_data ), nonce, read_packet_key ) != NETCODE_OK )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored encrypted packet. failed to decrypt\n" );
+            return NULL;
+        }
+
+        int decrypted_bytes = encrypted_bytes - NETCODE_MAC_BYTES;
+
+        // update the latest replay protection sequence #
+
+        if ( replay_protection && packet_type >= NETCODE_CONNECTION_KEEP_ALIVE_PACKET )
+        {
+            netcode_replay_protection_advance_sequence( replay_protection, *sequence );
+        }
+
+        // process the per-packet type data that was just decrypted
+        
+        switch ( packet_type )
+        {
+            case NETCODE_CONNECTION_DENIED_PACKET:
+            {
+                if ( decrypted_bytes != 0 )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection denied packet. decrypted packet data is wrong size\n" );
+                    return NULL;
+                }
+
+                struct netcode_connection_denied_packet_t * packet = (struct netcode_connection_denied_packet_t*) 
+                    allocate_function( allocator_context, sizeof( struct netcode_connection_denied_packet_t ) );
+
+                if ( !packet )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection denied packet. could not allocate packet struct\n" );
+                    return NULL;
+                }
+                
+                packet->packet_type = NETCODE_CONNECTION_DENIED_PACKET;
+                
+                return packet;
+            }
+            break;
+
+            case NETCODE_CONNECTION_CHALLENGE_PACKET:
+            {
+                if ( decrypted_bytes != 8 + NETCODE_CHALLENGE_TOKEN_BYTES )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection challenge packet. decrypted packet data is wrong size\n" );
+                    return NULL;
+                }
+
+                struct netcode_connection_challenge_packet_t * packet = (struct netcode_connection_challenge_packet_t*) 
+                    allocate_function( allocator_context, sizeof( struct netcode_connection_challenge_packet_t ) );
+
+                if ( !packet )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection challenge packet. could not allocate packet struct\n" );
+                    return NULL;
+                }
+                
+                packet->packet_type = NETCODE_CONNECTION_CHALLENGE_PACKET;
+                packet->challenge_token_sequence = netcode_read_uint64( &buffer );
+                netcode_read_bytes( &buffer, packet->challenge_token_data, NETCODE_CHALLENGE_TOKEN_BYTES );
+                
+                return packet;
+            }
+            break;
+
+            case NETCODE_CONNECTION_RESPONSE_PACKET:
+            {
+                if ( decrypted_bytes != 8 + NETCODE_CHALLENGE_TOKEN_BYTES )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection response packet. decrypted packet data is wrong size\n" );
+                    return NULL;
+                }
+
+                struct netcode_connection_response_packet_t * packet = (struct netcode_connection_response_packet_t*) 
+                    allocate_function( allocator_context, sizeof( struct netcode_connection_response_packet_t ) );
+
+                if ( !packet )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection response packet. could not allocate packet struct\n" );
+                    return NULL;
+                }
+                
+                packet->packet_type = NETCODE_CONNECTION_RESPONSE_PACKET;
+                packet->challenge_token_sequence = netcode_read_uint64( &buffer );
+                netcode_read_bytes( &buffer, packet->challenge_token_data, NETCODE_CHALLENGE_TOKEN_BYTES );
+                
+                return packet;
+            }
+            break;
+
+            case NETCODE_CONNECTION_KEEP_ALIVE_PACKET:
+            {
+                if ( decrypted_bytes != 8 )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection keep alive packet. decrypted packet data is wrong size\n" );
+                    return NULL;
+                }
+
+                struct netcode_connection_keep_alive_packet_t * packet = (struct netcode_connection_keep_alive_packet_t*) 
+                    allocate_function( allocator_context, sizeof( struct netcode_connection_keep_alive_packet_t ) );
+
+                if ( !packet )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection keep alive packet. could not allocate packet struct\n" );
+                    return NULL;
+                }
+                
+                packet->packet_type = NETCODE_CONNECTION_KEEP_ALIVE_PACKET;
+                packet->client_index = netcode_read_uint32( &buffer );
+                packet->max_clients = netcode_read_uint32( &buffer );
+                
+                return packet;
+            }
+            break;
+            
+            case NETCODE_CONNECTION_PAYLOAD_PACKET:
+            {
+                if ( decrypted_bytes < 1 )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection payload packet. payload is too small\n" );
+                    return NULL;
+                }
+
+                if ( decrypted_bytes > NETCODE_MAX_PAYLOAD_BYTES )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection payload packet. payload is too large\n" );
+                    return NULL;
+                }
+
+                struct netcode_connection_payload_packet_t * packet = netcode_create_payload_packet( decrypted_bytes, allocator_context, allocate_function );
+
+                if ( !packet )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection payload packet. could not allocate packet struct\n" );
+                    return NULL;
+                }
+                
+                memcpy( packet->payload_data, buffer, decrypted_bytes );
+                
+                return packet;
+            }
+            break;
+
+            case NETCODE_CONNECTION_DISCONNECT_PACKET:
+            {
+                if ( decrypted_bytes != 0 )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection disconnect packet. decrypted packet data is wrong size\n" );
+                    return NULL;
+                }
+
+                struct netcode_connection_disconnect_packet_t * packet = (struct netcode_connection_disconnect_packet_t*) 
+                    allocate_function( allocator_context, sizeof( struct netcode_connection_disconnect_packet_t ) );
+
+                if ( !packet )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "ignored connection disconnect packet. could not allocate packet struct\n" );
+                    return NULL;
+                }
+                
+                packet->packet_type = NETCODE_CONNECTION_DISCONNECT_PACKET;
+                
+                return packet;
+            }
+            break;
+
+            default:
+                return NULL;
+        }
+    }
+}
+
+// ----------------------------------------------------------------
+
+struct netcode_connect_token_t
+{
+    uint8_t version_info[NETCODE_VERSION_INFO_BYTES];
+    uint64_t protocol_id;
+    uint64_t create_timestamp;
+    uint64_t expire_timestamp;
+    uint8_t nonce[NETCODE_CONNECT_TOKEN_NONCE_BYTES];
+    uint8_t private_data[NETCODE_CONNECT_TOKEN_PRIVATE_BYTES];
+    int timeout_seconds;
+    int num_server_addresses;
+    struct netcode_address_t server_addresses[NETCODE_MAX_SERVERS_PER_CONNECT];
+    uint8_t client_to_server_key[NETCODE_KEY_BYTES];
+    uint8_t server_to_client_key[NETCODE_KEY_BYTES];
+};
+
+void netcode_write_connect_token( struct netcode_connect_token_t * connect_token, uint8_t * buffer, int buffer_length )
+{
+    netcode_assert( connect_token );
+    netcode_assert( buffer );
+    netcode_assert( buffer_length >= NETCODE_CONNECT_TOKEN_BYTES );
+
+    uint8_t * start = buffer;
+
+    (void) start;
+    (void) buffer_length;
+
+    netcode_write_bytes( &buffer, connect_token->version_info, NETCODE_VERSION_INFO_BYTES );
+
+    netcode_write_uint64( &buffer, connect_token->protocol_id );
+
+    netcode_write_uint64( &buffer, connect_token->create_timestamp );
+
+    netcode_write_uint64( &buffer, connect_token->expire_timestamp );
+
+    netcode_write_bytes( &buffer, connect_token->nonce, NETCODE_CONNECT_TOKEN_NONCE_BYTES );
+
+    netcode_write_bytes( &buffer, connect_token->private_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+    int i,j;
+
+    netcode_write_uint32( &buffer, connect_token->timeout_seconds );
+
+    netcode_write_uint32( &buffer, connect_token->num_server_addresses );
+
+    for ( i = 0; i < connect_token->num_server_addresses; ++i )
+    {
+        // todo: really just need a function to write an address. too much cut & paste here
+        if ( connect_token->server_addresses[i].type == NETCODE_ADDRESS_IPV4 )
+        {
+            netcode_write_uint8( &buffer, NETCODE_ADDRESS_IPV4 );
+            for ( j = 0; j < 4; ++j )
+            {
+                netcode_write_uint8( &buffer, connect_token->server_addresses[i].data.ipv4[j] );
+            }
+            netcode_write_uint16( &buffer, connect_token->server_addresses[i].port );
+        }
+        else if ( connect_token->server_addresses[i].type == NETCODE_ADDRESS_IPV6 )
+        {
+            netcode_write_uint8( &buffer, NETCODE_ADDRESS_IPV6 );
+            for ( j = 0; j < 8; ++j )
+            {
+                netcode_write_uint16( &buffer, connect_token->server_addresses[i].data.ipv6[j] );
+            }
+            netcode_write_uint16( &buffer, connect_token->server_addresses[i].port );
+        }
+        else
+        {
+            netcode_assert( 0 );
+        }
+    }
+
+    netcode_write_bytes( &buffer, connect_token->client_to_server_key, NETCODE_KEY_BYTES );
+
+    netcode_write_bytes( &buffer, connect_token->server_to_client_key, NETCODE_KEY_BYTES );
+
+    netcode_assert( buffer - start <= NETCODE_CONNECT_TOKEN_BYTES );
+
+    memset( buffer, 0, NETCODE_CONNECT_TOKEN_BYTES - ( buffer - start ) );
+}
+
+int netcode_read_connect_token( uint8_t * buffer, int buffer_length, struct netcode_connect_token_t * connect_token )
+{
+    netcode_assert( buffer );
+    netcode_assert( connect_token );
+
+    if ( buffer_length != NETCODE_CONNECT_TOKEN_BYTES )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: read connect data has bad buffer length (%d)\n", buffer_length );
+        return NETCODE_ERROR;
+    }
+
+    netcode_read_bytes( &buffer, connect_token->version_info, NETCODE_VERSION_INFO_BYTES );
+    if ( connect_token->version_info[0]  != 'N' || 
+         connect_token->version_info[1]  != 'E' || 
+         connect_token->version_info[2]  != 'T' || 
+         connect_token->version_info[3]  != 'C' || 
+         connect_token->version_info[4]  != 'O' ||
+         connect_token->version_info[5]  != 'D' ||
+         connect_token->version_info[6]  != 'E' ||
+         connect_token->version_info[7]  != ' ' || 
+         connect_token->version_info[8]  != '1' ||
+         connect_token->version_info[9]  != '.' ||
+         connect_token->version_info[10] != '0' ||
+         connect_token->version_info[11] != '2' ||
+         connect_token->version_info[12] != '\0' )
+    {
+        connect_token->version_info[12] = '\0';
+        netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: read connect data has bad version info (got %s, expected %s)\n", connect_token->version_info, NETCODE_VERSION_INFO );
+        return NETCODE_ERROR;
+    }
+
+    connect_token->protocol_id = netcode_read_uint64( &buffer );
+
+    connect_token->create_timestamp = netcode_read_uint64( &buffer );
+
+    connect_token->expire_timestamp = netcode_read_uint64( &buffer );
+
+    if ( connect_token->create_timestamp > connect_token->expire_timestamp )
+        return NETCODE_ERROR;
+
+    netcode_read_bytes( &buffer, connect_token->nonce, NETCODE_CONNECT_TOKEN_NONCE_BYTES );
+
+    netcode_read_bytes( &buffer, connect_token->private_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+    connect_token->timeout_seconds = (int) netcode_read_uint32( &buffer );
+
+    connect_token->num_server_addresses = netcode_read_uint32( &buffer );
+
+    if ( connect_token->num_server_addresses <= 0 || connect_token->num_server_addresses > NETCODE_MAX_SERVERS_PER_CONNECT )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: read connect data has bad number of server addresses (%d)\n", connect_token->num_server_addresses );
+        return NETCODE_ERROR;
+    }
+
+    int i,j;
+
+    for ( i = 0; i < connect_token->num_server_addresses; ++i )
+    {
+        // todo: really need a function to read an address
+        connect_token->server_addresses[i].type = netcode_read_uint8( &buffer );
+
+        if ( connect_token->server_addresses[i].type == NETCODE_ADDRESS_IPV4 )
+        {
+            for ( j = 0; j < 4; ++j )
+            {
+                connect_token->server_addresses[i].data.ipv4[j] = netcode_read_uint8( &buffer );
+            }
+            connect_token->server_addresses[i].port = netcode_read_uint16( &buffer );
+        }
+        else if ( connect_token->server_addresses[i].type == NETCODE_ADDRESS_IPV6 )
+        {
+            for ( j = 0; j < 8; ++j )
+            {
+                connect_token->server_addresses[i].data.ipv6[j] = netcode_read_uint16( &buffer );
+            }
+            connect_token->server_addresses[i].port = netcode_read_uint16( &buffer );
+        }
+        else
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: read connect data has bad address type (%d)\n", connect_token->server_addresses[i].type );
+            return NETCODE_ERROR;
+        }
+    }
+
+    netcode_read_bytes( &buffer, connect_token->client_to_server_key, NETCODE_KEY_BYTES );
+
+    netcode_read_bytes( &buffer, connect_token->server_to_client_key, NETCODE_KEY_BYTES );
+    
+    return NETCODE_OK;
+}
+
+// ----------------------------------------------------------------
+
+struct netcode_packet_queue_t
+{
+    void * allocator_context;
+    void * (*allocate_function)(void*,uint64_t);
+    void (*free_function)(void*,void*);
+    int num_packets;
+    int start_index;
+    void * packet_data[NETCODE_PACKET_QUEUE_SIZE];
+    uint64_t packet_sequence[NETCODE_PACKET_QUEUE_SIZE];
+};
+
+void netcode_packet_queue_init( struct netcode_packet_queue_t * queue, 
+                                void * allocator_context, 
+                                void * (*allocate_function)(void*,uint64_t), 
+                                void (*free_function)(void*,void*) )
+{
+    if ( allocate_function == NULL )
+    {
+        allocate_function = netcode_default_allocate_function;
+    }
+
+    if ( free_function == NULL )
+    {
+        free_function = netcode_default_free_function;
+    }
+
+    netcode_assert( queue );
+
+    queue->allocator_context = allocator_context;
+    queue->allocate_function = allocate_function;
+    queue->free_function = free_function;
+    queue->num_packets = 0;
+    queue->start_index = 0;
+    memset( queue->packet_data, 0, sizeof( queue->packet_data ) );
+    memset( queue->packet_sequence, 0, sizeof( queue->packet_sequence ) );
+}
+
+void netcode_packet_queue_clear( struct netcode_packet_queue_t * queue )
+{
+    int i;
+    for ( i = 0; i < queue->num_packets; ++i )
+    {
+        queue->free_function( queue->allocator_context, queue->packet_data[i] );
+    }
+    queue->num_packets = 0;
+    queue->start_index = 0;
+    memset( queue->packet_data, 0, sizeof( queue->packet_data ) );
+    memset( queue->packet_sequence, 0, sizeof( queue->packet_sequence ) );
+}
+
+int netcode_packet_queue_push( struct netcode_packet_queue_t * queue, void * packet_data, uint64_t packet_sequence )
+{
+    netcode_assert( queue );
+    netcode_assert( packet_data );
+    if ( queue->num_packets == NETCODE_PACKET_QUEUE_SIZE )
+    {
+        queue->free_function( queue->allocator_context, packet_data );
+        return 0;
+    }
+    int index = ( queue->start_index + queue->num_packets ) % NETCODE_PACKET_QUEUE_SIZE;
+    queue->packet_data[index] = packet_data;
+    queue->packet_sequence[index] = packet_sequence;
+    queue->num_packets++;
+    return 1;
+}
+
+void * netcode_packet_queue_pop( struct netcode_packet_queue_t * queue, uint64_t * packet_sequence )
+{
+    if ( queue->num_packets == 0 )
+        return NULL;
+    void * packet = queue->packet_data[queue->start_index];
+    if ( packet_sequence )
+        *packet_sequence = queue->packet_sequence[queue->start_index];
+    queue->start_index = ( queue->start_index + 1 ) % NETCODE_PACKET_QUEUE_SIZE;
+    queue->num_packets--;
+    return packet;
+}
+
+// ----------------------------------------------------------------
+
+#define NETCODE_NETWORK_SIMULATOR_NUM_PACKET_ENTRIES ( NETCODE_MAX_CLIENTS * 256 )
+#define NETCODE_NETWORK_SIMULATOR_NUM_PENDING_RECEIVE_PACKETS ( NETCODE_MAX_CLIENTS * 64 )
+
+struct netcode_network_simulator_packet_entry_t
+{
+    struct netcode_address_t from;
+    struct netcode_address_t to;
+    double delivery_time;
+    uint8_t * packet_data;
+    int packet_bytes;
+};
+
+struct netcode_network_simulator_t
+{
+    void * allocator_context;
+    void * (*allocate_function)(void*,uint64_t);
+    void (*free_function)(void*,void*);
+    float latency_milliseconds;
+    float jitter_milliseconds;
+    float packet_loss_percent;
+    float duplicate_packet_percent;
+    double time;
+    int current_index;
+    int num_pending_receive_packets;
+    struct netcode_network_simulator_packet_entry_t packet_entries[NETCODE_NETWORK_SIMULATOR_NUM_PACKET_ENTRIES];
+    struct netcode_network_simulator_packet_entry_t pending_receive_packets[NETCODE_NETWORK_SIMULATOR_NUM_PENDING_RECEIVE_PACKETS];
+};
+
+struct netcode_network_simulator_t * netcode_network_simulator_create( void * allocator_context, 
+                                                                       void * (*allocate_function)(void*,uint64_t), 
+                                                                       void (*free_function)(void*,void*) )
+{
+    if ( allocate_function == NULL )
+    {
+        allocate_function = netcode_default_allocate_function;
+    }
+
+    if ( free_function == NULL )
+    {
+        free_function = netcode_default_free_function;
+    }
+
+    struct netcode_network_simulator_t * network_simulator = (struct netcode_network_simulator_t*) 
+        allocate_function( allocator_context, sizeof( struct netcode_network_simulator_t ) );
+
+    netcode_assert( network_simulator );
+
+    memset( network_simulator, 0, sizeof( struct netcode_network_simulator_t ) );
+
+    network_simulator->allocator_context = allocator_context;
+    network_simulator->allocate_function = allocate_function;
+    network_simulator->free_function = free_function;
+
+    return network_simulator;
+}
+
+void netcode_network_simulator_reset( struct netcode_network_simulator_t * network_simulator )
+{
+    netcode_assert( network_simulator );
+
+    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "network simulator reset\n" );
+
+    int i;
+    for ( i = 0; i < NETCODE_NETWORK_SIMULATOR_NUM_PACKET_ENTRIES; ++i )
+    {
+        network_simulator->free_function( network_simulator->allocator_context, network_simulator->packet_entries[i].packet_data );
+        memset( &network_simulator->packet_entries[i], 0, sizeof( struct netcode_network_simulator_packet_entry_t ) );
+    }
+
+    for ( i = 0; i < network_simulator->num_pending_receive_packets; ++i )
+    {
+        network_simulator->free_function( network_simulator->allocator_context, network_simulator->pending_receive_packets[i].packet_data );
+        memset( &network_simulator->pending_receive_packets[i], 0, sizeof( struct netcode_network_simulator_packet_entry_t ) );
+    }
+
+    network_simulator->current_index = 0;
+    network_simulator->num_pending_receive_packets = 0;
+}
+
+void netcode_network_simulator_destroy( struct netcode_network_simulator_t * network_simulator )
+{
+    netcode_assert( network_simulator );
+    netcode_network_simulator_reset( network_simulator );
+    network_simulator->free_function( network_simulator->allocator_context, network_simulator );
+}
+
+float netcode_random_float( float a, float b )
+{
+    netcode_assert( a < b );
+    float random = ( (float) rand() ) / (float) RAND_MAX;
+    float diff = b - a;
+    float r = random * diff;
+    return a + r;
+}
+
+void netcode_network_simulator_queue_packet( struct netcode_network_simulator_t * network_simulator, 
+                                             struct netcode_address_t * from, 
+                                             struct netcode_address_t * to, 
+                                             uint8_t * packet_data, 
+                                             int packet_bytes, 
+                                             float delay )
+{
+    network_simulator->packet_entries[network_simulator->current_index].from = *from;
+    network_simulator->packet_entries[network_simulator->current_index].to = *to;
+    network_simulator->packet_entries[network_simulator->current_index].packet_data = 
+        (uint8_t*) network_simulator->allocate_function( network_simulator->allocator_context, packet_bytes );
+    memcpy( network_simulator->packet_entries[network_simulator->current_index].packet_data, packet_data, packet_bytes );
+    network_simulator->packet_entries[network_simulator->current_index].packet_bytes = packet_bytes;
+    network_simulator->packet_entries[network_simulator->current_index].delivery_time = network_simulator->time + delay;
+    network_simulator->current_index++;
+    network_simulator->current_index %= NETCODE_NETWORK_SIMULATOR_NUM_PACKET_ENTRIES;
+}
+
+void netcode_network_simulator_send_packet( struct netcode_network_simulator_t * network_simulator, 
+                                            struct netcode_address_t * from, 
+                                            struct netcode_address_t * to, 
+                                            uint8_t * packet_data, 
+                                            int packet_bytes )
+{
+    netcode_assert( network_simulator );
+    netcode_assert( from );
+    netcode_assert( from->type != 0 );
+    netcode_assert( to );
+    netcode_assert( to->type != 0 );
+    netcode_assert( packet_data );
+    netcode_assert( packet_bytes > 0 );
+    netcode_assert( packet_bytes <= NETCODE_MAX_PACKET_BYTES );
+
+    if ( netcode_random_float( 0.0f, 100.0f ) <= network_simulator->packet_loss_percent )
+        return;
+
+    if ( network_simulator->packet_entries[network_simulator->current_index].packet_data )
+    {
+        network_simulator->free_function( network_simulator->allocator_context, network_simulator->packet_entries[network_simulator->current_index].packet_data );
+        network_simulator->packet_entries[network_simulator->current_index].packet_data = NULL;
+    }
+
+    float delay = network_simulator->latency_milliseconds / 1000.0f;
+
+    if ( network_simulator->jitter_milliseconds > 0.0 )
+        delay += netcode_random_float( -network_simulator->jitter_milliseconds, +network_simulator->jitter_milliseconds ) / 1000.0f;
+
+    netcode_network_simulator_queue_packet( network_simulator, from, to, packet_data, packet_bytes, delay );
+
+    if ( netcode_random_float( 0.0f, 100.0f ) <= network_simulator->duplicate_packet_percent )
+    {
+        netcode_network_simulator_queue_packet( network_simulator, from, to, packet_data, packet_bytes, delay + netcode_random_float( 0, 1.0 ) );
+    }
+}
+
+int netcode_network_simulator_receive_packets( struct netcode_network_simulator_t * network_simulator, 
+                                               struct netcode_address_t * to, 
+                                               int max_packets, 
+                                               uint8_t ** packet_data, 
+                                               int * packet_bytes, 
+                                               struct netcode_address_t * from )
+{
+    netcode_assert( network_simulator );
+    netcode_assert( max_packets >= 0 );
+    netcode_assert( packet_data );
+    netcode_assert( packet_bytes );
+    netcode_assert( from );
+    netcode_assert( to );
+
+    int num_packets = 0;
+
+    int i;
+    for ( i = 0; i < network_simulator->num_pending_receive_packets; ++i )
+    {
+        if ( num_packets == max_packets )
+            break;
+
+        if ( !network_simulator->pending_receive_packets[i].packet_data )
+            continue;
+
+        if ( !netcode_address_equal( &network_simulator->pending_receive_packets[i].to, to ) )
+            continue;
+
+        packet_data[num_packets] = network_simulator->pending_receive_packets[i].packet_data;
+        packet_bytes[num_packets] = network_simulator->pending_receive_packets[i].packet_bytes;
+        from[num_packets] = network_simulator->pending_receive_packets[i].from;
+
+        network_simulator->pending_receive_packets[i].packet_data = NULL;
+
+        num_packets++;
+    }
+
+    netcode_assert( num_packets <= max_packets );
+
+    return num_packets;
+}
+
+void netcode_network_simulator_update( struct netcode_network_simulator_t * network_simulator, double time )
+{   
+    netcode_assert( network_simulator );
+
+    network_simulator->time = time;
+
+    // discard any pending receive packets that are still in the buffer
+
+    int i;
+    for ( i = 0; i < network_simulator->num_pending_receive_packets; ++i )
+    {
+        if ( network_simulator->pending_receive_packets[i].packet_data )
+        {
+            network_simulator->free_function( network_simulator->allocator_context, network_simulator->pending_receive_packets[i].packet_data );
+            network_simulator->pending_receive_packets[i].packet_data = NULL;
+        }
+    }
+
+    network_simulator->num_pending_receive_packets = 0;
+
+    // walk across packet entries and move any that are ready to be received into the pending receive buffer
+
+    for ( i = 0; i < NETCODE_NETWORK_SIMULATOR_NUM_PACKET_ENTRIES; ++i )
+    {
+        if ( !network_simulator->packet_entries[i].packet_data )
+            continue;
+
+        if ( network_simulator->num_pending_receive_packets == NETCODE_NETWORK_SIMULATOR_NUM_PENDING_RECEIVE_PACKETS )
+            break;
+
+        if ( network_simulator->packet_entries[i].packet_data && network_simulator->packet_entries[i].delivery_time <= time )
+        {
+            network_simulator->pending_receive_packets[network_simulator->num_pending_receive_packets] = network_simulator->packet_entries[i];
+            network_simulator->num_pending_receive_packets++;
+            network_simulator->packet_entries[i].packet_data = NULL;
+        }
+    }
+}
+
+// ----------------------------------------------------------------
+    
+NETCODE_CONST char * netcode_client_state_name( int client_state )
+{
+    switch ( client_state )
+    {
+        case NETCODE_CLIENT_STATE_CONNECT_TOKEN_EXPIRED:                return "connect token expired";
+        case NETCODE_CLIENT_STATE_INVALID_CONNECT_TOKEN:                return "invalid connect token";
+        case NETCODE_CLIENT_STATE_CONNECTION_TIMED_OUT:                 return "connection timed out";
+        case NETCODE_CLIENT_STATE_CONNECTION_REQUEST_TIMED_OUT:         return "connection request timed out";
+        case NETCODE_CLIENT_STATE_CONNECTION_RESPONSE_TIMED_OUT:        return "connection response timed out";
+        case NETCODE_CLIENT_STATE_CONNECTION_DENIED:                    return "connection denied";
+        case NETCODE_CLIENT_STATE_DISCONNECTED:                         return "disconnected";
+        case NETCODE_CLIENT_STATE_SENDING_CONNECTION_REQUEST:           return "sending connection request";
+        case NETCODE_CLIENT_STATE_SENDING_CONNECTION_RESPONSE:          return "sending connection response";
+        case NETCODE_CLIENT_STATE_CONNECTED:                            return "connected";
+        default:
+            netcode_assert( 0 );
+            return "???";
+    }
+}
+
+void netcode_default_client_config( struct netcode_client_config_t * config )
+{
+    netcode_assert( config );
+    config->allocator_context = NULL;
+    config->allocate_function = netcode_default_allocate_function;
+    config->free_function = netcode_default_free_function;
+    config->network_simulator = NULL;
+    config->callback_context = NULL;
+    config->state_change_callback = NULL;
+    config->send_loopback_packet_callback = NULL;
+    config->override_send_and_receive = 0;
+    config->send_packet_override = NULL;
+    config->receive_packet_override = NULL;
+};
+
+struct netcode_client_t
+{
+    struct netcode_client_config_t config;
+    int state;
+    double time;
+    double connect_start_time;
+    double last_packet_send_time;
+    double last_packet_receive_time;
+    int should_disconnect;
+    int should_disconnect_state;
+    uint64_t sequence;
+    int client_index;
+    int max_clients;
+    int server_address_index;
+    struct netcode_address_t address;
+    struct netcode_address_t server_address;
+    struct netcode_connect_token_t connect_token;
+    struct netcode_socket_holder_t socket_holder;
+    struct netcode_context_t context;
+    struct netcode_replay_protection_t replay_protection;
+    struct netcode_packet_queue_t packet_receive_queue;
+    uint64_t challenge_token_sequence;
+    uint8_t challenge_token_data[NETCODE_CHALLENGE_TOKEN_BYTES];
+    uint8_t * receive_packet_data[NETCODE_CLIENT_MAX_RECEIVE_PACKETS];
+    int receive_packet_bytes[NETCODE_CLIENT_MAX_RECEIVE_PACKETS];
+    struct netcode_address_t receive_from[NETCODE_CLIENT_MAX_RECEIVE_PACKETS];
+    int loopback;
+};
+
+int netcode_client_socket_create( struct netcode_socket_t * socket,
+                                  struct netcode_address_t * address,
+                                  int send_buffer_size,
+                                  int receive_buffer_size,
+                                  NETCODE_CONST struct netcode_client_config_t * config )
+{
+    netcode_assert( socket );
+    netcode_assert( address );
+    netcode_assert( config );
+
+    if ( !config->network_simulator )
+    {
+        if ( !config->override_send_and_receive )
+        {
+            if ( netcode_socket_create( socket, address, send_buffer_size, receive_buffer_size ) != NETCODE_SOCKET_ERROR_NONE )
+            {
+                return 0;
+            }
+        }
+    }
+    else
+    {
+        if ( address->port == 0 )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: must bind to a specific port when using network simulator\n" );
+            return 0;
+        }
+    }
+
+    return 1;
+}
+
+struct netcode_client_t * netcode_client_create_overload( NETCODE_CONST char * address1_string,
+                                                          NETCODE_CONST char * address2_string,
+                                                          NETCODE_CONST struct netcode_client_config_t * config,
+                                                          double time )
+{
+    netcode_assert( config );
+    netcode_assert( netcode.initialized );
+
+    struct netcode_address_t address1;
+    struct netcode_address_t address2;
+
+    memset( &address1, 0, sizeof( address1 ) );
+    memset( &address2, 0, sizeof( address2 ) );
+
+    if ( netcode_parse_address( address1_string, &address1 ) != NETCODE_OK )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to parse client address\n" );
+        return NULL;
+    }
+
+    if ( address2_string != NULL && netcode_parse_address( address2_string, &address2 ) != NETCODE_OK )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to parse client address2\n" );
+        return NULL;
+    }
+
+
+    struct netcode_socket_t socket_ipv4;
+    struct netcode_socket_t socket_ipv6;
+
+    memset( &socket_ipv4, 0, sizeof( socket_ipv4 ) );
+    memset( &socket_ipv6, 0, sizeof( socket_ipv6 ) );
+
+    if ( address1.type == NETCODE_ADDRESS_IPV4 || address2.type == NETCODE_ADDRESS_IPV4 )
+    {
+        if ( !netcode_client_socket_create( &socket_ipv4, address1.type == NETCODE_ADDRESS_IPV4 ? &address1 : &address2, NETCODE_CLIENT_SOCKET_SNDBUF_SIZE, NETCODE_CLIENT_SOCKET_RCVBUF_SIZE, config ) )
+        {
+            return NULL;
+        }
+    }
+
+    if ( address1.type == NETCODE_ADDRESS_IPV6 || address2.type == NETCODE_ADDRESS_IPV6 )
+    {
+        if ( !netcode_client_socket_create( &socket_ipv6, address1.type == NETCODE_ADDRESS_IPV6 ? &address1 : &address2, NETCODE_CLIENT_SOCKET_SNDBUF_SIZE, NETCODE_CLIENT_SOCKET_RCVBUF_SIZE, config ) )
+        {
+            return NULL;
+        }
+    }
+
+    struct netcode_client_t * client = (struct netcode_client_t*) config->allocate_function( config->allocator_context, sizeof( struct netcode_client_t ) );
+
+    if ( !client )
+    {
+        netcode_socket_destroy( &socket_ipv4 );
+        netcode_socket_destroy( &socket_ipv6 );
+        return NULL;
+    }
+
+    struct netcode_address_t socket_address = address1.type == NETCODE_ADDRESS_IPV4 ? socket_ipv4.address : socket_ipv6.address;
+
+    if ( !config->network_simulator )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_INFO, "client started on port %d\n", socket_address.port );
+    }
+    else
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_INFO, "client started on port %d (network simulator)\n", socket_address.port );
+    }
+
+    client->config = *config;
+    client->socket_holder.ipv4 = socket_ipv4;
+    client->socket_holder.ipv6 = socket_ipv6;
+    client->address = config->network_simulator ? address1 : socket_address;
+    client->state = NETCODE_CLIENT_STATE_DISCONNECTED;
+    client->time = time;
+    client->connect_start_time = 0.0;
+    client->last_packet_send_time = -1000.0;
+    client->last_packet_receive_time = -1000.0;
+    client->should_disconnect = 0;
+    client->should_disconnect_state = NETCODE_CLIENT_STATE_DISCONNECTED;
+    client->sequence = 0;
+    client->client_index = 0;
+    client->max_clients = 0;
+    client->server_address_index = 0;
+    client->challenge_token_sequence = 0;
+    client->loopback = 0;
+    memset( &client->server_address, 0, sizeof( struct netcode_address_t ) );
+    memset( &client->connect_token, 0, sizeof( struct netcode_connect_token_t ) );
+    memset( &client->context, 0, sizeof( struct netcode_context_t ) );
+    memset( client->challenge_token_data, 0, NETCODE_CHALLENGE_TOKEN_BYTES );
+
+    netcode_packet_queue_init( &client->packet_receive_queue, config->allocator_context, config->allocate_function, config->free_function );
+
+    netcode_replay_protection_reset( &client->replay_protection );
+
+    return client;
+}
+
+struct netcode_client_t * netcode_client_create( NETCODE_CONST char * address,
+                                                 NETCODE_CONST struct netcode_client_config_t * config,
+                                                 double time )
+{
+    return netcode_client_create_overload( address, NULL, config, time );
+}
+
+void netcode_client_destroy( struct netcode_client_t * client )
+{
+    netcode_assert( client );
+    if ( !client->loopback )
+        netcode_client_disconnect( client );
+    else
+        netcode_client_disconnect_loopback( client );
+    netcode_socket_destroy( &client->socket_holder.ipv4 );
+    netcode_socket_destroy( &client->socket_holder.ipv6 );
+    netcode_packet_queue_clear( &client->packet_receive_queue );
+    client->config.free_function( client->config.allocator_context, client );
+}
+
+void netcode_client_set_state( struct netcode_client_t * client, int client_state )
+{
+    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client changed state from '%s' to '%s'\n", 
+        netcode_client_state_name( client->state ), netcode_client_state_name( client_state ) );
+
+    if ( client->config.state_change_callback )
+    {
+        client->config.state_change_callback( client->config.callback_context, client->state, client_state );
+    }
+
+    client->state = client_state;
+}
+
+void netcode_client_reset_before_next_connect( struct netcode_client_t * client )
+{
+    client->connect_start_time = client->time;
+    client->last_packet_send_time = client->time - 1.0f;
+    client->last_packet_receive_time = client->time;
+    client->should_disconnect = 0;
+    client->should_disconnect_state = NETCODE_CLIENT_STATE_DISCONNECTED;
+    client->challenge_token_sequence = 0;
+
+    memset( client->challenge_token_data, 0, NETCODE_CHALLENGE_TOKEN_BYTES );
+
+    netcode_replay_protection_reset( &client->replay_protection );
+}
+
+void netcode_client_reset_connection_data( struct netcode_client_t * client, int client_state )
+{
+    netcode_assert( client );
+
+    client->sequence = 0;
+    client->loopback = 0;
+    client->client_index = 0;
+    client->max_clients = 0;
+    client->connect_start_time = 0.0;
+    client->server_address_index = 0;
+    memset( &client->server_address, 0, sizeof( struct netcode_address_t ) );
+    memset( &client->connect_token, 0, sizeof( struct netcode_connect_token_t ) );
+    memset( &client->context, 0, sizeof( struct netcode_context_t ) );
+
+    netcode_client_set_state( client, client_state );
+
+    netcode_client_reset_before_next_connect( client );
+
+    while ( 1 )
+    {
+        void * packet = netcode_packet_queue_pop( &client->packet_receive_queue, NULL );
+        if ( !packet )
+            break;
+        client->config.free_function( client->config.allocator_context, packet );
+    }
+
+    netcode_packet_queue_clear( &client->packet_receive_queue );
+}
+
+void netcode_client_disconnect_internal( struct netcode_client_t * client, int destination_state, int send_disconnect_packets );
+
+void netcode_client_connect( struct netcode_client_t * client, uint8_t * connect_token )
+{
+    netcode_assert( client );
+    netcode_assert( connect_token );
+
+    netcode_client_disconnect( client );
+
+    if ( netcode_read_connect_token( connect_token, NETCODE_CONNECT_TOKEN_BYTES, &client->connect_token ) != NETCODE_OK )
+    {
+        netcode_client_set_state( client, NETCODE_CLIENT_STATE_INVALID_CONNECT_TOKEN );
+        return;
+    }
+
+    client->server_address_index = 0;
+    client->server_address = client->connect_token.server_addresses[0];
+
+    char server_address_string[NETCODE_MAX_ADDRESS_STRING_LENGTH];
+
+    netcode_printf( NETCODE_LOG_LEVEL_INFO, "client connecting to server %s [%d/%d]\n", 
+        netcode_address_to_string( &client->server_address, server_address_string ), client->server_address_index + 1, client->connect_token.num_server_addresses );
+
+    memcpy( client->context.read_packet_key, client->connect_token.server_to_client_key, NETCODE_KEY_BYTES );
+    memcpy( client->context.write_packet_key, client->connect_token.client_to_server_key, NETCODE_KEY_BYTES );
+
+    netcode_client_reset_before_next_connect( client );
+
+    netcode_client_set_state( client, NETCODE_CLIENT_STATE_SENDING_CONNECTION_REQUEST );
+}
+
+void netcode_client_process_packet_internal( struct netcode_client_t * client, struct netcode_address_t * from, uint8_t * packet, uint64_t sequence )
+{
+    netcode_assert( client );
+    netcode_assert( packet );
+
+    uint8_t packet_type = ( (uint8_t*) packet ) [0];
+
+    switch ( packet_type )
+    {
+        case NETCODE_CONNECTION_DENIED_PACKET:
+        {
+            if ( ( client->state == NETCODE_CLIENT_STATE_SENDING_CONNECTION_REQUEST || 
+                   client->state == NETCODE_CLIENT_STATE_SENDING_CONNECTION_RESPONSE ) 
+                                                && 
+                      netcode_address_equal( from, &client->server_address ) )
+            {
+                client->should_disconnect = 1;
+                client->should_disconnect_state = NETCODE_CLIENT_STATE_CONNECTION_DENIED;
+                client->last_packet_receive_time = client->time;
+            }
+        }
+        break;
+
+        case NETCODE_CONNECTION_CHALLENGE_PACKET:
+        {
+            if ( client->state == NETCODE_CLIENT_STATE_SENDING_CONNECTION_REQUEST && netcode_address_equal( from, &client->server_address ) )
+            {
+                netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client received connection challenge packet from server\n" );
+
+                struct netcode_connection_challenge_packet_t * p = (struct netcode_connection_challenge_packet_t*) packet;
+                client->challenge_token_sequence = p->challenge_token_sequence;
+                memcpy( client->challenge_token_data, p->challenge_token_data, NETCODE_CHALLENGE_TOKEN_BYTES );
+                client->last_packet_receive_time = client->time;
+
+                netcode_client_set_state( client, NETCODE_CLIENT_STATE_SENDING_CONNECTION_RESPONSE );
+            }
+        }
+        break;
+
+        case NETCODE_CONNECTION_KEEP_ALIVE_PACKET:
+        {
+            if ( netcode_address_equal( from, &client->server_address ) )
+            {
+                struct netcode_connection_keep_alive_packet_t * p = (struct netcode_connection_keep_alive_packet_t*) packet;
+
+                if ( client->state == NETCODE_CLIENT_STATE_CONNECTED )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client received connection keep alive packet from server\n" );
+
+                    client->last_packet_receive_time = client->time;
+                }
+                else if ( client->state == NETCODE_CLIENT_STATE_SENDING_CONNECTION_RESPONSE )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client received connection keep alive packet from server\n" );
+
+                    client->last_packet_receive_time = client->time;
+                    client->client_index = p->client_index;
+                    client->max_clients = p->max_clients;
+
+                    netcode_client_set_state( client, NETCODE_CLIENT_STATE_CONNECTED );
+
+                    netcode_printf( NETCODE_LOG_LEVEL_INFO, "client connected to server\n" );
+                }
+            }
+        }
+        break;
+
+        case NETCODE_CONNECTION_PAYLOAD_PACKET:
+        {
+            if ( client->state == NETCODE_CLIENT_STATE_CONNECTED && netcode_address_equal( from, &client->server_address ) )
+            {
+                netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client received connection payload packet from server\n" );
+
+                netcode_packet_queue_push( &client->packet_receive_queue, packet, sequence );
+
+                client->last_packet_receive_time = client->time;
+
+                return;
+            }
+        }
+        break;
+
+        case NETCODE_CONNECTION_DISCONNECT_PACKET:
+        {
+            if ( client->state == NETCODE_CLIENT_STATE_CONNECTED && netcode_address_equal( from, &client->server_address ) )
+            {
+                netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client received disconnect packet from server\n" );
+
+                client->should_disconnect = 1;
+                client->should_disconnect_state = NETCODE_CLIENT_STATE_DISCONNECTED;
+                client->last_packet_receive_time = client->time;
+            }
+        }
+        break;
+
+        default:
+            break;
+    }
+
+    client->config.free_function( client->config.allocator_context, packet );    
+}
+
+void netcode_client_process_packet( struct netcode_client_t * client, struct netcode_address_t * from, uint8_t * packet_data, int packet_bytes )
+{
+    (void) client;
+    (void) from;
+    (void) packet_data;
+    (void) packet_bytes;
+
+    uint8_t allowed_packets[NETCODE_CONNECTION_NUM_PACKETS];
+    memset( allowed_packets, 0, sizeof( allowed_packets ) );
+    allowed_packets[NETCODE_CONNECTION_DENIED_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_CHALLENGE_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_KEEP_ALIVE_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_PAYLOAD_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_DISCONNECT_PACKET] = 1;
+
+    uint64_t current_timestamp = (uint64_t) time( NULL );
+
+    uint64_t sequence;
+
+    void * packet = netcode_read_packet( packet_data, 
+                                         packet_bytes, 
+                                         &sequence, 
+                                         client->context.read_packet_key, 
+                                         client->connect_token.protocol_id, 
+                                         current_timestamp, 
+                                         NULL, 
+                                         allowed_packets, 
+                                         &client->replay_protection, 
+                                         client->config.allocator_context, 
+                                         client->config.allocate_function );
+
+    if ( !packet )
+        return;
+    
+    netcode_client_process_packet_internal( client, from, (uint8_t*)packet, sequence );
+}
+
+void netcode_client_receive_packets( struct netcode_client_t * client )
+{
+    netcode_assert( client );
+    netcode_assert( !client->loopback );
+
+    uint8_t allowed_packets[NETCODE_CONNECTION_NUM_PACKETS];
+    memset( allowed_packets, 0, sizeof( allowed_packets ) );
+    allowed_packets[NETCODE_CONNECTION_DENIED_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_CHALLENGE_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_KEEP_ALIVE_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_PAYLOAD_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_DISCONNECT_PACKET] = 1;
+
+    uint64_t current_timestamp = (uint64_t) time( NULL );
+
+    if ( !client->config.network_simulator )
+    {
+        // process packets received from socket
+
+        while ( 1 )
+        {
+            struct netcode_address_t from;
+            uint8_t packet_data[NETCODE_MAX_PACKET_BYTES];
+            int packet_bytes = 0;
+
+            if ( client->config.override_send_and_receive )
+            {
+                packet_bytes = client->config.receive_packet_override( client->config.callback_context, &from, packet_data, NETCODE_MAX_PACKET_BYTES );
+            }
+            else if ( client->server_address.type == NETCODE_ADDRESS_IPV4 )
+            {
+                packet_bytes = netcode_socket_receive_packet( &client->socket_holder.ipv4, &from, packet_data, NETCODE_MAX_PACKET_BYTES );
+            }
+            else if ( client->server_address.type == NETCODE_ADDRESS_IPV6 )
+            {
+                packet_bytes = netcode_socket_receive_packet( &client->socket_holder.ipv6, &from, packet_data, NETCODE_MAX_PACKET_BYTES );
+            }
+
+            if ( packet_bytes == 0 )
+                break;
+
+            uint64_t sequence;
+            void * packet = netcode_read_packet( packet_data, 
+                                                 packet_bytes, 
+                                                 &sequence, 
+                                                 client->context.read_packet_key, 
+                                                 client->connect_token.protocol_id, 
+                                                 current_timestamp, 
+                                                 NULL, 
+                                                 allowed_packets, 
+                                                 &client->replay_protection, 
+                                                 client->config.allocator_context, 
+                                                 client->config.allocate_function );
+
+            if ( !packet )
+                continue;
+
+            netcode_client_process_packet_internal( client, &from, (uint8_t*)packet, sequence );
+        }
+    }
+    else
+    {
+        // process packets received from network simulator
+
+        int num_packets_received = netcode_network_simulator_receive_packets( client->config.network_simulator, 
+                                                                              &client->address, 
+                                                                              NETCODE_CLIENT_MAX_RECEIVE_PACKETS, 
+                                                                              client->receive_packet_data, 
+                                                                              client->receive_packet_bytes, 
+                                                                              client->receive_from );
+
+        int i;
+        for ( i = 0; i < num_packets_received; ++i )
+        {
+            uint64_t sequence;
+
+            void * packet = netcode_read_packet( client->receive_packet_data[i], 
+                                                 client->receive_packet_bytes[i], 
+                                                 &sequence, 
+                                                 client->context.read_packet_key, 
+                                                 client->connect_token.protocol_id, 
+                                                 current_timestamp, 
+                                                 NULL, 
+                                                 allowed_packets, 
+                                                 &client->replay_protection, 
+                                                 client->config.allocator_context, 
+                                                 client->config.allocate_function );
+
+            client->config.free_function( client->config.allocator_context, client->receive_packet_data[i] );
+
+            if ( !packet )
+                continue;
+
+            netcode_client_process_packet_internal( client, &client->receive_from[i], (uint8_t*)packet, sequence );
+        }
+    }
+}
+
+void netcode_client_send_packet_to_server_internal( struct netcode_client_t * client, void * packet )
+{
+    netcode_assert( client );
+    netcode_assert( !client->loopback );
+    
+    uint8_t packet_data[NETCODE_MAX_PACKET_BYTES];
+
+    int packet_bytes = netcode_write_packet( packet, 
+                                             packet_data, 
+                                             NETCODE_MAX_PACKET_BYTES, 
+                                             client->sequence++, 
+                                             client->context.write_packet_key, 
+                                             client->connect_token.protocol_id );
+
+    netcode_assert( packet_bytes <= NETCODE_MAX_PACKET_BYTES );
+
+    if ( client->config.network_simulator )
+    {
+        netcode_network_simulator_send_packet( client->config.network_simulator, &client->address, &client->server_address, packet_data, packet_bytes );
+    }
+    else
+    {
+        if ( client->config.override_send_and_receive )
+        {
+            client->config.send_packet_override( client->config.callback_context, &client->server_address, packet_data, packet_bytes );
+        }
+        else if ( client->server_address.type == NETCODE_ADDRESS_IPV4 )
+        {
+            netcode_socket_send_packet( &client->socket_holder.ipv4, &client->server_address, packet_data, packet_bytes );
+        }
+        else if ( client->server_address.type == NETCODE_ADDRESS_IPV6 )
+        {
+            netcode_socket_send_packet( &client->socket_holder.ipv6, &client->server_address, packet_data, packet_bytes );
+        }
+    }
+
+    client->last_packet_send_time = client->time;
+}
+
+void netcode_client_send_packets( struct netcode_client_t * client )
+{
+    netcode_assert( client );
+    netcode_assert( !client->loopback );
+
+    switch ( client->state )
+    {
+        case NETCODE_CLIENT_STATE_SENDING_CONNECTION_REQUEST:
+        {
+            if ( client->last_packet_send_time + ( 1.0 / NETCODE_PACKET_SEND_RATE ) >= client->time )
+                return;
+
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client sent connection request packet to server\n" );
+
+            struct netcode_connection_request_packet_t packet;
+            packet.packet_type = NETCODE_CONNECTION_REQUEST_PACKET;
+            memcpy( packet.version_info, NETCODE_VERSION_INFO, NETCODE_VERSION_INFO_BYTES );
+            packet.protocol_id = client->connect_token.protocol_id;
+            packet.connect_token_expire_timestamp = client->connect_token.expire_timestamp;
+            memcpy( packet.connect_token_nonce, client->connect_token.nonce, NETCODE_CONNECT_TOKEN_NONCE_BYTES );
+            memcpy( packet.connect_token_data, client->connect_token.private_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+            netcode_client_send_packet_to_server_internal( client, &packet );
+        }
+        break;
+
+        case NETCODE_CLIENT_STATE_SENDING_CONNECTION_RESPONSE:
+        {
+            if ( client->last_packet_send_time + ( 1.0 / NETCODE_PACKET_SEND_RATE ) >= client->time )
+                return;
+
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client sent connection response packet to server\n" );
+
+            struct netcode_connection_response_packet_t packet;
+            packet.packet_type = NETCODE_CONNECTION_RESPONSE_PACKET;
+            packet.challenge_token_sequence = client->challenge_token_sequence;
+            memcpy( packet.challenge_token_data, client->challenge_token_data, NETCODE_CHALLENGE_TOKEN_BYTES );
+
+            netcode_client_send_packet_to_server_internal( client, &packet );
+        }
+        break;
+
+        case NETCODE_CLIENT_STATE_CONNECTED:
+        {
+            if ( client->last_packet_send_time + ( 1.0 / NETCODE_PACKET_SEND_RATE ) >= client->time )
+                return;
+
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client sent connection keep-alive packet to server\n" );
+
+            struct netcode_connection_keep_alive_packet_t packet;
+            packet.packet_type = NETCODE_CONNECTION_KEEP_ALIVE_PACKET;
+            packet.client_index = 0;
+            packet.max_clients = 0;
+
+            netcode_client_send_packet_to_server_internal( client, &packet );
+        }
+        break;
+        
+        default:
+            break;
+    }
+}
+
+int netcode_client_connect_to_next_server( struct netcode_client_t * client )
+{
+    netcode_assert( client );
+
+    if ( client->server_address_index + 1 >= client->connect_token.num_server_addresses )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client has no more servers to connect to\n" );
+        return 0;
+    }
+
+    client->server_address_index++;
+    client->server_address = client->connect_token.server_addresses[client->server_address_index];
+
+    netcode_client_reset_before_next_connect( client );
+
+    char server_address_string[NETCODE_MAX_ADDRESS_STRING_LENGTH];
+
+    netcode_printf( NETCODE_LOG_LEVEL_INFO, "client connecting to next server %s [%d/%d]\n", 
+        netcode_address_to_string( &client->server_address, server_address_string ), 
+        client->server_address_index + 1, 
+        client->connect_token.num_server_addresses );
+
+    netcode_client_set_state( client, NETCODE_CLIENT_STATE_SENDING_CONNECTION_REQUEST );
+
+    return 1;
+}
+
+void netcode_client_update( struct netcode_client_t * client, double time )
+{
+    netcode_assert( client );
+
+    client->time = time;
+
+    if ( client->loopback )
+        return;
+
+    netcode_client_receive_packets( client );
+
+    netcode_client_send_packets( client );
+
+    if ( client->state > NETCODE_CLIENT_STATE_DISCONNECTED && client->state < NETCODE_CLIENT_STATE_CONNECTED )
+    {
+        uint64_t connect_token_expire_seconds = ( client->connect_token.expire_timestamp - client->connect_token.create_timestamp );            
+        if ( client->time - client->connect_start_time >= connect_token_expire_seconds )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_INFO, "client connect failed. connect token expired\n" );
+            netcode_client_disconnect_internal( client, NETCODE_CLIENT_STATE_CONNECT_TOKEN_EXPIRED, 0 );
+            return;
+        }
+    }
+
+    if ( client->should_disconnect )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client should disconnect -> %s\n", netcode_client_state_name( client->should_disconnect_state ) );
+        if ( netcode_client_connect_to_next_server( client ) )
+            return;
+        netcode_client_disconnect_internal( client, client->should_disconnect_state, 0 );
+        return;
+    }
+
+    switch ( client->state )
+    {
+        case NETCODE_CLIENT_STATE_SENDING_CONNECTION_REQUEST:
+        {
+            if ( client->connect_token.timeout_seconds > 0 && client->last_packet_receive_time + client->connect_token.timeout_seconds < time )
+            {
+                netcode_printf( NETCODE_LOG_LEVEL_INFO, "client connect failed. connection request timed out\n" );
+                if ( netcode_client_connect_to_next_server( client ) )
+                    return;
+                netcode_client_disconnect_internal( client, NETCODE_CLIENT_STATE_CONNECTION_REQUEST_TIMED_OUT, 0 );
+                return;
+            }
+        }
+        break;
+
+        case NETCODE_CLIENT_STATE_SENDING_CONNECTION_RESPONSE:
+        {
+            if ( client->connect_token.timeout_seconds > 0 && client->last_packet_receive_time + client->connect_token.timeout_seconds < time )
+            {
+                netcode_printf( NETCODE_LOG_LEVEL_INFO, "client connect failed. connection response timed out\n" );
+                if ( netcode_client_connect_to_next_server( client ) )
+                    return;
+                netcode_client_disconnect_internal( client, NETCODE_CLIENT_STATE_CONNECTION_RESPONSE_TIMED_OUT, 0 );
+                return;
+            }
+        }
+        break;
+
+        case NETCODE_CLIENT_STATE_CONNECTED:
+        {
+            if ( client->connect_token.timeout_seconds > 0 && client->last_packet_receive_time + client->connect_token.timeout_seconds < time )
+            {
+                netcode_printf( NETCODE_LOG_LEVEL_INFO, "client connection timed out\n" );
+                netcode_client_disconnect_internal( client, NETCODE_CLIENT_STATE_CONNECTION_TIMED_OUT, 0 );
+                return;
+            }
+        }
+        break;
+
+        default:
+            break;
+    }
+}
+
+uint64_t netcode_client_next_packet_sequence( struct netcode_client_t * client )
+{
+    netcode_assert( client );
+    return client->sequence;  
+}
+
+void netcode_client_send_packet( struct netcode_client_t * client, NETCODE_CONST uint8_t * packet_data, int packet_bytes )
+{
+    netcode_assert( client );
+    netcode_assert( packet_data );
+    netcode_assert( packet_bytes >= 0 );
+    netcode_assert( packet_bytes <= NETCODE_MAX_PACKET_SIZE );
+
+    if ( client->state != NETCODE_CLIENT_STATE_CONNECTED )
+        return;
+
+    if ( !client->loopback )
+    {
+        uint8_t buffer[NETCODE_MAX_PAYLOAD_BYTES*2];
+
+        struct netcode_connection_payload_packet_t * packet = (struct netcode_connection_payload_packet_t*) buffer;
+
+        packet->packet_type = NETCODE_CONNECTION_PAYLOAD_PACKET;
+        packet->payload_bytes = packet_bytes;
+        memcpy( packet->payload_data, packet_data, packet_bytes );
+
+        netcode_client_send_packet_to_server_internal( client, packet );
+    }
+    else
+    {
+        client->config.send_loopback_packet_callback( client->config.callback_context, 
+                                                      client->client_index, 
+                                                      packet_data,
+                                                      packet_bytes,
+                                                      client->sequence++ );
+    }
+}
+
+uint8_t * netcode_client_receive_packet( struct netcode_client_t * client, int * packet_bytes, uint64_t * packet_sequence )
+{
+    netcode_assert( client );
+    netcode_assert( packet_bytes );
+
+    struct netcode_connection_payload_packet_t * packet = (struct netcode_connection_payload_packet_t*) 
+        netcode_packet_queue_pop( &client->packet_receive_queue, packet_sequence );
+    
+    if ( packet )
+    {
+        netcode_assert( packet->packet_type == NETCODE_CONNECTION_PAYLOAD_PACKET );
+        *packet_bytes = packet->payload_bytes;
+        netcode_assert( *packet_bytes >= 0 );
+        netcode_assert( *packet_bytes <= NETCODE_MAX_PAYLOAD_BYTES );
+        return (uint8_t*) &packet->payload_data;
+    }
+    else
+    {
+        return NULL;
+    }
+}
+
+void netcode_client_free_packet( struct netcode_client_t * client, void * packet )
+{
+    netcode_assert( client );
+    netcode_assert( packet );
+    uint8_t * packet_data = (uint8_t*) packet;
+    int offset = offsetof( struct netcode_connection_payload_packet_t, payload_data );
+    client->config.free_function( client->config.allocator_context, packet_data - offset );
+}
+
+void netcode_client_disconnect( struct netcode_client_t * client )
+{
+    netcode_assert( client );
+    netcode_assert( !client->loopback );
+    netcode_client_disconnect_internal( client, NETCODE_CLIENT_STATE_DISCONNECTED, 1 );
+}
+
+void netcode_client_disconnect_internal( struct netcode_client_t * client, int destination_state, int send_disconnect_packets )
+{
+    netcode_assert( !client->loopback );
+    netcode_assert( destination_state <= NETCODE_CLIENT_STATE_DISCONNECTED );
+
+    if ( client->state <= NETCODE_CLIENT_STATE_DISCONNECTED || client->state == destination_state )
+        return;
+
+    netcode_printf( NETCODE_LOG_LEVEL_INFO, "client disconnected\n" );
+
+    if ( !client->loopback && send_disconnect_packets && client->state > NETCODE_CLIENT_STATE_DISCONNECTED )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client sent disconnect packets to server\n" );
+
+        int i;
+        for ( i = 0; i < NETCODE_NUM_DISCONNECT_PACKETS; ++i )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client sent disconnect packet %d\n", i );
+
+            struct netcode_connection_disconnect_packet_t packet;
+            packet.packet_type = NETCODE_CONNECTION_DISCONNECT_PACKET;
+
+            netcode_client_send_packet_to_server_internal( client, &packet );
+        }
+    }
+
+    netcode_client_reset_connection_data( client, destination_state );
+}
+
+int netcode_client_state( struct netcode_client_t * client )
+{
+    netcode_assert( client );
+    return client->state;
+}
+
+int netcode_client_index( struct netcode_client_t * client )
+{
+    netcode_assert( client );
+    return client->client_index;
+}
+
+int netcode_client_max_clients( struct netcode_client_t * client )
+{   
+    netcode_assert( client );
+    return client->max_clients;
+}
+
+void netcode_client_connect_loopback( struct netcode_client_t * client, int client_index, int max_clients )
+{
+    netcode_assert( client );
+    netcode_assert( client->state <= NETCODE_CLIENT_STATE_DISCONNECTED );
+    netcode_printf( NETCODE_LOG_LEVEL_INFO, "client connected to server via loopback as client %d\n", client_index );
+    client->state = NETCODE_CLIENT_STATE_CONNECTED;
+    client->client_index = client_index;
+    client->max_clients = max_clients;
+    client->loopback = 1;
+}
+
+void netcode_client_disconnect_loopback( struct netcode_client_t * client )
+{
+    netcode_assert( client );
+    netcode_assert( client->loopback );
+    netcode_client_reset_connection_data( client, NETCODE_CLIENT_STATE_DISCONNECTED );
+}
+
+int netcode_client_loopback( struct netcode_client_t * client )
+{
+    netcode_assert( client );
+    return client->loopback;
+}
+
+void netcode_client_process_loopback_packet( struct netcode_client_t * client, NETCODE_CONST uint8_t * packet_data, int packet_bytes, uint64_t packet_sequence )
+{
+    netcode_assert( client );
+    netcode_assert( client->loopback );
+    struct netcode_connection_payload_packet_t * packet = netcode_create_payload_packet( packet_bytes, client->config.allocator_context, client->config.allocate_function );
+    if ( !packet )
+        return;
+    memcpy( packet->payload_data, packet_data, packet_bytes );
+    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "client processing loopback packet from server\n" );
+    netcode_packet_queue_push( &client->packet_receive_queue, packet, packet_sequence );
+}
+
+uint16_t netcode_client_get_port( struct netcode_client_t * client )
+{
+    netcode_assert( client );
+    return client->address.type == NETCODE_ADDRESS_IPV4 ? client->socket_holder.ipv4.address.port : client->socket_holder.ipv6.address.port;
+}
+
+struct netcode_address_t * netcode_client_server_address( struct netcode_client_t * client )
+{
+    netcode_assert( client );
+    return &client->server_address;
+}
+
+// ----------------------------------------------------------------
+
+#define NETCODE_MAX_ENCRYPTION_MAPPINGS ( NETCODE_MAX_CLIENTS * 4 )
+
+struct netcode_encryption_manager_t
+{
+    int num_encryption_mappings;
+    int timeout[NETCODE_MAX_ENCRYPTION_MAPPINGS];
+    double expire_time[NETCODE_MAX_ENCRYPTION_MAPPINGS];
+    double last_access_time[NETCODE_MAX_ENCRYPTION_MAPPINGS];
+    struct netcode_address_t address[NETCODE_MAX_ENCRYPTION_MAPPINGS];
+    uint8_t send_key[NETCODE_KEY_BYTES*NETCODE_MAX_ENCRYPTION_MAPPINGS];
+    uint8_t receive_key[NETCODE_KEY_BYTES*NETCODE_MAX_ENCRYPTION_MAPPINGS];
+};
+
+void netcode_encryption_manager_reset( struct netcode_encryption_manager_t * encryption_manager )
+{
+    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "reset encryption manager\n" );
+
+    netcode_assert( encryption_manager );
+
+    encryption_manager->num_encryption_mappings = 0;
+    
+    int i;
+    for ( i = 0; i < NETCODE_MAX_ENCRYPTION_MAPPINGS; ++i )
+    {
+        encryption_manager->expire_time[i] = -1.0;
+        encryption_manager->last_access_time[i] = -1000.0;
+        memset( &encryption_manager->address[i], 0, sizeof( struct netcode_address_t ) );
+    }
+
+    memset( encryption_manager->timeout, 0, sizeof( encryption_manager->timeout ) );    
+    memset( encryption_manager->send_key, 0, sizeof( encryption_manager->send_key ) );
+    memset( encryption_manager->receive_key, 0, sizeof( encryption_manager->receive_key ) );
+}
+
+int netcode_encryption_manager_entry_expired( struct netcode_encryption_manager_t * encryption_manager, int index, double time )
+{
+    return ( encryption_manager->timeout[index] > 0 && ( encryption_manager->last_access_time[index] + encryption_manager->timeout[index] ) < time ) ||
+           ( encryption_manager->expire_time[index] >= 0.0 && encryption_manager->expire_time[index] < time );
+}
+
+int netcode_encryption_manager_add_encryption_mapping( struct netcode_encryption_manager_t * encryption_manager, 
+                                                       struct netcode_address_t * address, 
+                                                       uint8_t * send_key, 
+                                                       uint8_t * receive_key, 
+                                                       double time, 
+                                                       double expire_time,
+                                                       int timeout )
+{
+    int i;
+    for ( i = 0; i < encryption_manager->num_encryption_mappings; ++i )
+    {
+        if ( netcode_address_equal( &encryption_manager->address[i], address ) && !netcode_encryption_manager_entry_expired( encryption_manager, i, time ) )
+        {
+            encryption_manager->timeout[i] = timeout;
+            encryption_manager->expire_time[i] = expire_time;
+            encryption_manager->last_access_time[i] = time;
+            memcpy( encryption_manager->send_key + i * NETCODE_KEY_BYTES, send_key, NETCODE_KEY_BYTES );
+            memcpy( encryption_manager->receive_key + i * NETCODE_KEY_BYTES, receive_key, NETCODE_KEY_BYTES );
+            return 1;
+        }
+    }
+
+    for ( i = 0; i < NETCODE_MAX_ENCRYPTION_MAPPINGS; ++i )
+    {
+        if ( encryption_manager->address[i].type == NETCODE_ADDRESS_NONE || netcode_encryption_manager_entry_expired( encryption_manager, i, time ) )
+        {
+            encryption_manager->timeout[i] = timeout;
+            encryption_manager->address[i] = *address;
+            encryption_manager->expire_time[i] = expire_time;
+            encryption_manager->last_access_time[i] = time;
+            memcpy( encryption_manager->send_key + i * NETCODE_KEY_BYTES, send_key, NETCODE_KEY_BYTES );
+            memcpy( encryption_manager->receive_key + i * NETCODE_KEY_BYTES, receive_key, NETCODE_KEY_BYTES );
+            if ( i + 1 > encryption_manager->num_encryption_mappings )
+                encryption_manager->num_encryption_mappings = i + 1;
+            return 1;
+        }
+    }
+
+    return 0;
+}
+
+int netcode_encryption_manager_remove_encryption_mapping( struct netcode_encryption_manager_t * encryption_manager, struct netcode_address_t * address, double time )
+{
+    netcode_assert( encryption_manager );
+    netcode_assert( address );
+
+    int i;
+    for ( i = 0; i < encryption_manager->num_encryption_mappings; ++i )
+    {
+        if ( netcode_address_equal( &encryption_manager->address[i], address ) )
+        {
+            encryption_manager->expire_time[i] = -1.0;
+            encryption_manager->last_access_time[i] = -1000.0;
+            memset( &encryption_manager->address[i], 0, sizeof( struct netcode_address_t ) );
+            memset( encryption_manager->send_key + i * NETCODE_KEY_BYTES, 0, NETCODE_KEY_BYTES );
+            memset( encryption_manager->receive_key + i * NETCODE_KEY_BYTES, 0, NETCODE_KEY_BYTES );
+
+            if ( i + 1 == encryption_manager->num_encryption_mappings )
+            {
+                int index = i - 1;
+                while ( index >= 0 )
+                {
+                    if ( !netcode_encryption_manager_entry_expired( encryption_manager, index, time ) )
+                    {
+                        break;
+                    }
+                    encryption_manager->address[index].type = NETCODE_ADDRESS_NONE;
+                    index--;
+                }
+                encryption_manager->num_encryption_mappings = index + 1;
+            }
+
+            return 1;
+        }
+    }
+
+    return 0;
+}
+
+int netcode_encryption_manager_find_encryption_mapping( struct netcode_encryption_manager_t * encryption_manager, struct netcode_address_t * address, double time )
+{
+    int i;
+    for ( i = 0; i < encryption_manager->num_encryption_mappings; ++i )
+    {
+        if ( netcode_address_equal( &encryption_manager->address[i], address ) && !netcode_encryption_manager_entry_expired( encryption_manager, i, time ) )
+        {
+            encryption_manager->last_access_time[i] = time;
+            return i;
+        }
+    }
+    return -1;
+}
+
+int netcode_encryption_manager_touch( struct netcode_encryption_manager_t * encryption_manager, int index, struct netcode_address_t * address, double time )
+{
+    netcode_assert( index >= 0 );
+    netcode_assert( index < encryption_manager->num_encryption_mappings );
+    if ( !netcode_address_equal( &encryption_manager->address[index], address ) )
+        return 0;
+    encryption_manager->last_access_time[index] = time;
+    return 1;
+}
+
+void netcode_encryption_manager_set_expire_time( struct netcode_encryption_manager_t * encryption_manager, int index, double expire_time )
+{
+    netcode_assert( index >= 0 );
+    netcode_assert( index < encryption_manager->num_encryption_mappings );
+    encryption_manager->expire_time[index] = expire_time;
+}
+
+
+uint8_t * netcode_encryption_manager_get_send_key( struct netcode_encryption_manager_t * encryption_manager, int index )
+{
+    netcode_assert( encryption_manager );
+    if ( index == -1 )
+        return NULL;
+    netcode_assert( index >= 0 );
+    netcode_assert( index < encryption_manager->num_encryption_mappings );
+    return encryption_manager->send_key + index * NETCODE_KEY_BYTES;
+}
+
+uint8_t * netcode_encryption_manager_get_receive_key( struct netcode_encryption_manager_t * encryption_manager, int index )
+{
+    netcode_assert( encryption_manager );
+    if ( index == -1 )
+        return NULL;
+    netcode_assert( index >= 0 );
+    netcode_assert( index < encryption_manager->num_encryption_mappings );
+    return encryption_manager->receive_key + index * NETCODE_KEY_BYTES;
+}
+
+int netcode_encryption_manager_get_timeout( struct netcode_encryption_manager_t * encryption_manager, int index )
+{
+    netcode_assert( encryption_manager );
+    if ( index == -1 )
+        return 0;
+    netcode_assert( index >= 0 );
+    netcode_assert( index < encryption_manager->num_encryption_mappings );
+    return encryption_manager->timeout[index];
+}
+
+// ----------------------------------------------------------------
+
+#define NETCODE_MAX_CONNECT_TOKEN_ENTRIES ( NETCODE_MAX_CLIENTS * 8 )
+
+struct netcode_connect_token_entry_t
+{
+    double time;
+    uint8_t mac[NETCODE_MAC_BYTES];
+    struct netcode_address_t address;
+};
+
+void netcode_connect_token_entries_reset( struct netcode_connect_token_entry_t * connect_token_entries )
+{
+    int i;
+    for ( i = 0; i < NETCODE_MAX_CONNECT_TOKEN_ENTRIES; ++i )
+    {
+        connect_token_entries[i].time = -1000.0;
+        memset( connect_token_entries[i].mac, 0, NETCODE_MAC_BYTES );
+        memset( &connect_token_entries[i].address, 0, sizeof( struct netcode_address_t ) );
+    }
+}
+
+int netcode_connect_token_entries_find_or_add( struct netcode_connect_token_entry_t * connect_token_entries, 
+                                               struct netcode_address_t * address, 
+                                               uint8_t * mac, 
+                                               double time )
+{
+    netcode_assert( connect_token_entries );
+    netcode_assert( address );
+    netcode_assert( mac );
+
+    // find the matching entry for the token mac and the oldest token entry. constant time worst case. This is intentional!
+
+    int matching_token_index = -1;
+    int oldest_token_index = -1;
+    double oldest_token_time = 0.0;
+
+    int i;
+    for ( i = 0; i < NETCODE_MAX_CONNECT_TOKEN_ENTRIES; ++i )
+    {
+        if ( memcmp( mac, connect_token_entries[i].mac, NETCODE_MAC_BYTES ) == 0 )
+            matching_token_index = i;
+        
+        if ( oldest_token_index == -1 || connect_token_entries[i].time < oldest_token_time )
+        {
+            oldest_token_time = connect_token_entries[i].time;
+            oldest_token_index = i;
+        }
+    }
+
+    // if no entry is found with the mac, this is a new connect token. replace the oldest token entry.
+
+    netcode_assert( oldest_token_index != -1 );
+
+    if ( matching_token_index == -1 )
+    {
+        connect_token_entries[oldest_token_index].time = time;
+        connect_token_entries[oldest_token_index].address = *address;
+        memcpy( connect_token_entries[oldest_token_index].mac, mac, NETCODE_MAC_BYTES );
+        return 1;
+    }
+
+    // allow connect tokens we have already seen from the same address
+
+    netcode_assert( matching_token_index >= 0 );
+    netcode_assert( matching_token_index < NETCODE_MAX_CONNECT_TOKEN_ENTRIES );
+    if ( netcode_address_equal( &connect_token_entries[matching_token_index].address, address ) )
+        return 1;
+
+    return 0;
+}
+
+// ----------------------------------------------------------------
+
+#define NETCODE_SERVER_FLAG_IGNORE_CONNECTION_REQUEST_PACKETS       1
+#define NETCODE_SERVER_FLAG_IGNORE_CONNECTION_RESPONSE_PACKETS      (1<<1)
+
+void netcode_default_server_config( struct netcode_server_config_t * config )
+{
+    netcode_assert( config );
+    config->allocator_context = NULL;
+    config->allocate_function = netcode_default_allocate_function;
+    config->free_function = netcode_default_free_function;
+    config->network_simulator = NULL;
+    config->callback_context = NULL;
+    config->connect_disconnect_callback = NULL;
+    config->send_loopback_packet_callback = NULL;
+    config->override_send_and_receive = 0;
+    config->send_packet_override = NULL;
+    config->receive_packet_override = NULL;
+};
+
+struct netcode_server_t
+{
+    struct netcode_server_config_t config;
+    struct netcode_socket_holder_t socket_holder;
+    struct netcode_address_t address;
+    uint32_t flags;
+    double time;
+    int running;
+    int max_clients;
+    int num_connected_clients;
+    uint64_t global_sequence;
+    uint64_t challenge_sequence;
+    uint8_t challenge_key[NETCODE_KEY_BYTES];
+    int client_connected[NETCODE_MAX_CLIENTS];
+    int client_timeout[NETCODE_MAX_CLIENTS];
+    int client_loopback[NETCODE_MAX_CLIENTS];
+    int client_confirmed[NETCODE_MAX_CLIENTS];
+    int client_encryption_index[NETCODE_MAX_CLIENTS];
+    uint64_t client_id[NETCODE_MAX_CLIENTS];
+    uint64_t client_sequence[NETCODE_MAX_CLIENTS];
+    double client_last_packet_send_time[NETCODE_MAX_CLIENTS];
+    double client_last_packet_receive_time[NETCODE_MAX_CLIENTS];
+    uint8_t client_user_data[NETCODE_MAX_CLIENTS][NETCODE_USER_DATA_BYTES];
+    struct netcode_replay_protection_t client_replay_protection[NETCODE_MAX_CLIENTS];
+    struct netcode_packet_queue_t client_packet_queue[NETCODE_MAX_CLIENTS];
+    struct netcode_address_t client_address[NETCODE_MAX_CLIENTS];
+    struct netcode_connect_token_entry_t connect_token_entries[NETCODE_MAX_CONNECT_TOKEN_ENTRIES];
+    struct netcode_encryption_manager_t encryption_manager;
+    uint8_t * receive_packet_data[NETCODE_SERVER_MAX_RECEIVE_PACKETS];
+    int receive_packet_bytes[NETCODE_SERVER_MAX_RECEIVE_PACKETS];
+    struct netcode_address_t receive_from[NETCODE_SERVER_MAX_RECEIVE_PACKETS];
+};
+
+int netcode_server_socket_create( struct netcode_socket_t * socket,
+                                  struct netcode_address_t * address,
+                                  int send_buffer_size,
+                                  int receive_buffer_size,
+                                  NETCODE_CONST struct netcode_server_config_t * config )
+{
+    netcode_assert( socket );
+    netcode_assert( address );
+    netcode_assert( config );
+
+    if ( !config->network_simulator )
+    {
+        if ( !config->override_send_and_receive )
+        {
+            if ( netcode_socket_create( socket, address, send_buffer_size, receive_buffer_size ) != NETCODE_SOCKET_ERROR_NONE )
+            {
+                return 0;
+            }
+        }
+    }
+
+    return 1;
+}
+
+struct netcode_server_t * netcode_server_create_overload( NETCODE_CONST char * server_address1_string, NETCODE_CONST char * server_address2_string, NETCODE_CONST struct netcode_server_config_t * config, double time )
+{
+    netcode_assert( config );
+    netcode_assert( netcode.initialized );
+
+    struct netcode_address_t server_address1;
+    struct netcode_address_t server_address2;
+
+    memset( &server_address1, 0, sizeof( server_address1 ) );
+    memset( &server_address2, 0, sizeof( server_address2 ) );
+
+    if ( netcode_parse_address( server_address1_string, &server_address1 ) != NETCODE_OK )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to parse server public address\n" );
+        return NULL;
+    }
+
+    if ( server_address2_string != NULL && netcode_parse_address( server_address2_string, &server_address2 ) != NETCODE_OK )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to parse server public address2\n" );
+        return NULL;
+    }
+
+    struct netcode_address_t bind_address_ipv4;
+    struct netcode_address_t bind_address_ipv6;
+
+    memset( &bind_address_ipv4, 0, sizeof( bind_address_ipv4 ) );
+    memset( &bind_address_ipv6, 0, sizeof( bind_address_ipv6 ) );
+
+    struct netcode_socket_t socket_ipv4;
+    struct netcode_socket_t socket_ipv6;
+
+    memset( &socket_ipv4, 0, sizeof( socket_ipv4 ) );
+    memset( &socket_ipv6, 0, sizeof( socket_ipv6 ) );
+
+    if ( server_address1.type == NETCODE_ADDRESS_IPV4 || server_address2.type == NETCODE_ADDRESS_IPV4 )
+    {
+        bind_address_ipv4.type = NETCODE_ADDRESS_IPV4;
+        bind_address_ipv4.port = server_address1.type == NETCODE_ADDRESS_IPV4 ? server_address1.port : server_address2.port;
+
+        if ( !netcode_server_socket_create( &socket_ipv4, &bind_address_ipv4, NETCODE_SERVER_SOCKET_SNDBUF_SIZE, NETCODE_SERVER_SOCKET_RCVBUF_SIZE, config ) )
+        {
+            return NULL;
+        }
+    }
+
+    if ( server_address1.type == NETCODE_ADDRESS_IPV6 || server_address2.type == NETCODE_ADDRESS_IPV6 )
+    {
+        bind_address_ipv6.type = NETCODE_ADDRESS_IPV6;
+        bind_address_ipv6.port = server_address1.type == NETCODE_ADDRESS_IPV6 ? server_address1.port : server_address2.port;
+
+        if ( !netcode_server_socket_create( &socket_ipv6, &bind_address_ipv6, NETCODE_SERVER_SOCKET_SNDBUF_SIZE, NETCODE_SERVER_SOCKET_RCVBUF_SIZE, config ) )
+        {
+            return NULL;
+        }
+    }
+
+    struct netcode_server_t * server = (struct netcode_server_t*) config->allocate_function( config->allocator_context, sizeof( struct netcode_server_t ) );
+    if ( !server )
+    {
+        netcode_socket_destroy( &socket_ipv4 );
+        netcode_socket_destroy( &socket_ipv6 );
+        return NULL;
+    }
+
+    if ( !config->network_simulator )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_INFO, "server listening on %s\n", server_address1_string );
+    }
+    else
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_INFO, "server listening on %s (network simulator)\n", server_address1_string );
+    }
+
+    server->config = *config;
+    server->socket_holder.ipv4 = socket_ipv4;
+    server->socket_holder.ipv6 = socket_ipv6;
+    server->address = server_address1.type == NETCODE_ADDRESS_IPV4 ? server->socket_holder.ipv4.address : server->socket_holder.ipv6.address;
+    server->flags = 0;
+    server->time = time;
+    server->running = 0;
+    server->max_clients = 0;
+    server->num_connected_clients = 0;
+    server->global_sequence = 1ULL << 63;
+
+    memset( server->client_connected, 0, sizeof( server->client_connected ) );
+    memset( server->client_loopback, 0, sizeof( server->client_loopback ) );
+    memset( server->client_confirmed, 0, sizeof( server->client_confirmed ) );
+    memset( server->client_id, 0, sizeof( server->client_id ) );
+    memset( server->client_sequence, 0, sizeof( server->client_sequence ) );
+    memset( server->client_last_packet_send_time, 0, sizeof( server->client_last_packet_send_time ) );
+    memset( server->client_last_packet_receive_time, 0, sizeof( server->client_last_packet_receive_time ) );
+    memset( server->client_address, 0, sizeof( server->client_address ) );
+    memset( server->client_user_data, 0, sizeof( server->client_user_data ) );
+
+    int i;
+    for ( i = 0; i < NETCODE_MAX_CLIENTS; ++i )
+        server->client_encryption_index[i] = -1;
+
+    netcode_connect_token_entries_reset( server->connect_token_entries );
+
+    netcode_encryption_manager_reset( &server->encryption_manager );
+
+    for ( i = 0; i < NETCODE_MAX_CLIENTS; ++i )
+        netcode_replay_protection_reset( &server->client_replay_protection[i] );
+
+    memset( &server->client_packet_queue, 0, sizeof( server->client_packet_queue ) );
+
+    return server;
+}
+
+struct netcode_server_t * netcode_server_create( NETCODE_CONST char * server_address_string, NETCODE_CONST struct netcode_server_config_t * config, double time )
+{
+    return netcode_server_create_overload( server_address_string, NULL, config, time );
+}
+
+void netcode_server_stop( struct netcode_server_t * server );
+
+void netcode_server_destroy( struct netcode_server_t * server )
+{
+    netcode_assert( server );
+
+    netcode_server_stop( server );
+
+    netcode_socket_destroy( &server->socket_holder.ipv4 );
+    netcode_socket_destroy( &server->socket_holder.ipv6 );
+
+    server->config.free_function( server->config.allocator_context, server );
+}
+
+void netcode_server_start( struct netcode_server_t * server, int max_clients )
+{
+    netcode_assert( server );
+    netcode_assert( max_clients > 0 );
+    netcode_assert( max_clients <= NETCODE_MAX_CLIENTS );
+
+    if ( server->running )
+        netcode_server_stop( server );
+
+    netcode_printf( NETCODE_LOG_LEVEL_INFO, "server started with %d client slots\n", max_clients );
+
+    server->running = 1;
+    server->max_clients = max_clients;
+    server->num_connected_clients = 0;
+    server->challenge_sequence = 0;    
+    netcode_generate_key( server->challenge_key );
+
+    int i;
+    for ( i = 0; i < server->max_clients; ++i )
+    {
+        netcode_packet_queue_init( &server->client_packet_queue[i], server->config.allocator_context, server->config.allocate_function, server->config.free_function );
+    }
+}
+
+void netcode_server_send_global_packet( struct netcode_server_t * server, void * packet, struct netcode_address_t * to, uint8_t * packet_key )
+{
+    netcode_assert( server );
+    netcode_assert( packet );
+    netcode_assert( to );
+    netcode_assert( packet_key );
+
+    uint8_t packet_data[NETCODE_MAX_PACKET_BYTES];
+
+    int packet_bytes = netcode_write_packet( packet, packet_data, NETCODE_MAX_PACKET_BYTES, server->global_sequence, packet_key, server->config.protocol_id );
+
+    netcode_assert( packet_bytes <= NETCODE_MAX_PACKET_BYTES );
+
+    if ( server->config.network_simulator )
+    {
+        netcode_network_simulator_send_packet( server->config.network_simulator, &server->address, to, packet_data, packet_bytes );
+    }
+    else
+    {
+        if ( server->config.override_send_and_receive )
+        {
+            server->config.send_packet_override( server->config.callback_context, to, packet_data, packet_bytes );
+        }
+        else if ( to->type == NETCODE_ADDRESS_IPV4 )
+        {
+            netcode_socket_send_packet( &server->socket_holder.ipv4, to, packet_data, packet_bytes );
+        }
+        else if ( to->type == NETCODE_ADDRESS_IPV6 )
+        {
+            netcode_socket_send_packet( &server->socket_holder.ipv6, to, packet_data, packet_bytes );
+        }
+    }
+
+    server->global_sequence++;
+}
+
+void netcode_server_send_client_packet( struct netcode_server_t * server, void * packet, int client_index )
+{
+    netcode_assert( server );
+    netcode_assert( packet );
+    netcode_assert( client_index >= 0 );
+    netcode_assert( client_index < server->max_clients );
+    netcode_assert( server->client_connected[client_index] );
+    netcode_assert( !server->client_loopback[client_index] );
+
+    uint8_t packet_data[NETCODE_MAX_PACKET_BYTES];
+
+    if ( !netcode_encryption_manager_touch( &server->encryption_manager, 
+                                            server->client_encryption_index[client_index], 
+                                            &server->client_address[client_index], 
+                                            server->time ) )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: encryption mapping is out of date for client %d\n", client_index );
+        return;
+    }
+
+    uint8_t * packet_key = netcode_encryption_manager_get_send_key( &server->encryption_manager, server->client_encryption_index[client_index] );
+
+    int packet_bytes = netcode_write_packet( packet, packet_data, NETCODE_MAX_PACKET_BYTES, server->client_sequence[client_index], packet_key, server->config.protocol_id );
+
+    netcode_assert( packet_bytes <= NETCODE_MAX_PACKET_BYTES );
+
+    if ( server->config.network_simulator )
+    {
+        netcode_network_simulator_send_packet( server->config.network_simulator, &server->address, &server->client_address[client_index], packet_data, packet_bytes );
+    }
+    else
+    {
+        if ( server->config.override_send_and_receive )
+        {
+            server->config.send_packet_override( server->config.callback_context, &server->client_address[client_index], packet_data, packet_bytes );
+        }
+        else
+        {
+            if ( server->client_address[client_index].type == NETCODE_ADDRESS_IPV4 )
+            {
+                netcode_socket_send_packet( &server->socket_holder.ipv4, &server->client_address[client_index], packet_data, packet_bytes );
+            }
+            else if ( server->client_address[client_index].type == NETCODE_ADDRESS_IPV6 )
+            {
+                netcode_socket_send_packet( &server->socket_holder.ipv6, &server->client_address[client_index], packet_data, packet_bytes );
+            }
+        }
+    }
+
+    server->client_sequence[client_index]++;
+
+    server->client_last_packet_send_time[client_index] = server->time;
+}
+
+void netcode_server_disconnect_client_internal( struct netcode_server_t * server, int client_index, int send_disconnect_packets )
+{
+    netcode_assert( server );
+    netcode_assert( server->running );
+    netcode_assert( client_index >= 0 );
+    netcode_assert( client_index < server->max_clients );
+    netcode_assert( server->client_connected[client_index] );
+    netcode_assert( !server->client_loopback[client_index] );
+
+    netcode_printf( NETCODE_LOG_LEVEL_INFO, "server disconnected client %d\n", client_index );
+
+    if ( server->config.connect_disconnect_callback )
+    {
+        server->config.connect_disconnect_callback( server->config.callback_context, client_index, 0 );
+    }
+
+    if ( send_disconnect_packets )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server sent disconnect packets to client %d\n", client_index );
+
+        int i;
+        for ( i = 0; i < NETCODE_NUM_DISCONNECT_PACKETS; ++i )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server sent disconnect packet %d\n", i );
+
+            struct netcode_connection_disconnect_packet_t packet;
+            packet.packet_type = NETCODE_CONNECTION_DISCONNECT_PACKET;
+
+            netcode_server_send_client_packet( server, &packet, client_index );
+        }
+    }
+
+    while ( 1 )
+    {
+        void * packet = netcode_packet_queue_pop( &server->client_packet_queue[client_index], NULL );
+        if ( !packet )
+            break;
+        server->config.free_function( server->config.allocator_context, packet );
+    }
+
+    netcode_packet_queue_clear( &server->client_packet_queue[client_index] );
+
+    netcode_replay_protection_reset( &server->client_replay_protection[client_index] );
+
+    netcode_encryption_manager_remove_encryption_mapping( &server->encryption_manager, &server->client_address[client_index], server->time );
+
+    server->client_connected[client_index] = 0;
+    server->client_confirmed[client_index] = 0;
+    server->client_id[client_index] = 0;
+    server->client_sequence[client_index] = 0;
+    server->client_last_packet_send_time[client_index] = 0.0;
+    server->client_last_packet_receive_time[client_index] = 0.0;
+    memset( &server->client_address[client_index], 0, sizeof( struct netcode_address_t ) );
+    server->client_encryption_index[client_index] = -1;
+    memset( server->client_user_data[client_index], 0, NETCODE_USER_DATA_BYTES );
+
+    server->num_connected_clients--;
+
+    netcode_assert( server->num_connected_clients >= 0 );
+}
+
+void netcode_server_disconnect_client( struct netcode_server_t * server, int client_index )
+{
+    netcode_assert( server );
+
+    if ( !server->running )
+        return;
+
+    netcode_assert( client_index >= 0 );
+    netcode_assert( client_index < server->max_clients );
+    netcode_assert( server->client_loopback[client_index] == 0 );
+
+    if ( !server->client_connected[client_index] )
+        return;
+
+    if ( server->client_loopback[client_index] )
+        return;
+
+    netcode_server_disconnect_client_internal( server, client_index, 1 );
+}
+
+void netcode_server_disconnect_all_clients( struct netcode_server_t * server )
+{
+    netcode_assert( server );
+
+    if ( !server->running )
+        return;
+
+    int i;
+    for ( i = 0; i < server->max_clients; ++i )
+    {
+        if ( server->client_connected[i] && !server->client_loopback[i] )
+        {
+            netcode_server_disconnect_client_internal( server, i, 1 );
+        }
+    }
+}
+
+void netcode_server_stop( struct netcode_server_t * server )
+{
+    netcode_assert( server );
+
+    if ( !server->running )
+        return;
+
+    netcode_server_disconnect_all_clients( server );
+
+    server->running = 0;
+    server->max_clients = 0;
+    server->num_connected_clients = 0;
+
+    server->global_sequence = 0;
+    server->challenge_sequence = 0;
+    memset( server->challenge_key, 0, NETCODE_KEY_BYTES );
+
+    netcode_connect_token_entries_reset( server->connect_token_entries );
+
+    netcode_encryption_manager_reset( &server->encryption_manager );
+
+    netcode_printf( NETCODE_LOG_LEVEL_INFO, "server stopped\n" );
+}
+
+int netcode_server_find_client_index_by_id( struct netcode_server_t * server, uint64_t client_id )
+{
+    netcode_assert( server );
+
+    int i;
+    for ( i = 0; i < server->max_clients; ++i )
+    {   
+        if ( server->client_connected[i] && server->client_id[i] == client_id )
+            return i;
+    }
+
+    return -1;
+}
+
+int netcode_server_find_client_index_by_address( struct netcode_server_t * server, struct netcode_address_t * address )
+{
+    netcode_assert( server );
+    netcode_assert( address );
+
+    if ( address->type == 0 )
+        return -1;
+
+    int i;
+    for ( i = 0; i < server->max_clients; ++i )
+    {   
+        if ( server->client_connected[i] && netcode_address_equal( &server->client_address[i], address ) )
+            return i;
+    }
+
+    return -1;
+}
+
+void netcode_server_process_connection_request_packet( struct netcode_server_t * server, 
+                                                       struct netcode_address_t * from, 
+                                                       struct netcode_connection_request_packet_t * packet )
+{
+    netcode_assert( server );
+
+    (void) from;
+
+    struct netcode_connect_token_private_t connect_token_private;
+    if ( netcode_read_connect_token_private( packet->connect_token_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES, &connect_token_private ) != NETCODE_OK )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server ignored connection request. failed to read connect token\n" );
+        return;
+    }
+
+    int found_server_address = 0;
+    int i;
+    for ( i = 0; i < connect_token_private.num_server_addresses; ++i )
+    {
+        if ( netcode_address_equal( &server->address, &connect_token_private.server_addresses[i] ) )
+        {
+            found_server_address = 1;
+        }
+    }
+    if ( !found_server_address )
+    {   
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server ignored connection request. server address not in connect token whitelist\n" );
+        return;
+    }
+
+    if ( netcode_server_find_client_index_by_address( server, from ) != -1 )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server ignored connection request. a client with this address is already connected\n" );
+        return;
+    }
+
+    if ( netcode_server_find_client_index_by_id( server, connect_token_private.client_id ) != -1 )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server ignored connection request. a client with this id is already connected\n" );
+        return;
+    }
+
+    if ( !netcode_connect_token_entries_find_or_add( server->connect_token_entries, 
+                                                     from, 
+                                                     packet->connect_token_data + NETCODE_CONNECT_TOKEN_PRIVATE_BYTES - NETCODE_MAC_BYTES, 
+                                                     server->time ) )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server ignored connection request. connect token has already been used\n" );
+        return;
+    }
+
+    if ( server->num_connected_clients == server->max_clients )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server denied connection request. server is full\n" );
+
+        struct netcode_connection_denied_packet_t p;
+        p.packet_type = NETCODE_CONNECTION_DENIED_PACKET;
+        
+        netcode_server_send_global_packet( server, &p, from, connect_token_private.server_to_client_key );
+
+        return;
+    }
+
+    double expire_time = ( connect_token_private.timeout_seconds >= 0 ) ? server->time + connect_token_private.timeout_seconds : -1.0;
+
+    if ( !netcode_encryption_manager_add_encryption_mapping( &server->encryption_manager, 
+                                                             from, 
+                                                             connect_token_private.server_to_client_key, 
+                                                             connect_token_private.client_to_server_key, 
+                                                             server->time, 
+                                                             expire_time,
+                                                             connect_token_private.timeout_seconds ) )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server ignored connection request. failed to add encryption mapping\n" );
+        return;
+    }
+
+    struct netcode_challenge_token_t challenge_token;
+    challenge_token.client_id = connect_token_private.client_id;
+    memcpy( challenge_token.user_data, connect_token_private.user_data, NETCODE_USER_DATA_BYTES );
+
+    struct netcode_connection_challenge_packet_t challenge_packet;
+    challenge_packet.packet_type = NETCODE_CONNECTION_CHALLENGE_PACKET;
+    challenge_packet.challenge_token_sequence = server->challenge_sequence;
+    netcode_write_challenge_token( &challenge_token, challenge_packet.challenge_token_data, NETCODE_CHALLENGE_TOKEN_BYTES );
+    if ( netcode_encrypt_challenge_token( challenge_packet.challenge_token_data, 
+                                          NETCODE_CHALLENGE_TOKEN_BYTES, 
+                                          server->challenge_sequence, 
+                                          server->challenge_key ) != NETCODE_OK )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server ignored connection request. failed to encrypt challenge token\n" );
+        return;
+    }
+
+    server->challenge_sequence++;
+
+    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server sent connection challenge packet\n" );
+
+    netcode_server_send_global_packet( server, &challenge_packet, from, connect_token_private.server_to_client_key );
+}
+
+int netcode_server_find_free_client_index( struct netcode_server_t * server )
+{
+    netcode_assert( server );
+
+    int i;
+    for ( i = 0; i < server->max_clients; ++i )
+    {
+        if ( !server->client_connected[i] )
+            return i;
+    }
+
+    return -1;
+}
+
+void netcode_server_connect_client( struct netcode_server_t * server, 
+                                    int client_index, 
+                                    struct netcode_address_t * address, 
+                                    uint64_t client_id, 
+                                    int encryption_index,
+                                    int timeout_seconds, 
+                                    void * user_data )
+{
+    netcode_assert( server );
+    netcode_assert( server->running );
+    netcode_assert( client_index >= 0 );
+    netcode_assert( client_index < server->max_clients );
+    netcode_assert( address );
+    netcode_assert( encryption_index != -1 );
+    netcode_assert( user_data );
+
+    server->num_connected_clients++;
+
+    netcode_assert( server->num_connected_clients <= server->max_clients );
+
+    netcode_assert( server->client_connected[client_index] == 0 );
+
+    netcode_encryption_manager_set_expire_time( &server->encryption_manager, encryption_index, -1.0 );
+
+    server->client_connected[client_index] = 1;
+    server->client_timeout[client_index] = timeout_seconds;
+    server->client_encryption_index[client_index] = encryption_index;
+    server->client_id[client_index] = client_id;
+    server->client_sequence[client_index] = 0;
+    server->client_address[client_index] = *address;
+    server->client_last_packet_send_time[client_index] = server->time;
+    server->client_last_packet_receive_time[client_index] = server->time;
+    memcpy( server->client_user_data[client_index], user_data, NETCODE_USER_DATA_BYTES );
+
+    char address_string[NETCODE_MAX_ADDRESS_STRING_LENGTH];
+
+    netcode_printf( NETCODE_LOG_LEVEL_INFO, "server accepted client %s %.16" PRIx64 " in slot %d\n", 
+        netcode_address_to_string( address, address_string ), client_id, client_index );
+
+    struct netcode_connection_keep_alive_packet_t packet;
+    packet.packet_type = NETCODE_CONNECTION_KEEP_ALIVE_PACKET;
+    packet.client_index = client_index;
+    packet.max_clients = server->max_clients;
+
+    netcode_server_send_client_packet( server, &packet, client_index );
+
+    if ( server->config.connect_disconnect_callback )
+    {
+        server->config.connect_disconnect_callback( server->config.callback_context, client_index, 1 );
+    }
+}
+
+void netcode_server_process_connection_response_packet( struct netcode_server_t * server, 
+                                                        struct netcode_address_t * from, 
+                                                        struct netcode_connection_response_packet_t * packet, 
+                                                        int encryption_index )
+{
+    netcode_assert( server );
+
+    if ( netcode_decrypt_challenge_token( packet->challenge_token_data, 
+                                          NETCODE_CHALLENGE_TOKEN_BYTES, 
+                                          packet->challenge_token_sequence, 
+                                          server->challenge_key ) != NETCODE_OK )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server ignored connection response. failed to decrypt challenge token\n" );
+        return;
+    }
+
+    struct netcode_challenge_token_t challenge_token;
+    if ( netcode_read_challenge_token( packet->challenge_token_data, NETCODE_CHALLENGE_TOKEN_BYTES, &challenge_token ) != NETCODE_OK )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server ignored connection response. failed to read challenge token\n" );
+        return;
+    }
+
+    uint8_t * packet_send_key = netcode_encryption_manager_get_send_key( &server->encryption_manager, encryption_index );
+
+    if ( !packet_send_key )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server ignored connection response. no packet send key\n" );
+        return;
+    }
+
+    if ( netcode_server_find_client_index_by_address( server, from ) != -1 )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server ignored connection response. a client with this address is already connected\n" );
+        return;
+    }
+
+    if ( netcode_server_find_client_index_by_id( server, challenge_token.client_id ) != -1 )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server ignored connection response. a client with this id is already connected\n" );
+        return;
+    }
+
+    if ( server->num_connected_clients == server->max_clients )
+    {
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server denied connection response. server is full\n" );
+
+        struct netcode_connection_denied_packet_t p;
+        p.packet_type = NETCODE_CONNECTION_DENIED_PACKET;
+
+        netcode_server_send_global_packet( server, &p, from, packet_send_key );
+
+        return;
+    }
+
+    int client_index = netcode_server_find_free_client_index( server );
+
+    netcode_assert( client_index != -1 );
+
+    int timeout_seconds = netcode_encryption_manager_get_timeout( &server->encryption_manager, encryption_index );
+
+    netcode_server_connect_client( server, client_index, from, challenge_token.client_id, encryption_index, timeout_seconds, challenge_token.user_data );
+}
+
+void netcode_server_process_packet_internal( struct netcode_server_t * server, 
+                                             struct netcode_address_t * from, 
+                                             void * packet, 
+                                             uint64_t sequence, 
+                                             int encryption_index, 
+                                             int client_index )
+{
+    netcode_assert( server );
+    netcode_assert( packet );
+
+    (void) from;
+    (void) sequence;
+
+    uint8_t packet_type = ( (uint8_t*) packet ) [0];
+
+    switch ( packet_type )
+    {
+        case NETCODE_CONNECTION_REQUEST_PACKET:
+        {    
+            if ( ( server->flags & NETCODE_SERVER_FLAG_IGNORE_CONNECTION_REQUEST_PACKETS ) == 0 )
+            {
+                char from_address_string[NETCODE_MAX_ADDRESS_STRING_LENGTH];
+                netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server received connection request from %s\n", netcode_address_to_string( from, from_address_string ) );
+                netcode_server_process_connection_request_packet( server, from, (struct netcode_connection_request_packet_t*) packet );
+            }
+        }
+        break;
+
+        case NETCODE_CONNECTION_RESPONSE_PACKET:
+        {    
+            if ( ( server->flags & NETCODE_SERVER_FLAG_IGNORE_CONNECTION_RESPONSE_PACKETS ) == 0 )
+            {
+                char from_address_string[NETCODE_MAX_ADDRESS_STRING_LENGTH];
+                netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server received connection response from %s\n", netcode_address_to_string( from, from_address_string ) );
+                netcode_server_process_connection_response_packet( server, from, (struct netcode_connection_response_packet_t*) packet, encryption_index );
+            }
+        }
+        break;
+
+        case NETCODE_CONNECTION_KEEP_ALIVE_PACKET:
+        {
+            if ( client_index != -1 )
+            {
+                netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server received connection keep alive packet from client %d\n", client_index );
+                server->client_last_packet_receive_time[client_index] = server->time;
+                if ( !server->client_confirmed[client_index] )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server confirmed connection with client %d\n", client_index );
+                    server->client_confirmed[client_index] = 1;
+                }
+            }
+        }
+        break;
+
+        case NETCODE_CONNECTION_PAYLOAD_PACKET:
+        {
+            if ( client_index != -1 )
+            {
+                netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server received connection payload packet from client %d\n", client_index );
+                server->client_last_packet_receive_time[client_index] = server->time;
+                if ( !server->client_confirmed[client_index] )
+                {
+                    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server confirmed connection with client %d\n", client_index );
+                    server->client_confirmed[client_index] = 1;
+                }
+                netcode_packet_queue_push( &server->client_packet_queue[client_index], packet, sequence );
+                return;
+            }
+        }
+        break;
+
+        case NETCODE_CONNECTION_DISCONNECT_PACKET:
+        {
+            if ( client_index != -1 )
+            {
+                netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server received disconnect packet from client %d\n", client_index );
+                netcode_server_disconnect_client_internal( server, client_index, 0 );
+           }
+        }
+        break;
+
+        default:
+            break;
+    }
+
+    server->config.free_function( server->config.allocator_context, packet );
+}
+
+void netcode_server_process_packet( struct netcode_server_t * server, struct netcode_address_t * from, uint8_t * packet_data, int packet_bytes )
+{
+    uint8_t allowed_packets[NETCODE_CONNECTION_NUM_PACKETS];
+    memset( allowed_packets, 0, sizeof( allowed_packets ) );
+    allowed_packets[NETCODE_CONNECTION_REQUEST_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_RESPONSE_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_KEEP_ALIVE_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_PAYLOAD_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_DISCONNECT_PACKET] = 1;
+
+    uint64_t current_timestamp = (uint64_t) time( NULL );
+
+    uint64_t sequence;
+
+    int encryption_index = -1;
+    int client_index = netcode_server_find_client_index_by_address( server, from );
+    if ( client_index != -1 )
+    {
+        netcode_assert( client_index >= 0 );
+        netcode_assert( client_index < server->max_clients );
+        encryption_index = server->client_encryption_index[client_index];
+    }
+    else
+    {
+        encryption_index = netcode_encryption_manager_find_encryption_mapping( &server->encryption_manager, from, server->time );
+    }
+    
+    uint8_t * read_packet_key = netcode_encryption_manager_get_receive_key( &server->encryption_manager, encryption_index );
+
+    if ( !read_packet_key && packet_data[0] != 0 )
+    {
+        char address_string[NETCODE_MAX_ADDRESS_STRING_LENGTH];
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server could not process packet because no encryption mapping exists for %s\n", netcode_address_to_string( from, address_string ) );
+        return;
+    }
+
+    void * packet = netcode_read_packet( packet_data, 
+                                         packet_bytes, 
+                                         &sequence, 
+                                         read_packet_key, 
+                                         server->config.protocol_id, 
+                                         current_timestamp, 
+                                         server->config.private_key, 
+                                         allowed_packets, 
+                                         ( client_index != -1 ) ? &server->client_replay_protection[client_index] : NULL, 
+                                         server->config.allocator_context, 
+                                         server->config.allocate_function );
+
+    if ( !packet )
+        return;
+
+    netcode_server_process_packet_internal( server, from, packet, sequence, encryption_index, client_index );
+}
+
+void netcode_server_read_and_process_packet( struct netcode_server_t * server, 
+                                             struct netcode_address_t * from, 
+                                             uint8_t * packet_data, 
+                                             int packet_bytes, 
+                                             uint64_t current_timestamp, 
+                                             uint8_t * allowed_packets )
+{
+    if ( !server->running )
+        return;
+
+    if ( packet_bytes <= 1 )
+        return;
+
+    uint64_t sequence;
+
+    int encryption_index = -1;
+    int client_index = netcode_server_find_client_index_by_address( server, from );
+    if ( client_index != -1 )
+    {
+        netcode_assert( client_index >= 0 );
+        netcode_assert( client_index < server->max_clients );
+        encryption_index = server->client_encryption_index[client_index];
+    }
+    else
+    {
+        encryption_index = netcode_encryption_manager_find_encryption_mapping( &server->encryption_manager, from, server->time );
+    }
+    
+    uint8_t * read_packet_key = netcode_encryption_manager_get_receive_key( &server->encryption_manager, encryption_index );
+
+    if ( !read_packet_key && packet_data[0] != 0 )
+    {
+        char address_string[NETCODE_MAX_ADDRESS_STRING_LENGTH];
+        netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server could not process packet because no encryption mapping exists for %s\n", netcode_address_to_string( from, address_string ) );
+        return;
+    }
+
+    void * packet = netcode_read_packet( packet_data, 
+                                         packet_bytes, 
+                                         &sequence, 
+                                         read_packet_key, 
+                                         server->config.protocol_id, 
+                                         current_timestamp, 
+                                         server->config.private_key, 
+                                         allowed_packets, 
+                                         ( client_index != -1 ) ? &server->client_replay_protection[client_index] : NULL, 
+                                         server->config.allocator_context, 
+                                         server->config.allocate_function );
+
+    if ( !packet )
+        return;
+
+    netcode_server_process_packet_internal( server, from, packet, sequence, encryption_index, client_index );
+}
+
+void netcode_server_receive_packets( struct netcode_server_t * server )
+{
+    netcode_assert( server );
+
+    uint8_t allowed_packets[NETCODE_CONNECTION_NUM_PACKETS];
+    memset( allowed_packets, 0, sizeof( allowed_packets ) );
+    allowed_packets[NETCODE_CONNECTION_REQUEST_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_RESPONSE_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_KEEP_ALIVE_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_PAYLOAD_PACKET] = 1;
+    allowed_packets[NETCODE_CONNECTION_DISCONNECT_PACKET] = 1;
+
+    uint64_t current_timestamp = (uint64_t) time( NULL );
+
+    if ( !server->config.network_simulator )
+    {
+        // process packets received from socket
+
+        while ( 1 )
+        {
+            struct netcode_address_t from;
+            
+            uint8_t packet_data[NETCODE_MAX_PACKET_BYTES];
+            
+            int packet_bytes = 0;
+            
+            if ( server->config.override_send_and_receive )
+            {
+                packet_bytes = server->config.receive_packet_override( server->config.callback_context, &from, packet_data, NETCODE_MAX_PACKET_BYTES );
+            }
+            else
+            {
+                if (server->socket_holder.ipv4.handle != 0)
+                    packet_bytes = netcode_socket_receive_packet( &server->socket_holder.ipv4, &from, packet_data, NETCODE_MAX_PACKET_BYTES );
+
+                if ( packet_bytes == 0 && server->socket_holder.ipv6.handle != 0)
+                    packet_bytes = netcode_socket_receive_packet( &server->socket_holder.ipv6, &from, packet_data, NETCODE_MAX_PACKET_BYTES );
+            }
+
+            if ( packet_bytes == 0 )
+                break;
+
+            netcode_server_read_and_process_packet( server, &from, packet_data, packet_bytes, current_timestamp, allowed_packets );
+        }
+    }
+    else
+    {
+        // process packets received from network simulator
+
+        int num_packets_received = netcode_network_simulator_receive_packets( server->config.network_simulator, 
+                                                                              &server->address, 
+                                                                              NETCODE_SERVER_MAX_RECEIVE_PACKETS, 
+                                                                              server->receive_packet_data, 
+                                                                              server->receive_packet_bytes, 
+                                                                              server->receive_from );
+
+        int i;
+        for ( i = 0; i < num_packets_received; ++i )
+        {
+            netcode_server_read_and_process_packet( server, 
+                                                    &server->receive_from[i], 
+                                                    server->receive_packet_data[i], 
+                                                    server->receive_packet_bytes[i], 
+                                                    current_timestamp, 
+                                                    allowed_packets );
+
+            server->config.free_function( server->config.allocator_context, server->receive_packet_data[i] );
+        }
+    }
+}
+
+void netcode_server_send_packets( struct netcode_server_t * server )
+{
+    netcode_assert( server );
+
+    if ( !server->running )
+        return;
+
+    int i;
+    for ( i = 0; i < server->max_clients; ++i )
+    {
+        if ( server->client_connected[i] && !server->client_loopback[i] &&
+             ( server->client_last_packet_send_time[i] + ( 1.0 / NETCODE_PACKET_SEND_RATE ) <= server->time ) )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server sent connection keep alive packet to client %d\n", i );
+            struct netcode_connection_keep_alive_packet_t packet;
+            packet.packet_type = NETCODE_CONNECTION_KEEP_ALIVE_PACKET;
+            packet.client_index = i;
+            packet.max_clients = server->max_clients;
+            netcode_server_send_client_packet( server, &packet, i );
+        }
+    }
+}
+
+void netcode_server_check_for_timeouts( struct netcode_server_t * server )
+{
+    netcode_assert( server );
+
+    if ( !server->running )
+        return;
+
+    int i;
+    for ( i = 0; i < server->max_clients; ++i )
+    {
+        if ( server->client_connected[i] && server->client_timeout[i] > 0 && !server->client_loopback[i] &&
+             ( server->client_last_packet_receive_time[i] + server->client_timeout[i] <= server->time ) )
+        {
+            netcode_printf( NETCODE_LOG_LEVEL_INFO, "server timed out client %d\n", i );
+            netcode_server_disconnect_client_internal( server, i, 0 );
+            return;
+        }
+    }
+}
+
+int netcode_server_client_connected( struct netcode_server_t * server, int client_index )
+{
+    netcode_assert( server );
+
+    if ( !server->running )
+        return 0;
+
+    if ( client_index < 0 || client_index >= server->max_clients )
+        return 0;
+
+    return server->client_connected[client_index];
+}
+
+uint64_t netcode_server_client_id( struct netcode_server_t * server, int client_index )
+{
+    netcode_assert( server );
+
+    if ( !server->running )
+        return 0;
+
+    if ( client_index < 0 || client_index >= server->max_clients )
+        return 0;
+
+    return server->client_id[client_index];
+}
+
+struct netcode_address_t * netcode_server_client_address( struct netcode_server_t * server, int client_index )
+{
+    netcode_assert( server );
+
+    if (!server->running)
+        return NULL;
+
+    if (client_index < 0 || client_index >= server->max_clients)
+        return NULL;
+
+    return &server->client_address[client_index];
+}
+
+uint64_t netcode_server_next_packet_sequence( struct netcode_server_t * server, int client_index )
+{
+    netcode_assert( client_index >= 0 );
+    netcode_assert( client_index < server->max_clients );
+    if ( !server->client_connected[client_index] )
+        return 0;
+    return server->client_sequence[client_index];    
+}
+
+void netcode_server_send_packet( struct netcode_server_t * server, int client_index, NETCODE_CONST uint8_t * packet_data, int packet_bytes )
+{
+    netcode_assert( server );
+    netcode_assert( packet_data );
+    netcode_assert( packet_bytes >= 0 );
+    netcode_assert( packet_bytes <= NETCODE_MAX_PACKET_SIZE );
+
+    if ( !server->running )
+        return;
+
+    netcode_assert( client_index >= 0 );
+    netcode_assert( client_index < server->max_clients );
+    if ( !server->client_connected[client_index] )
+        return;
+
+    if ( !server->client_loopback[client_index] )
+    {
+        uint8_t buffer[NETCODE_MAX_PAYLOAD_BYTES*2];
+
+        struct netcode_connection_payload_packet_t * packet = (struct netcode_connection_payload_packet_t*) buffer;
+
+        packet->packet_type = NETCODE_CONNECTION_PAYLOAD_PACKET;
+        packet->payload_bytes = packet_bytes;
+        memcpy( packet->payload_data, packet_data, packet_bytes );
+
+        if ( !server->client_confirmed[client_index] )
+        {
+            struct netcode_connection_keep_alive_packet_t keep_alive_packet;
+            keep_alive_packet.packet_type = NETCODE_CONNECTION_KEEP_ALIVE_PACKET;
+            keep_alive_packet.client_index = client_index;
+            keep_alive_packet.max_clients = server->max_clients;
+            netcode_server_send_client_packet( server, &keep_alive_packet, client_index );
+        }
+
+        netcode_server_send_client_packet( server, packet, client_index );
+    }
+    else
+    {
+        netcode_assert( server->config.send_loopback_packet_callback );
+
+        server->config.send_loopback_packet_callback( server->config.callback_context,
+                                                      client_index, 
+                                                      packet_data, 
+                                                      packet_bytes, 
+                                                      server->client_sequence[client_index]++ );
+
+        server->client_last_packet_send_time[client_index] = server->time;
+    }
+}
+
+uint8_t * netcode_server_receive_packet( struct netcode_server_t * server, int client_index, int * packet_bytes, uint64_t * packet_sequence )
+{
+    netcode_assert( server );
+    netcode_assert( packet_bytes );
+
+    if ( !server->running )
+        return NULL;
+
+    if ( !server->client_connected[client_index] )
+        return NULL;
+
+    netcode_assert( client_index >= 0 );
+    netcode_assert( client_index < server->max_clients );
+
+    struct netcode_connection_payload_packet_t * packet = (struct netcode_connection_payload_packet_t*) 
+        netcode_packet_queue_pop( &server->client_packet_queue[client_index], packet_sequence );
+    
+    if ( packet )
+    {
+        netcode_assert( packet->packet_type == NETCODE_CONNECTION_PAYLOAD_PACKET );
+        *packet_bytes = packet->payload_bytes;
+        netcode_assert( *packet_bytes >= 0 );
+        netcode_assert( *packet_bytes <= NETCODE_MAX_PAYLOAD_BYTES );
+        return (uint8_t*) &packet->payload_data;
+    }
+    else
+    {
+        return NULL;
+    }
+}
+
+void netcode_server_free_packet( struct netcode_server_t * server, void * packet )
+{
+    netcode_assert( server );
+    netcode_assert( packet );
+    (void) server;
+    int offset = offsetof( struct netcode_connection_payload_packet_t, payload_data );
+    server->config.free_function( server->config.allocator_context, ( (uint8_t*) packet ) - offset );
+}
+
+int netcode_server_num_connected_clients( struct netcode_server_t * server )
+{
+    netcode_assert( server );
+    return server->num_connected_clients;
+}
+
+void * netcode_server_client_user_data( struct netcode_server_t * server, int client_index )
+{
+    netcode_assert( server );
+    netcode_assert( client_index >= 0 );
+    netcode_assert( client_index < server->max_clients );
+    return server->client_user_data[client_index];
+}
+
+int netcode_server_running( struct netcode_server_t * server )
+{
+    netcode_assert( server );
+    return server->running;
+}
+
+int netcode_server_max_clients( struct netcode_server_t * server )
+{
+    return server->max_clients;
+}
+
+void netcode_server_update( struct netcode_server_t * server, double time )
+{
+    netcode_assert( server );
+    server->time = time;
+    netcode_server_receive_packets( server );
+    netcode_server_send_packets( server );
+    netcode_server_check_for_timeouts( server );
+}
+
+void netcode_server_connect_loopback_client( struct netcode_server_t * server, int client_index, uint64_t client_id, NETCODE_CONST uint8_t * user_data )
+{
+    netcode_assert( server );
+    netcode_assert( client_index >= 0 );
+    netcode_assert( client_index < server->max_clients );
+    netcode_assert( server->running );
+    netcode_assert( !server->client_connected[client_index] );
+
+    server->num_connected_clients++;
+
+    netcode_assert( server->num_connected_clients <= server->max_clients );
+
+    server->client_loopback[client_index] = 1;
+    server->client_connected[client_index] = 1;
+    server->client_confirmed[client_index] = 1;
+    server->client_encryption_index[client_index] = -1;
+    server->client_id[client_index] = client_id;
+    server->client_sequence[client_index] = 0;
+    memset( &server->client_address[client_index], 0, sizeof( struct netcode_address_t ) );
+    server->client_last_packet_send_time[client_index] = server->time;
+    server->client_last_packet_receive_time[client_index] = server->time;
+
+    if ( user_data )
+    {
+        memcpy( server->client_user_data[client_index], user_data, NETCODE_USER_DATA_BYTES );
+    }
+    else
+    {
+        memset( server->client_user_data[client_index], 0, NETCODE_USER_DATA_BYTES );
+    }
+
+    netcode_printf( NETCODE_LOG_LEVEL_INFO, "server connected loopback client %.16" PRIx64 " in slot %d\n", client_id, client_index );
+
+    if ( server->config.connect_disconnect_callback )
+    {
+        server->config.connect_disconnect_callback( server->config.callback_context, client_index, 1 );
+    }
+}
+
+void netcode_server_disconnect_loopback_client( struct netcode_server_t * server, int client_index )
+{
+    netcode_assert( server );
+    netcode_assert( client_index >= 0 );
+    netcode_assert( client_index < server->max_clients );
+    netcode_assert( server->running );
+    netcode_assert( server->client_connected[client_index] );
+    netcode_assert( server->client_loopback[client_index] );
+
+    netcode_printf( NETCODE_LOG_LEVEL_INFO, "server disconnected loopback client %d\n", client_index );
+
+    if ( server->config.connect_disconnect_callback )
+    {
+        server->config.connect_disconnect_callback( server->config.callback_context, client_index, 0 );
+    }
+
+    while ( 1 )
+    {
+        void * packet = netcode_packet_queue_pop( &server->client_packet_queue[client_index], NULL );
+        if ( !packet )
+            break;
+        server->config.free_function( server->config.allocator_context, packet );
+    }
+
+    netcode_packet_queue_clear( &server->client_packet_queue[client_index] );
+
+    server->client_connected[client_index] = 0;
+    server->client_loopback[client_index] = 0;
+    server->client_confirmed[client_index] = 0;
+    server->client_id[client_index] = 0;
+    server->client_sequence[client_index] = 0;
+    server->client_last_packet_send_time[client_index] = 0.0;
+    server->client_last_packet_receive_time[client_index] = 0.0;
+    memset( &server->client_address[client_index], 0, sizeof( struct netcode_address_t ) );
+    server->client_encryption_index[client_index] = -1;
+    memset( server->client_user_data[client_index], 0, NETCODE_USER_DATA_BYTES );
+
+    server->num_connected_clients--;
+
+    netcode_assert( server->num_connected_clients >= 0 );
+}
+
+int netcode_server_client_loopback( struct netcode_server_t * server, int client_index )
+{
+    netcode_assert( server );
+    netcode_assert( server->running );
+    netcode_assert( client_index >= 0 );
+    netcode_assert( client_index < server->max_clients );
+    return server->client_loopback[client_index];
+}
+
+void netcode_server_process_loopback_packet( struct netcode_server_t * server, int client_index, NETCODE_CONST uint8_t * packet_data, int packet_bytes, uint64_t packet_sequence )
+{
+    netcode_assert( server );
+    netcode_assert( client_index >= 0 );
+    netcode_assert( client_index < server->max_clients );
+    netcode_assert( packet_data );
+    netcode_assert( packet_bytes >= 0 );
+    netcode_assert( packet_bytes <= NETCODE_MAX_PACKET_SIZE );
+    netcode_assert( server->client_connected[client_index] );
+    netcode_assert( server->client_loopback[client_index] );
+    netcode_assert( server->running );
+
+    struct netcode_connection_payload_packet_t * packet = netcode_create_payload_packet( packet_bytes, server->config.allocator_context, server->config.allocate_function );
+    if ( !packet )
+        return;
+
+    memcpy( packet->payload_data, packet_data, packet_bytes );
+
+    netcode_printf( NETCODE_LOG_LEVEL_DEBUG, "server processing loopback packet from client %d\n", client_index );
+
+    server->client_last_packet_receive_time[client_index] = server->time;
+
+    netcode_packet_queue_push( &server->client_packet_queue[client_index], packet, packet_sequence );
+}
+
+uint16_t netcode_server_get_port( struct netcode_server_t * server )
+{
+    netcode_assert( server );
+    return server->address.type == NETCODE_ADDRESS_IPV4 ? server->socket_holder.ipv4.address.port : server->socket_holder.ipv6.address.port;
+}
+
+// ----------------------------------------------------------------
+
+int netcode_generate_connect_token( int num_server_addresses, 
+                                    NETCODE_CONST char ** public_server_addresses, 
+                                    NETCODE_CONST char ** internal_server_addresses, 
+                                    int expire_seconds, 
+                                    int timeout_seconds,
+                                    uint64_t client_id, 
+                                    uint64_t protocol_id, 
+                                    NETCODE_CONST uint8_t * private_key, 
+                                    uint8_t * user_data, 
+                                    uint8_t * output_buffer )
+{
+    netcode_assert( num_server_addresses > 0 );
+    netcode_assert( num_server_addresses <= NETCODE_MAX_SERVERS_PER_CONNECT );
+    netcode_assert( public_server_addresses );
+    netcode_assert( internal_server_addresses );
+    netcode_assert( private_key );
+    netcode_assert( user_data );
+    netcode_assert( output_buffer );
+
+    // parse public server addresses
+
+    struct netcode_address_t parsed_public_server_addresses[NETCODE_MAX_SERVERS_PER_CONNECT];
+    int i;
+    for ( i = 0; i < num_server_addresses; ++i )
+    {
+        if ( netcode_parse_address( public_server_addresses[i], &parsed_public_server_addresses[i] ) != NETCODE_OK )
+        {
+            return NETCODE_ERROR;
+        }
+    }
+
+    // parse internal server addresses
+
+     struct netcode_address_t parsed_internal_server_addresses[NETCODE_MAX_SERVERS_PER_CONNECT];
+    for ( i = 0; i < num_server_addresses; ++i )
+    {
+        if ( netcode_parse_address( internal_server_addresses[i], &parsed_internal_server_addresses[i] ) != NETCODE_OK )
+        {
+            return NETCODE_ERROR;
+        }
+    }
+
+    // generate a connect token
+
+    uint8_t nonce[NETCODE_CONNECT_TOKEN_NONCE_BYTES];
+    netcode_generate_nonce(nonce);
+
+    struct netcode_connect_token_private_t connect_token_private;
+    netcode_generate_connect_token_private( &connect_token_private, client_id, timeout_seconds, num_server_addresses, parsed_internal_server_addresses, user_data );
+
+    // write it to a buffer
+
+    uint8_t connect_token_data[NETCODE_CONNECT_TOKEN_PRIVATE_BYTES];
+    netcode_write_connect_token_private( &connect_token_private, connect_token_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+    // encrypt the buffer
+
+    uint64_t create_timestamp = time( NULL );
+    uint64_t expire_timestamp = ( expire_seconds >= 0 ) ? ( create_timestamp + expire_seconds ) : 0xFFFFFFFFFFFFFFFFULL;
+    if ( netcode_encrypt_connect_token_private( connect_token_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES, NETCODE_VERSION_INFO, protocol_id, expire_timestamp, nonce, private_key ) != NETCODE_OK )
+        return NETCODE_ERROR;
+
+    // wrap a connect token around the private connect token data
+
+    struct netcode_connect_token_t connect_token;
+    memcpy( connect_token.version_info, NETCODE_VERSION_INFO, NETCODE_VERSION_INFO_BYTES );
+    connect_token.protocol_id = protocol_id;
+    connect_token.create_timestamp = create_timestamp;
+    connect_token.expire_timestamp = expire_timestamp;
+    memcpy( connect_token.nonce, nonce, NETCODE_CONNECT_TOKEN_NONCE_BYTES );
+    memcpy( connect_token.private_data, connect_token_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+    connect_token.num_server_addresses = num_server_addresses;
+    for ( i = 0; i < num_server_addresses; ++i )
+        connect_token.server_addresses[i] = parsed_public_server_addresses[i];
+    memcpy( connect_token.client_to_server_key, connect_token_private.client_to_server_key, NETCODE_KEY_BYTES );
+    memcpy( connect_token.server_to_client_key, connect_token_private.server_to_client_key, NETCODE_KEY_BYTES );
+    connect_token.timeout_seconds = timeout_seconds;
+
+    // write the connect token to the output buffer
+
+    netcode_write_connect_token( &connect_token, output_buffer, NETCODE_CONNECT_TOKEN_BYTES );
+
+    return NETCODE_OK;
+}
+
+// ---------------------------------------------------------------
+
+#if __APPLE__
+
+// MacOS
+
+#include <unistd.h>
+#include <mach/mach.h>
+#include <mach/mach_time.h>
+
+void netcode_sleep( double time )
+{
+    usleep( (int) ( time * 1000000 ) );
+}
+
+static uint64_t start = 0;
+static mach_timebase_info_data_t timebase_info;
+
+double netcode_time()
+{
+    if ( start == 0 )
+    {
+        mach_timebase_info( &timebase_info );
+        start = mach_absolute_time();
+        return 0.0;
+    }
+    uint64_t current = mach_absolute_time();
+    return ( (double) ( current - start ) ) * ( (double) timebase_info.numer ) / ( (double) timebase_info.denom ) / 1000000000.0;
+}
+
+#elif __linux
+
+// linux
+
+#include <unistd.h>
+
+void netcode_sleep( double time )
+{
+    usleep( (int) ( time * 1000000 ) );
+}
+
+double netcode_time()
+{
+    static double start = -1;
+    if ( start == -1 )
+    {
+        struct timespec ts;
+        clock_gettime( CLOCK_MONOTONIC_RAW, &ts );
+        start = ts.tv_sec + ( (double) ( ts.tv_nsec ) ) / 1000000000.0;
+        return 0.0;
+    }
+    struct timespec ts;
+    clock_gettime( CLOCK_MONOTONIC_RAW, &ts );
+    double current = ts.tv_sec + ( (double) ( ts.tv_nsec ) ) / 1000000000.0;
+    return current - start;
+}
+
+#elif defined( _WIN32 )
+
+// windows
+
+#define NOMINMAX
+#include <windows.h>
+
+void netcode_sleep( double time )
+{
+    int milliseconds = (int) ( time * 1000 );
+    Sleep( milliseconds );
+}
+
+static int timer_initialized = 0;
+static LARGE_INTEGER timer_frequency;
+static LARGE_INTEGER timer_start;
+
+double netcode_time()
+{
+    if ( !timer_initialized )
+    {
+        QueryPerformanceFrequency( &timer_frequency );
+        QueryPerformanceCounter( &timer_start );
+        timer_initialized = 1;
+    }
+    LARGE_INTEGER now;
+    QueryPerformanceCounter( &now );
+    return ( (double) ( now.QuadPart - timer_start.QuadPart ) ) / ( (double) ( timer_frequency.QuadPart ) );
+}
+
+#else
+
+#error unsupported platform!
+
+#endif
+
+// ---------------------------------------------------------------
+
+#if NETCODE_ENABLE_TESTS
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <memory.h>
+#include <time.h>
+
+static void check_handler( NETCODE_CONST char * condition, 
+                           NETCODE_CONST char * function,
+                           NETCODE_CONST char * file,
+                           int line )
+{
+    printf( "check failed: ( %s ), function %s, file %s, line %d\n", condition, function, file, line );
+#ifndef NDEBUG
+    #if defined( __GNUC__ )
+        __builtin_trap();
+    #elif defined( _MSC_VER )
+        __debugbreak();
+    #endif
+#endif
+    exit( 1 );
+}
+
+#define check( condition )                                                                                      \
+do                                                                                                              \
+{                                                                                                               \
+    if ( !(condition) )                                                                                         \
+    {                                                                                                           \
+        check_handler( #condition, (NETCODE_CONST char*) __FUNCTION__, (char*) __FILE__, __LINE__ );            \
+    }                                                                                                           \
+} while(0)
+
+static void test_queue()
+{
+    struct netcode_packet_queue_t queue;
+
+    netcode_packet_queue_init( &queue, NULL, NULL, NULL );
+
+    check( queue.num_packets == 0 );
+    check( queue.start_index == 0 );
+
+    // attempting to pop a packet off an empty queue should return NULL
+
+    check( netcode_packet_queue_pop( &queue, NULL ) == NULL );
+
+    // add some packets to the queue and make sure they pop off in the correct order
+    {
+        #define NUM_PACKETS 100
+
+        void * packets[NUM_PACKETS];
+
+        int i;
+        for ( i = 0; i < NUM_PACKETS; ++i )
+        {
+            packets[i] = malloc( (i+1) * 256 );
+            check( netcode_packet_queue_push( &queue, packets[i], (uint64_t) i ) == 1 );
+        }
+
+        check( queue.num_packets == NUM_PACKETS );
+
+        for ( i = 0; i < NUM_PACKETS; ++i )
+        {
+            uint64_t sequence = 0;
+            void * packet = netcode_packet_queue_pop( &queue, &sequence );
+            check( sequence == (uint64_t) i ) ;
+            check( packet == packets[i] );
+            free( packet );
+        }
+    }
+
+    // after all entries are popped off, the queue is empty, so calls to pop should return NULL
+
+    check( queue.num_packets == 0 );
+
+    check( netcode_packet_queue_pop( &queue, NULL ) == NULL );
+
+    // test that the packet queue can be filled to max capacity
+
+    void * packets[NETCODE_PACKET_QUEUE_SIZE];
+
+    int i;
+    for ( i = 0; i < NETCODE_PACKET_QUEUE_SIZE; ++i )
+    {
+        packets[i] = malloc( i * 256 );
+        check( netcode_packet_queue_push( &queue, packets[i], (uint64_t) i ) == 1 );
+    }
+
+    check( queue.num_packets == NETCODE_PACKET_QUEUE_SIZE );
+
+    // when the queue is full, attempting to push a packet should fail and return 0
+
+    check( netcode_packet_queue_push( &queue, malloc( 100 ), 0 ) == 0 );
+
+    // make sure all packets pop off in the correct order
+
+    for ( i = 0; i < NETCODE_PACKET_QUEUE_SIZE; ++i )
+    {
+        uint64_t sequence = 0;
+        void * packet = netcode_packet_queue_pop( &queue, &sequence );
+        check( sequence == (uint64_t) i );
+        check( packet == packets[i] );
+        free( packet );
+    }
+
+    // add some packets again
+
+    for ( i = 0; i < NETCODE_PACKET_QUEUE_SIZE; ++i )
+    {
+        packets[i] = malloc( i * 256 );
+        check( netcode_packet_queue_push( &queue, packets[i], (uint64_t) i ) == 1 );
+    }
+
+    // clear the queue and make sure that all packets are freed
+
+    netcode_packet_queue_clear( &queue );
+
+    check( queue.start_index == 0 );
+    check( queue.num_packets == 0 );
+    for ( i = 0; i < NETCODE_PACKET_QUEUE_SIZE; ++i )
+        check( queue.packet_data[i] == NULL );
+}
+
+static void test_endian()
+{
+    uint32_t value = 0x11223344;
+
+    char * bytes = (char*) &value;
+
+#if NETCODE_LITTLE_ENDIAN
+
+    check( bytes[0] == 0x44 );
+    check( bytes[1] == 0x33 );
+    check( bytes[2] == 0x22 );
+    check( bytes[3] == 0x11 );
+
+#else // #if NETCODE_LITTLE_ENDIAN
+
+    check( bytes[3] == 0x44 );
+    check( bytes[2] == 0x33 );
+    check( bytes[1] == 0x22 );
+    check( bytes[0] == 0x11 );
+
+#endif // #if NETCODE_LITTLE_ENDIAN
+}
+
+static void test_sequence()
+{
+    check( netcode_sequence_number_bytes_required( 0 ) == 1 );
+    check( netcode_sequence_number_bytes_required( 0x11 ) == 1 );
+    check( netcode_sequence_number_bytes_required( 0x1122 ) == 2 );
+    check( netcode_sequence_number_bytes_required( 0x112233 ) == 3 );
+    check( netcode_sequence_number_bytes_required( 0x11223344 ) == 4 );
+    check( netcode_sequence_number_bytes_required( 0x1122334455 ) == 5 );
+    check( netcode_sequence_number_bytes_required( 0x112233445566 ) == 6 );
+    check( netcode_sequence_number_bytes_required( 0x11223344556677 ) == 7 );
+    check( netcode_sequence_number_bytes_required( 0x1122334455667788 ) == 8 );
+}
+
+static void test_address()
+{
+    {
+        struct netcode_address_t address;
+        check( netcode_parse_address( "", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( "[", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( "[]", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( "[]:", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( ":", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( "1", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( "12", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( "123", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( "1234", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( "1234.0.12313.0000", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( "1234.0.12313.0000.0.0.0.0.0", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( "1312313:123131:1312313:123131:1312313:123131:1312313:123131:1312313:123131:1312313:123131", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( ".", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( "..", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( "...", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( "....", &address ) == NETCODE_ERROR );
+        check( netcode_parse_address( ".....", &address ) == NETCODE_ERROR );
+    }
+
+    {
+        struct netcode_address_t address;
+        check( netcode_parse_address( "107.77.207.77", &address ) == NETCODE_OK );
+        check( address.type == NETCODE_ADDRESS_IPV4 );
+        check( address.port == 0 );
+        check( address.data.ipv4[0] == 107 );
+        check( address.data.ipv4[1] == 77 );
+        check( address.data.ipv4[2] == 207 );
+        check( address.data.ipv4[3] == 77 );
+    }
+
+    {
+        struct netcode_address_t address;
+        check( netcode_parse_address( "127.0.0.1", &address ) == NETCODE_OK );
+        check( address.type == NETCODE_ADDRESS_IPV4 );
+        check( address.port == 0 );
+        check( address.data.ipv4[0] == 127 );
+        check( address.data.ipv4[1] == 0 );
+        check( address.data.ipv4[2] == 0 );
+        check( address.data.ipv4[3] == 1 );
+    }
+
+    {
+        struct netcode_address_t address;
+        check( netcode_parse_address( "107.77.207.77:40000", &address ) == NETCODE_OK );
+        check( address.type == NETCODE_ADDRESS_IPV4 );
+        check( address.port == 40000 );
+        check( address.data.ipv4[0] == 107 );
+        check( address.data.ipv4[1] == 77 );
+        check( address.data.ipv4[2] == 207 );
+        check( address.data.ipv4[3] == 77 );
+    }
+
+    {
+        struct netcode_address_t address;
+        check( netcode_parse_address( "127.0.0.1:40000", &address ) == NETCODE_OK );
+        check( address.type == NETCODE_ADDRESS_IPV4 );
+        check( address.port == 40000 );
+        check( address.data.ipv4[0] == 127 );
+        check( address.data.ipv4[1] == 0 );
+        check( address.data.ipv4[2] == 0 );
+        check( address.data.ipv4[3] == 1 );
+    }
+
+    {
+        struct netcode_address_t address;
+        check( netcode_parse_address( "fe80::202:b3ff:fe1e:8329", &address ) == NETCODE_OK );
+        check( address.type == NETCODE_ADDRESS_IPV6 );
+        check( address.port == 0 );
+        check( address.data.ipv6[0] == 0xfe80 );
+        check( address.data.ipv6[1] == 0x0000 );
+        check( address.data.ipv6[2] == 0x0000 );
+        check( address.data.ipv6[3] == 0x0000 );
+        check( address.data.ipv6[4] == 0x0202 );
+        check( address.data.ipv6[5] == 0xb3ff );
+        check( address.data.ipv6[6] == 0xfe1e );
+        check( address.data.ipv6[7] == 0x8329 );
+    }
+
+    {
+        struct netcode_address_t address;
+        check( netcode_parse_address( "::", &address ) == NETCODE_OK );
+        check( address.type == NETCODE_ADDRESS_IPV6 );
+        check( address.port == 0 );
+        check( address.data.ipv6[0] == 0x0000 );
+        check( address.data.ipv6[1] == 0x0000 );
+        check( address.data.ipv6[2] == 0x0000 );
+        check( address.data.ipv6[3] == 0x0000 );
+        check( address.data.ipv6[4] == 0x0000 );
+        check( address.data.ipv6[5] == 0x0000 );
+        check( address.data.ipv6[6] == 0x0000 );
+        check( address.data.ipv6[7] == 0x0000 );
+    }
+
+    {
+        struct netcode_address_t address;
+        check( netcode_parse_address( "::1", &address ) == NETCODE_OK );
+        check( address.type == NETCODE_ADDRESS_IPV6 );
+        check( address.port == 0 );
+        check( address.data.ipv6[0] == 0x0000 );
+        check( address.data.ipv6[1] == 0x0000 );
+        check( address.data.ipv6[2] == 0x0000 );
+        check( address.data.ipv6[3] == 0x0000 );
+        check( address.data.ipv6[4] == 0x0000 );
+        check( address.data.ipv6[5] == 0x0000 );
+        check( address.data.ipv6[6] == 0x0000 );
+        check( address.data.ipv6[7] == 0x0001 );
+    }
+
+    {
+        struct netcode_address_t address;
+        check( netcode_parse_address( "[fe80::202:b3ff:fe1e:8329]:40000", &address ) == NETCODE_OK );
+        check( address.type == NETCODE_ADDRESS_IPV6 );
+        check( address.port == 40000 );
+        check( address.data.ipv6[0] == 0xfe80 );
+        check( address.data.ipv6[1] == 0x0000 );
+        check( address.data.ipv6[2] == 0x0000 );
+        check( address.data.ipv6[3] == 0x0000 );
+        check( address.data.ipv6[4] == 0x0202 );
+        check( address.data.ipv6[5] == 0xb3ff );
+        check( address.data.ipv6[6] == 0xfe1e );
+        check( address.data.ipv6[7] == 0x8329 );
+    }
+
+    {
+        struct netcode_address_t address;
+        check( netcode_parse_address( "[::]:40000", &address ) == NETCODE_OK );
+        check( address.type == NETCODE_ADDRESS_IPV6 );
+        check( address.port == 40000 );
+        check( address.data.ipv6[0] == 0x0000 );
+        check( address.data.ipv6[1] == 0x0000 );
+        check( address.data.ipv6[2] == 0x0000 );
+        check( address.data.ipv6[3] == 0x0000 );
+        check( address.data.ipv6[4] == 0x0000 );
+        check( address.data.ipv6[5] == 0x0000 );
+        check( address.data.ipv6[6] == 0x0000 );
+        check( address.data.ipv6[7] == 0x0000 );
+    }
+
+    {
+        struct netcode_address_t address;
+        check( netcode_parse_address( "[::1]:40000", &address ) == NETCODE_OK );
+        check( address.type == NETCODE_ADDRESS_IPV6 );
+        check( address.port == 40000 );
+        check( address.data.ipv6[0] == 0x0000 );
+        check( address.data.ipv6[1] == 0x0000 );
+        check( address.data.ipv6[2] == 0x0000 );
+        check( address.data.ipv6[3] == 0x0000 );
+        check( address.data.ipv6[4] == 0x0000 );
+        check( address.data.ipv6[5] == 0x0000 );
+        check( address.data.ipv6[6] == 0x0000 );
+        check( address.data.ipv6[7] == 0x0001 );
+    }
+}
+
+#define TEST_PROTOCOL_ID            0x1122334455667788ULL
+#define TEST_CLIENT_ID              0x1ULL
+#define TEST_SERVER_PORT            40000
+#define TEST_CONNECT_TOKEN_EXPIRY   30
+#define TEST_TIMEOUT_SECONDS        15
+
+static void test_connect_token()
+{
+    // generate a connect token
+
+    struct netcode_address_t server_address;
+    server_address.type = NETCODE_ADDRESS_IPV4;
+    server_address.data.ipv4[0] = 127;
+    server_address.data.ipv4[1] = 0;
+    server_address.data.ipv4[2] = 0;
+    server_address.data.ipv4[3] = 1;
+    server_address.port = TEST_SERVER_PORT;
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes( user_data, NETCODE_USER_DATA_BYTES );
+
+    struct netcode_connect_token_private_t input_token;
+
+    netcode_generate_connect_token_private( &input_token, TEST_CLIENT_ID, TEST_TIMEOUT_SECONDS, 1, &server_address, user_data );
+
+    check( input_token.client_id == TEST_CLIENT_ID );
+    check( input_token.num_server_addresses == 1 );
+    check( memcmp( input_token.user_data, user_data, NETCODE_USER_DATA_BYTES ) == 0 );
+    check( netcode_address_equal( &input_token.server_addresses[0], &server_address ) );
+
+    // write it to a buffer
+
+    uint8_t buffer[NETCODE_CONNECT_TOKEN_PRIVATE_BYTES];
+
+    netcode_write_connect_token_private( &input_token, buffer, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+    // encrypt the buffer
+
+    uint64_t expire_timestamp = time( NULL ) + 30;
+    uint8_t nonce[NETCODE_CONNECT_TOKEN_NONCE_BYTES];
+    netcode_generate_nonce(nonce);
+    uint8_t key[NETCODE_KEY_BYTES];
+    netcode_generate_key( key );    
+
+    check( netcode_encrypt_connect_token_private( buffer, 
+                                                  NETCODE_CONNECT_TOKEN_PRIVATE_BYTES, 
+                                                  NETCODE_VERSION_INFO, 
+                                                  TEST_PROTOCOL_ID, 
+                                                  expire_timestamp, 
+                                                  nonce, 
+                                                  key ) == NETCODE_OK );
+
+    // decrypt the buffer
+
+    check( netcode_decrypt_connect_token_private( buffer, 
+                                                  NETCODE_CONNECT_TOKEN_PRIVATE_BYTES, 
+                                                  NETCODE_VERSION_INFO, 
+                                                  TEST_PROTOCOL_ID, 
+                                                  expire_timestamp, 
+                                                  nonce, 
+                                                  key ) == NETCODE_OK );
+
+    // read the connect token back in
+
+    struct netcode_connect_token_private_t output_token;
+
+    check( netcode_read_connect_token_private( buffer, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES, &output_token ) == NETCODE_OK );
+
+    // make sure that everything matches the original connect token
+
+    check( output_token.client_id == input_token.client_id );
+    check( output_token.timeout_seconds == input_token.timeout_seconds );
+    check( output_token.num_server_addresses == input_token.num_server_addresses );
+    check( netcode_address_equal( &output_token.server_addresses[0], &input_token.server_addresses[0] ) );
+    check( memcmp( output_token.client_to_server_key, input_token.client_to_server_key, NETCODE_KEY_BYTES ) == 0 );
+    check( memcmp( output_token.server_to_client_key, input_token.server_to_client_key, NETCODE_KEY_BYTES ) == 0 );
+    check( memcmp( output_token.user_data, input_token.user_data, NETCODE_USER_DATA_BYTES ) == 0 );
+}
+
+static void test_challenge_token()
+{
+    // generate a challenge token
+
+    struct netcode_challenge_token_t input_token;
+
+    input_token.client_id = TEST_CLIENT_ID;
+    netcode_random_bytes( input_token.user_data, NETCODE_USER_DATA_BYTES );
+
+    // write it to a buffer
+
+    uint8_t buffer[NETCODE_CHALLENGE_TOKEN_BYTES];
+
+    netcode_write_challenge_token( &input_token, buffer, NETCODE_CHALLENGE_TOKEN_BYTES );
+
+    // encrypt the buffer
+
+    uint64_t sequence = 1000;
+    uint8_t key[NETCODE_KEY_BYTES]; 
+    netcode_generate_key( key );    
+
+    check( netcode_encrypt_challenge_token( buffer, NETCODE_CHALLENGE_TOKEN_BYTES, sequence, key ) == NETCODE_OK );
+
+    // decrypt the buffer
+
+    check( netcode_decrypt_challenge_token( buffer, NETCODE_CHALLENGE_TOKEN_BYTES, sequence, key ) == NETCODE_OK );
+
+    // read the challenge token back in
+
+    struct netcode_challenge_token_t output_token;
+
+    check( netcode_read_challenge_token( buffer, NETCODE_CHALLENGE_TOKEN_BYTES, &output_token ) == NETCODE_OK );
+
+    // make sure that everything matches the original challenge token
+
+    check( output_token.client_id == input_token.client_id );
+    check( memcmp( output_token.user_data, input_token.user_data, NETCODE_USER_DATA_BYTES ) == 0 );
+}
+
+static void test_connection_request_packet()
+{
+    // generate a connect token
+
+    struct netcode_address_t server_address;
+    server_address.type = NETCODE_ADDRESS_IPV4;
+    server_address.data.ipv4[0] = 127;
+    server_address.data.ipv4[1] = 0;
+    server_address.data.ipv4[2] = 0;
+    server_address.data.ipv4[3] = 1;
+    server_address.port = TEST_SERVER_PORT;
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes( user_data, NETCODE_USER_DATA_BYTES );
+
+    struct netcode_connect_token_private_t input_token;
+
+    netcode_generate_connect_token_private( &input_token, TEST_CLIENT_ID, TEST_TIMEOUT_SECONDS, 1, &server_address, user_data );
+
+    check( input_token.client_id == TEST_CLIENT_ID );
+    check( input_token.num_server_addresses == 1 );
+    check( memcmp( input_token.user_data, user_data, NETCODE_USER_DATA_BYTES ) == 0 );
+    check( netcode_address_equal( &input_token.server_addresses[0], &server_address ) );
+
+    // write the conect token to a buffer (non-encrypted)
+
+    uint8_t connect_token_data[NETCODE_CONNECT_TOKEN_PRIVATE_BYTES];
+
+    netcode_write_connect_token_private( &input_token, connect_token_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+    // copy to a second buffer then encrypt it in place (we need the unencrypted token for verification later on)
+
+    uint8_t encrypted_connect_token_data[NETCODE_CONNECT_TOKEN_PRIVATE_BYTES];
+
+    memcpy( encrypted_connect_token_data, connect_token_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+    uint64_t connect_token_expire_timestamp = time( NULL ) + 30;
+    uint8_t connect_token_nonce[NETCODE_CONNECT_TOKEN_NONCE_BYTES];
+    netcode_generate_nonce(connect_token_nonce);
+    uint8_t connect_token_key[NETCODE_KEY_BYTES];
+    netcode_generate_key( connect_token_key );
+
+    check( netcode_encrypt_connect_token_private( encrypted_connect_token_data, 
+                                                  NETCODE_CONNECT_TOKEN_PRIVATE_BYTES, 
+                                                  NETCODE_VERSION_INFO, 
+                                                  TEST_PROTOCOL_ID, 
+                                                  connect_token_expire_timestamp, 
+                                                  connect_token_nonce, 
+                                                  connect_token_key ) == NETCODE_OK );
+
+    // setup a connection request packet wrapping the encrypted connect token
+
+    struct netcode_connection_request_packet_t input_packet;
+
+    input_packet.packet_type = NETCODE_CONNECTION_REQUEST_PACKET;
+    memcpy( input_packet.version_info, NETCODE_VERSION_INFO, NETCODE_VERSION_INFO_BYTES );
+    input_packet.protocol_id = TEST_PROTOCOL_ID;
+    input_packet.connect_token_expire_timestamp = connect_token_expire_timestamp;
+    memcpy( input_packet.connect_token_nonce, connect_token_nonce, NETCODE_CONNECT_TOKEN_NONCE_BYTES );
+    memcpy( input_packet.connect_token_data, encrypted_connect_token_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+    // write the connection request packet to a buffer
+
+    uint8_t buffer[2048];
+
+    uint8_t packet_key[NETCODE_KEY_BYTES];
+
+    netcode_generate_key( packet_key );
+
+    int bytes_written = netcode_write_packet( &input_packet, buffer, sizeof( buffer ), 1000, packet_key, TEST_PROTOCOL_ID );
+
+    check( bytes_written > 0 );
+
+    // read the connection request packet back in from the buffer (the connect token data is decrypted as part of the read packet validation)
+
+    uint64_t sequence = 1000;
+
+    uint8_t allowed_packets[NETCODE_CONNECTION_NUM_PACKETS];
+    memset( allowed_packets, 1, sizeof( allowed_packets ) );
+
+    struct netcode_connection_request_packet_t * output_packet = (struct netcode_connection_request_packet_t*) 
+        netcode_read_packet( buffer, bytes_written, &sequence, packet_key, TEST_PROTOCOL_ID, time( NULL ), connect_token_key, allowed_packets, NULL, NULL, NULL );
+
+    check( output_packet );
+
+    // make sure the read packet matches what was written
+    
+    check( output_packet->packet_type == NETCODE_CONNECTION_REQUEST_PACKET );
+    check( memcmp( output_packet->version_info, input_packet.version_info, NETCODE_VERSION_INFO_BYTES ) == 0 );
+    check( output_packet->protocol_id == input_packet.protocol_id );
+    check( output_packet->connect_token_expire_timestamp == input_packet.connect_token_expire_timestamp );
+    check( memcmp( output_packet->connect_token_nonce, input_packet.connect_token_nonce, NETCODE_CONNECT_TOKEN_NONCE_BYTES ) == 0 );
+    check( memcmp( output_packet->connect_token_data, connect_token_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES - NETCODE_MAC_BYTES ) == 0 );
+
+    free( output_packet );
+}
+
+void test_connection_denied_packet()
+{
+    // setup a connection denied packet
+
+    struct netcode_connection_denied_packet_t input_packet;
+
+    input_packet.packet_type = NETCODE_CONNECTION_DENIED_PACKET;
+
+    // write the packet to a buffer
+
+    uint8_t buffer[NETCODE_MAX_PACKET_BYTES];
+
+    uint8_t packet_key[NETCODE_KEY_BYTES];
+
+    netcode_generate_key( packet_key );
+
+    int bytes_written = netcode_write_packet( &input_packet, buffer, sizeof( buffer ), 1000, packet_key, TEST_PROTOCOL_ID );
+
+    check( bytes_written > 0 );
+
+    // read the packet back in from the buffer
+
+    uint64_t sequence;
+
+    uint8_t allowed_packet_types[NETCODE_CONNECTION_NUM_PACKETS];
+    memset( allowed_packet_types, 1, sizeof( allowed_packet_types ) );
+
+    struct netcode_connection_denied_packet_t * output_packet = (struct netcode_connection_denied_packet_t*) 
+        netcode_read_packet( buffer, bytes_written, &sequence, packet_key, TEST_PROTOCOL_ID, time( NULL ), NULL, allowed_packet_types, NULL, NULL, NULL );
+
+    check( output_packet );
+
+    // make sure the read packet matches what was written
+    
+    check( output_packet->packet_type == NETCODE_CONNECTION_DENIED_PACKET );
+
+    free( output_packet );
+}
+
+void test_connection_challenge_packet()
+{
+    // setup a connection challenge packet
+
+    struct netcode_connection_challenge_packet_t input_packet;
+
+    input_packet.packet_type = NETCODE_CONNECTION_CHALLENGE_PACKET;
+    input_packet.challenge_token_sequence = 0;
+    netcode_random_bytes( input_packet.challenge_token_data, NETCODE_CHALLENGE_TOKEN_BYTES );
+
+    // write the packet to a buffer
+
+    uint8_t buffer[NETCODE_MAX_PACKET_BYTES];
+
+    uint8_t packet_key[NETCODE_KEY_BYTES];
+
+    netcode_generate_key( packet_key );
+
+    int bytes_written = netcode_write_packet( &input_packet, buffer, sizeof( buffer ), 1000, packet_key, TEST_PROTOCOL_ID );
+
+    check( bytes_written > 0 );
+
+    // read the packet back in from the buffer
+
+    uint64_t sequence;
+
+    uint8_t allowed_packet_types[NETCODE_CONNECTION_NUM_PACKETS];
+    memset( allowed_packet_types, 1, sizeof( allowed_packet_types ) );
+
+    struct netcode_connection_challenge_packet_t * output_packet = (struct netcode_connection_challenge_packet_t*) 
+        netcode_read_packet( buffer, bytes_written, &sequence, packet_key, TEST_PROTOCOL_ID, time( NULL ), NULL, allowed_packet_types, NULL, NULL, NULL );
+
+    check( output_packet );
+
+    // make sure the read packet packet matches what was written
+    
+    check( output_packet->packet_type == NETCODE_CONNECTION_CHALLENGE_PACKET );
+    check( output_packet->challenge_token_sequence == input_packet.challenge_token_sequence );
+    check( memcmp( output_packet->challenge_token_data, input_packet.challenge_token_data, NETCODE_CHALLENGE_TOKEN_BYTES ) == 0 );
+
+    free( output_packet );
+}
+
+void test_connection_response_packet()
+{
+    // setup a connection response packet
+
+    struct netcode_connection_response_packet_t input_packet;
+
+    input_packet.packet_type = NETCODE_CONNECTION_RESPONSE_PACKET;
+    input_packet.challenge_token_sequence = 0;
+    netcode_random_bytes( input_packet.challenge_token_data, NETCODE_CHALLENGE_TOKEN_BYTES );
+
+    // write the packet to a buffer
+
+    uint8_t buffer[NETCODE_MAX_PACKET_BYTES];
+
+    uint8_t packet_key[NETCODE_KEY_BYTES];
+
+    netcode_generate_key( packet_key );
+    
+    int bytes_written = netcode_write_packet( &input_packet, buffer, sizeof( buffer ), 1000, packet_key, TEST_PROTOCOL_ID );
+
+    check( bytes_written > 0 );
+
+    // read the packet back in from the buffer
+
+    uint64_t sequence;
+
+    uint8_t allowed_packet_types[NETCODE_CONNECTION_NUM_PACKETS];
+    memset( allowed_packet_types, 1, sizeof( allowed_packet_types ) );
+
+    struct netcode_connection_response_packet_t * output_packet = (struct netcode_connection_response_packet_t*) 
+        netcode_read_packet( buffer, bytes_written, &sequence, packet_key, TEST_PROTOCOL_ID, time( NULL ), NULL, allowed_packet_types, NULL, NULL, NULL );
+
+    check( output_packet );
+
+    // make sure the read packet matches what was written
+    
+    check( output_packet->packet_type == NETCODE_CONNECTION_RESPONSE_PACKET );
+    check( output_packet->challenge_token_sequence == input_packet.challenge_token_sequence );
+    check( memcmp( output_packet->challenge_token_data, input_packet.challenge_token_data, NETCODE_CHALLENGE_TOKEN_BYTES ) == 0 );
+
+    free( output_packet );
+}
+
+void test_connection_keep_alive_packet()
+{
+    // setup a connection keep alive packet
+
+    struct netcode_connection_keep_alive_packet_t input_packet;
+
+    input_packet.packet_type = NETCODE_CONNECTION_KEEP_ALIVE_PACKET;
+    input_packet.client_index = 10;
+    input_packet.max_clients = 16;
+
+    // write the packet to a buffer
+
+    uint8_t buffer[NETCODE_MAX_PACKET_BYTES];
+
+    uint8_t packet_key[NETCODE_KEY_BYTES];
+
+    netcode_generate_key( packet_key );
+
+    int bytes_written = netcode_write_packet( &input_packet, buffer, sizeof( buffer ), 1000, packet_key, TEST_PROTOCOL_ID );
+
+    check( bytes_written > 0 );
+
+    // read the packet back in from the buffer
+
+    uint64_t sequence;
+
+    uint8_t allowed_packet_types[NETCODE_CONNECTION_NUM_PACKETS];
+    memset( allowed_packet_types, 1, sizeof( allowed_packet_types ) );
+    
+    struct netcode_connection_keep_alive_packet_t * output_packet = (struct netcode_connection_keep_alive_packet_t*) 
+        netcode_read_packet( buffer, bytes_written, &sequence, packet_key, TEST_PROTOCOL_ID, time( NULL ), NULL, allowed_packet_types, NULL, NULL, NULL );
+
+    check( output_packet );
+
+    // make sure the read packet matches what was written
+    
+    check( output_packet->packet_type == NETCODE_CONNECTION_KEEP_ALIVE_PACKET );
+    check( output_packet->client_index == input_packet.client_index );
+    check( output_packet->max_clients == input_packet.max_clients );
+
+    free( output_packet );
+}
+
+void test_connection_payload_packet()
+{
+    // setup a connection payload packet
+
+    struct netcode_connection_payload_packet_t * input_packet = netcode_create_payload_packet( NETCODE_MAX_PAYLOAD_BYTES, NULL, NULL );
+
+    check( input_packet->packet_type == NETCODE_CONNECTION_PAYLOAD_PACKET );
+    check( input_packet->payload_bytes == NETCODE_MAX_PAYLOAD_BYTES );
+
+    netcode_random_bytes( input_packet->payload_data, NETCODE_MAX_PAYLOAD_BYTES );
+    
+    // write the packet to a buffer
+
+    uint8_t buffer[NETCODE_MAX_PACKET_BYTES];
+
+    uint8_t packet_key[NETCODE_KEY_BYTES];
+
+    netcode_generate_key( packet_key );
+
+    int bytes_written = netcode_write_packet( input_packet, buffer, sizeof( buffer ), 1000, packet_key, TEST_PROTOCOL_ID );
+
+    check( bytes_written > 0 );
+
+    // read the packet back in from the buffer
+
+    uint64_t sequence;
+
+    uint8_t allowed_packet_types[NETCODE_CONNECTION_NUM_PACKETS];
+    memset( allowed_packet_types, 1, sizeof( allowed_packet_types ) );
+
+    struct netcode_connection_payload_packet_t * output_packet = (struct netcode_connection_payload_packet_t*) 
+        netcode_read_packet( buffer, bytes_written, &sequence, packet_key, TEST_PROTOCOL_ID, time( NULL ), NULL, allowed_packet_types, NULL, NULL, NULL );
+
+    check( output_packet );
+
+    // make sure the read packet matches what was written
+    
+    check( output_packet->packet_type == NETCODE_CONNECTION_PAYLOAD_PACKET );
+    check( output_packet->payload_bytes == input_packet->payload_bytes );
+    check( memcmp( output_packet->payload_data, input_packet->payload_data, NETCODE_MAX_PAYLOAD_BYTES ) == 0 );
+
+    free( input_packet );
+    free( output_packet );
+}
+
+void test_connection_disconnect_packet()
+{
+    // setup a connection disconnect packet
+
+    struct netcode_connection_disconnect_packet_t input_packet;
+
+    input_packet.packet_type = NETCODE_CONNECTION_DISCONNECT_PACKET;
+
+    // write the packet to a buffer
+
+    uint8_t buffer[NETCODE_MAX_PACKET_BYTES];
+
+    uint8_t packet_key[NETCODE_KEY_BYTES];
+
+    netcode_generate_key( packet_key );
+
+    int bytes_written = netcode_write_packet( &input_packet, buffer, sizeof( buffer ), 1000, packet_key, TEST_PROTOCOL_ID );
+
+    check( bytes_written > 0 );
+
+    // read the packet back in from the buffer
+
+    uint64_t sequence;
+
+    uint8_t allowed_packet_types[NETCODE_CONNECTION_NUM_PACKETS];
+    memset( allowed_packet_types, 1, sizeof( allowed_packet_types ) );
+
+    struct netcode_connection_disconnect_packet_t * output_packet = (struct netcode_connection_disconnect_packet_t*) 
+        netcode_read_packet( buffer, bytes_written, &sequence, packet_key, TEST_PROTOCOL_ID, time( NULL ), NULL, allowed_packet_types, NULL, NULL, NULL );
+
+    check( output_packet );
+
+    // make sure the read packet matches what was written
+    
+    check( output_packet->packet_type == NETCODE_CONNECTION_DISCONNECT_PACKET );
+
+    free( output_packet );
+}
+
+void test_connect_token_public()
+{
+    // generate a private connect token
+
+    struct netcode_address_t server_address;
+    server_address.type = NETCODE_ADDRESS_IPV4;
+    server_address.data.ipv4[0] = 127;
+    server_address.data.ipv4[1] = 0;
+    server_address.data.ipv4[2] = 0;
+    server_address.data.ipv4[3] = 1;
+    server_address.port = TEST_SERVER_PORT;
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes( user_data, NETCODE_USER_DATA_BYTES );
+
+    struct netcode_connect_token_private_t connect_token_private;
+
+    netcode_generate_connect_token_private( &connect_token_private, TEST_CLIENT_ID, TEST_TIMEOUT_SECONDS, 1, &server_address, user_data );
+
+    check( connect_token_private.client_id == TEST_CLIENT_ID );
+    check( connect_token_private.num_server_addresses == 1 );
+    check( memcmp( connect_token_private.user_data, user_data, NETCODE_USER_DATA_BYTES ) == 0 );
+    check( netcode_address_equal( &connect_token_private.server_addresses[0], &server_address ) );
+
+    // write it to a buffer
+
+    uint8_t connect_token_private_data[NETCODE_CONNECT_TOKEN_PRIVATE_BYTES];
+    netcode_write_connect_token_private( &connect_token_private, connect_token_private_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+
+    // encrypt the buffer
+
+    uint64_t create_timestamp = time( NULL );
+    uint64_t expire_timestamp = create_timestamp + 30;
+    uint8_t connect_token_nonce[NETCODE_CONNECT_TOKEN_NONCE_BYTES];
+    netcode_generate_nonce( connect_token_nonce );    
+    uint8_t key[NETCODE_KEY_BYTES];
+    netcode_generate_key( key );    
+    check( netcode_encrypt_connect_token_private( connect_token_private_data, 
+                                                  NETCODE_CONNECT_TOKEN_PRIVATE_BYTES, 
+                                                  NETCODE_VERSION_INFO, 
+                                                  TEST_PROTOCOL_ID, 
+                                                  expire_timestamp, 
+                                                  connect_token_nonce, 
+                                                  key ) == 1 );
+
+    // wrap a public connect token around the private connect token data
+
+    struct netcode_connect_token_t input_connect_token;
+    memset( &input_connect_token, 0, sizeof( struct netcode_connect_token_t ) );
+    memcpy( input_connect_token.version_info, NETCODE_VERSION_INFO, NETCODE_VERSION_INFO_BYTES );
+    input_connect_token.protocol_id = TEST_PROTOCOL_ID;
+    input_connect_token.create_timestamp = create_timestamp;
+    input_connect_token.expire_timestamp = expire_timestamp;
+    memcpy( input_connect_token.nonce, connect_token_nonce, NETCODE_CONNECT_TOKEN_NONCE_BYTES );
+    memcpy( input_connect_token.private_data, connect_token_private_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES );
+    input_connect_token.num_server_addresses = 1;
+    input_connect_token.server_addresses[0] = server_address;
+    memcpy( input_connect_token.client_to_server_key, connect_token_private.client_to_server_key, NETCODE_KEY_BYTES );
+    memcpy( input_connect_token.server_to_client_key, connect_token_private.server_to_client_key, NETCODE_KEY_BYTES );
+    input_connect_token.timeout_seconds = (int) TEST_TIMEOUT_SECONDS;
+
+    // write the connect token to a buffer
+
+    uint8_t buffer[NETCODE_CONNECT_TOKEN_BYTES];
+    netcode_write_connect_token( &input_connect_token, buffer, NETCODE_CONNECT_TOKEN_BYTES );
+
+    // read the buffer back in
+
+    struct netcode_connect_token_t output_connect_token;
+    memset( &output_connect_token, 0, sizeof( struct netcode_connect_token_t ) );
+    check( netcode_read_connect_token( buffer, NETCODE_CONNECT_TOKEN_BYTES, &output_connect_token ) == 1 );
+
+    // make sure the public connect token matches what was written
+
+    check( memcmp( output_connect_token.version_info, input_connect_token.version_info, NETCODE_VERSION_INFO_BYTES ) == 0 );
+    check( output_connect_token.protocol_id == input_connect_token.protocol_id );
+    check( output_connect_token.create_timestamp == input_connect_token.create_timestamp );
+    check( output_connect_token.expire_timestamp == input_connect_token.expire_timestamp );
+    check( memcmp( output_connect_token.nonce, input_connect_token.nonce, NETCODE_CONNECT_TOKEN_NONCE_BYTES ) == 0 );
+    check( memcmp( output_connect_token.private_data, input_connect_token.private_data, NETCODE_CONNECT_TOKEN_PRIVATE_BYTES ) == 0 );
+    check( output_connect_token.num_server_addresses == input_connect_token.num_server_addresses );
+    check( netcode_address_equal( &output_connect_token.server_addresses[0], &input_connect_token.server_addresses[0] ) );
+    check( memcmp( output_connect_token.client_to_server_key, input_connect_token.client_to_server_key, NETCODE_KEY_BYTES ) == 0 );
+    check( memcmp( output_connect_token.server_to_client_key, input_connect_token.server_to_client_key, NETCODE_KEY_BYTES ) == 0 );
+    check( output_connect_token.timeout_seconds == input_connect_token.timeout_seconds );
+}
+
+void test_encryption_manager()
+{
+    struct netcode_encryption_manager_t encryption_manager;
+
+    netcode_encryption_manager_reset( &encryption_manager );
+
+    double time = 100.0;
+
+    // generate some test encryption mappings
+
+    struct encryption_mapping_t
+    {
+        struct netcode_address_t address;
+        uint8_t send_key[NETCODE_KEY_BYTES];
+        uint8_t receive_key[NETCODE_KEY_BYTES];
+    };
+
+    #define NUM_ENCRYPTION_MAPPINGS 5
+
+    struct encryption_mapping_t encryption_mapping[NUM_ENCRYPTION_MAPPINGS];
+    memset( encryption_mapping, 0, sizeof( encryption_mapping ) );
+    int i;
+    for ( i = 0; i < NUM_ENCRYPTION_MAPPINGS; ++i )
+    {
+        encryption_mapping[i].address.type = NETCODE_ADDRESS_IPV6;
+        encryption_mapping[i].address.data.ipv6[7] = 1;
+        encryption_mapping[i].address.port = ( uint16_t) ( 20000 + i );
+        netcode_generate_key( encryption_mapping[i].send_key );
+        netcode_generate_key( encryption_mapping[i].receive_key );
+    }
+
+    // add the encryption mappings to the manager and make sure they can be looked up by address
+
+    for ( i = 0; i < NUM_ENCRYPTION_MAPPINGS; ++i )
+    {
+        int encryption_index = netcode_encryption_manager_find_encryption_mapping( &encryption_manager, &encryption_mapping[i].address, time );
+
+        check( encryption_index == -1 );
+
+        check( netcode_encryption_manager_get_send_key( &encryption_manager, encryption_index ) == NULL );
+        check( netcode_encryption_manager_get_receive_key( &encryption_manager, encryption_index ) == NULL );
+
+        check( netcode_encryption_manager_add_encryption_mapping( &encryption_manager, 
+                                                                  &encryption_mapping[i].address, 
+                                                                  encryption_mapping[i].send_key, 
+                                                                  encryption_mapping[i].receive_key, 
+                                                                  time, 
+                                                                  -1.0,
+                                                                  TEST_TIMEOUT_SECONDS ) );
+
+        encryption_index = netcode_encryption_manager_find_encryption_mapping( &encryption_manager, &encryption_mapping[i].address, time );
+
+        uint8_t * send_key = netcode_encryption_manager_get_send_key( &encryption_manager, encryption_index );
+        uint8_t * receive_key = netcode_encryption_manager_get_receive_key( &encryption_manager, encryption_index );
+
+        check( send_key );
+        check( receive_key );
+
+        check( memcmp( send_key, encryption_mapping[i].send_key, NETCODE_KEY_BYTES ) == 0 );
+        check( memcmp( receive_key, encryption_mapping[i].receive_key, NETCODE_KEY_BYTES ) == 0 );
+    }
+
+    // removing an encryption mapping that doesn't exist should return 0
+    {
+        struct netcode_address_t address;
+        address.type = NETCODE_ADDRESS_IPV6;
+        address.data.ipv6[7] = 1;
+        address.port = 50000;
+
+        check( netcode_encryption_manager_remove_encryption_mapping( &encryption_manager, &address, time ) == 0 );
+    }
+
+    // remove the first and last encryption mappings
+
+    check( netcode_encryption_manager_remove_encryption_mapping( &encryption_manager, &encryption_mapping[0].address, time ) == 1 );
+
+    check( netcode_encryption_manager_remove_encryption_mapping( &encryption_manager, &encryption_mapping[NUM_ENCRYPTION_MAPPINGS-1].address, time ) == 1 );
+
+    // make sure the encryption mappings that were removed can no longer be looked up by address
+
+    for ( i = 0; i < NUM_ENCRYPTION_MAPPINGS; ++i )
+    {
+        int encryption_index = netcode_encryption_manager_find_encryption_mapping( &encryption_manager, &encryption_mapping[i].address, time );
+
+        uint8_t * send_key = netcode_encryption_manager_get_send_key( &encryption_manager, encryption_index );
+        uint8_t * receive_key = netcode_encryption_manager_get_receive_key( &encryption_manager, encryption_index );
+
+        if ( i != 0 && i != NUM_ENCRYPTION_MAPPINGS - 1 )
+        {
+            check( send_key );
+            check( receive_key );
+
+            check( memcmp( send_key, encryption_mapping[i].send_key, NETCODE_KEY_BYTES ) == 0 );
+            check( memcmp( receive_key, encryption_mapping[i].receive_key, NETCODE_KEY_BYTES ) == 0 );
+        }
+        else
+        {
+            check( !send_key );
+            check( !receive_key );
+        }
+    }
+
+    // add the encryption mappings back in
+    
+    check( netcode_encryption_manager_add_encryption_mapping( &encryption_manager, 
+                                                              &encryption_mapping[0].address, 
+                                                              encryption_mapping[0].send_key, 
+                                                              encryption_mapping[0].receive_key, 
+                                                              time, 
+                                                              -1.0,
+                                                              TEST_TIMEOUT_SECONDS ) );
+    
+    check( netcode_encryption_manager_add_encryption_mapping( &encryption_manager, 
+                                                              &encryption_mapping[NUM_ENCRYPTION_MAPPINGS-1].address, 
+                                                              encryption_mapping[NUM_ENCRYPTION_MAPPINGS-1].send_key, 
+                                                              encryption_mapping[NUM_ENCRYPTION_MAPPINGS-1].receive_key, 
+                                                              time, 
+                                                              -1.0,
+                                                              TEST_TIMEOUT_SECONDS ) );
+
+    // all encryption mappings should be able to be looked up by address again
+
+    for ( i = 0; i < NUM_ENCRYPTION_MAPPINGS; ++i )
+    {
+        int encryption_index = netcode_encryption_manager_find_encryption_mapping( &encryption_manager, &encryption_mapping[i].address, time );
+
+        uint8_t * send_key = netcode_encryption_manager_get_send_key( &encryption_manager, encryption_index );
+        uint8_t * receive_key = netcode_encryption_manager_get_receive_key( &encryption_manager, encryption_index );
+
+        check( send_key );
+        check( receive_key );
+
+        check( memcmp( send_key, encryption_mapping[i].send_key, NETCODE_KEY_BYTES ) == 0 );
+        check( memcmp( receive_key, encryption_mapping[i].receive_key, NETCODE_KEY_BYTES ) == 0 );
+    }
+
+    // check that encryption mappings time out properly
+
+    time += TEST_TIMEOUT_SECONDS * 2;
+
+    for ( i = 0; i < NUM_ENCRYPTION_MAPPINGS; ++i )
+    {
+        int encryption_index = netcode_encryption_manager_find_encryption_mapping( &encryption_manager, &encryption_mapping[i].address, time );
+
+        uint8_t * send_key = netcode_encryption_manager_get_send_key( &encryption_manager, encryption_index );
+        uint8_t * receive_key = netcode_encryption_manager_get_receive_key( &encryption_manager, encryption_index );
+
+        check( !send_key );
+        check( !receive_key );
+    }
+
+    // add the same encryption mappings after timeout
+
+    for ( i = 0; i < NUM_ENCRYPTION_MAPPINGS; ++i )
+    {
+        int encryption_index = netcode_encryption_manager_find_encryption_mapping( &encryption_manager, &encryption_mapping[i].address, time );
+
+        check( encryption_index == -1 );
+
+        check( netcode_encryption_manager_get_send_key( &encryption_manager, encryption_index ) == NULL );
+        check( netcode_encryption_manager_get_receive_key( &encryption_manager, encryption_index ) == NULL );
+
+        check( netcode_encryption_manager_add_encryption_mapping( &encryption_manager, 
+                                                                  &encryption_mapping[i].address, 
+                                                                  encryption_mapping[i].send_key, 
+                                                                  encryption_mapping[i].receive_key, 
+                                                                  time, 
+                                                                  -1.0,
+                                                                  TEST_TIMEOUT_SECONDS ) );
+
+        encryption_index = netcode_encryption_manager_find_encryption_mapping( &encryption_manager, &encryption_mapping[i].address, time );
+
+        uint8_t * send_key = netcode_encryption_manager_get_send_key( &encryption_manager, encryption_index );
+        uint8_t * receive_key = netcode_encryption_manager_get_receive_key( &encryption_manager, encryption_index );
+
+        check( send_key );
+        check( receive_key );
+
+        check( memcmp( send_key, encryption_mapping[i].send_key, NETCODE_KEY_BYTES ) == 0 );
+        check( memcmp( receive_key, encryption_mapping[i].receive_key, NETCODE_KEY_BYTES ) == 0 );
+    }
+
+    // reset the encryption mapping and verify that all encryption mappings have been removed
+
+    netcode_encryption_manager_reset( &encryption_manager );
+
+    for ( i = 0; i < NUM_ENCRYPTION_MAPPINGS; ++i )
+    {
+        int encryption_index = netcode_encryption_manager_find_encryption_mapping( &encryption_manager, &encryption_mapping[i].address, time );
+
+        uint8_t * send_key = netcode_encryption_manager_get_send_key( &encryption_manager, encryption_index );
+        uint8_t * receive_key = netcode_encryption_manager_get_receive_key( &encryption_manager, encryption_index );
+
+        check( !send_key );
+        check( !receive_key );
+    }
+
+    // test the expire time for encryption mapping works as expected
+
+    check( netcode_encryption_manager_add_encryption_mapping( &encryption_manager, 
+                                                              &encryption_mapping[0].address, 
+                                                              encryption_mapping[0].send_key, 
+                                                              encryption_mapping[0].receive_key, 
+                                                              time, 
+                                                              time + 1.0,
+                                                              TEST_TIMEOUT_SECONDS ) );
+
+    int encryption_index = netcode_encryption_manager_find_encryption_mapping( &encryption_manager, &encryption_mapping[0].address, time );
+
+    check( encryption_index != -1 );
+
+    check( netcode_encryption_manager_find_encryption_mapping( &encryption_manager, &encryption_mapping[0].address, time + 1.1f ) == -1 );
+
+    netcode_encryption_manager_set_expire_time( &encryption_manager, encryption_index, -1.0 );
+
+    check( netcode_encryption_manager_find_encryption_mapping( &encryption_manager, &encryption_mapping[0].address, time ) == encryption_index );
+}
+
+void test_replay_protection()
+{
+    struct netcode_replay_protection_t replay_protection;
+
+    int i;
+    for ( i = 0; i < 2; ++i )
+    {
+        netcode_replay_protection_reset( &replay_protection );
+
+        check( replay_protection.most_recent_sequence == 0 );
+
+        // the first time we receive packets, they should not be already received
+
+        #define MAX_SEQUENCE ( NETCODE_REPLAY_PROTECTION_BUFFER_SIZE * 4 )
+
+        uint64_t sequence;
+        for ( sequence = 0; sequence < MAX_SEQUENCE; ++sequence )
+        {
+            check( netcode_replay_protection_already_received( &replay_protection, sequence ) == 0 );
+            netcode_replay_protection_advance_sequence( &replay_protection, sequence );
+        }
+
+        // old packets outside buffer should be considered already received
+
+        check( netcode_replay_protection_already_received( &replay_protection, 0 ) == 1 );
+
+        // packets received a second time should be flagged already received
+
+        for ( sequence = MAX_SEQUENCE - 10; sequence < MAX_SEQUENCE; ++sequence )
+        {
+            check( netcode_replay_protection_already_received( &replay_protection, sequence ) == 1 );
+        }
+
+        // jumping ahead to a much higher sequence should be considered not already received
+
+        check( netcode_replay_protection_already_received( &replay_protection, MAX_SEQUENCE + NETCODE_REPLAY_PROTECTION_BUFFER_SIZE ) == 0 );
+
+        // old packets should be considered already received
+
+        for ( sequence = 0; sequence < MAX_SEQUENCE; ++sequence )
+        {
+            check( netcode_replay_protection_already_received( &replay_protection, sequence ) == 1 );
+        }
+    }
+}
+
+void test_client_create()
+{
+    {
+        struct netcode_client_config_t client_config;
+        netcode_default_client_config( &client_config );
+
+        struct netcode_client_t * client = netcode_client_create( "127.0.0.1:40000", &client_config, 0.0 );
+
+        struct netcode_address_t test_address;
+        netcode_parse_address( "127.0.0.1:40000", &test_address );
+
+        check( client );
+        check( client->socket_holder.ipv4.handle != 0 );
+        check( client->socket_holder.ipv6.handle == 0 );
+        check( netcode_address_equal( &client->address, &test_address ) );
+
+        netcode_client_destroy( client );
+    }
+
+    {
+        struct netcode_client_config_t client_config;
+        netcode_default_client_config( &client_config );
+
+        struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, 0.0 );
+
+        struct netcode_address_t test_address;
+        netcode_parse_address( "[::]:50000", &test_address );
+
+        check( client );
+        check( client->socket_holder.ipv4.handle == 0 );
+        check( client->socket_holder.ipv6.handle != 0 );
+        check( netcode_address_equal( &client->address, &test_address ) );
+
+        netcode_client_destroy( client );
+    }
+
+    {
+        struct netcode_client_config_t client_config;
+        netcode_default_client_config( &client_config );
+
+        struct netcode_client_t * client = netcode_client_create_overload( "127.0.0.1:40000", "[::]:50000", &client_config, 0.0 );
+
+        struct netcode_address_t test_address;
+        netcode_parse_address( "127.0.0.1:40000", &test_address );
+
+        check( client );
+        check( client->socket_holder.ipv4.handle != 0 );
+        check( client->socket_holder.ipv6.handle != 0 );
+        check( netcode_address_equal( &client->address, &test_address ) );
+
+        netcode_client_destroy( client );
+    }
+
+    {
+        struct netcode_client_config_t client_config;
+        netcode_default_client_config( &client_config );
+
+        struct netcode_client_t * client = netcode_client_create_overload( "[::]:50000", "127.0.0.1:40000", &client_config, 0.0 );
+
+        struct netcode_address_t test_address;
+        netcode_parse_address( "[::]:50000", &test_address );
+
+        check( client );
+        check( client->socket_holder.ipv4.handle != 0 );
+        check( client->socket_holder.ipv6.handle != 0 );
+        check( netcode_address_equal( &client->address, &test_address ) );
+
+        netcode_client_destroy( client );
+    }
+}
+
+void test_server_create()
+{
+    {
+        struct netcode_server_config_t server_config;
+        netcode_default_server_config( &server_config );
+
+        struct netcode_server_t * server = netcode_server_create( "127.0.0.1:40000", &server_config, 0.0 );
+
+        struct netcode_address_t test_address;
+        netcode_parse_address( "127.0.0.1:40000", &test_address );
+
+        check( server );
+        check( server->socket_holder.ipv4.handle != 0 );
+        check( server->socket_holder.ipv6.handle == 0 );
+        check( netcode_address_equal( &server->address, &test_address ) );
+
+        netcode_server_destroy( server );
+    }
+
+    {
+        struct netcode_server_config_t server_config;
+        netcode_default_server_config( &server_config );
+
+        struct netcode_server_t * server = netcode_server_create( "[::1]:50000", &server_config, 0.0 );
+
+        struct netcode_address_t test_address;
+        netcode_parse_address( "[::1]:50000", &test_address );
+
+        check( server );
+        check( server->socket_holder.ipv4.handle == 0 );
+        check( server->socket_holder.ipv6.handle != 0 );
+        check( netcode_address_equal( &server->address, &test_address ) );
+
+        netcode_server_destroy( server );
+    }
+
+    {
+        struct netcode_server_config_t server_config;
+        netcode_default_server_config( &server_config );
+
+        struct netcode_server_t * server = netcode_server_create_overload( "127.0.0.1:40000", "[::1]:50000", &server_config, 0.0 );
+
+        struct netcode_address_t test_address;
+        netcode_parse_address( "127.0.0.1:40000", &test_address );
+
+        check( server );
+        check( server->socket_holder.ipv4.handle != 0 );
+        check( server->socket_holder.ipv6.handle != 0 );
+        check( netcode_address_equal( &server->address, &test_address ) );
+
+        netcode_server_destroy( server );
+    }
+
+    {
+        struct netcode_server_config_t server_config;
+        netcode_default_server_config( &server_config );
+
+        struct netcode_server_t * server = netcode_server_create_overload( "[::1]:50000", "127.0.0.1:40000", &server_config, 0.0 );
+
+        struct netcode_address_t test_address;
+        netcode_parse_address( "[::1]:50000", &test_address );
+
+        check( server );
+        check( server->socket_holder.ipv4.handle != 0 );
+        check( server->socket_holder.ipv6.handle != 0 );
+        check( netcode_address_equal( &server->address, &test_address ) );
+
+        netcode_server_destroy( server );
+    }
+}
+
+static uint8_t private_key[NETCODE_KEY_BYTES] = { 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 };
+
+void test_client_server_connect()
+{
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    network_simulator->latency_milliseconds = 250;
+    network_simulator->jitter_milliseconds = 250;
+    network_simulator->packet_loss_percent = 5;
+    network_simulator->duplicate_packet_percent = 10;
+
+    double time = 0.0;
+    double delta_time = 1.0 / 10.0;
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+    check( client );
+
+    struct netcode_server_config_t server_config;
+    netcode_default_server_config( &server_config );
+    server_config.protocol_id = TEST_PROTOCOL_ID;
+    server_config.network_simulator = network_simulator;
+    memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+    struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+    check( server );
+
+    netcode_server_start( server, 1 );
+
+    NETCODE_CONST char * server_address = "[::1]:40000";
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( client, connect_token );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED );
+    check( netcode_client_index( client ) == 0 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    int server_num_packets_received = 0;
+    int client_num_packets_received = 0;
+
+    uint8_t packet_data[NETCODE_MAX_PACKET_SIZE];
+    int i;
+    for ( i = 0; i < NETCODE_MAX_PACKET_SIZE; ++i )
+        packet_data[i] = (uint8_t) i;
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        netcode_client_send_packet( client, packet_data, NETCODE_MAX_PACKET_SIZE );
+
+        netcode_server_send_packet( server, 0, packet_data, NETCODE_MAX_PACKET_SIZE );
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            uint8_t * packet = netcode_client_receive_packet( client, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            (void) packet_sequence;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            client_num_packets_received++;
+            netcode_client_free_packet( client, packet );
+        }
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            void * packet = netcode_server_receive_packet( server, 0, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            (void) packet_sequence;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            server_num_packets_received++;
+            netcode_server_free_packet( server, packet );
+        }
+
+        if ( client_num_packets_received >= 10 && server_num_packets_received >= 10 )
+        {
+            if ( netcode_server_client_connected( server, 0 ) )
+            {
+                netcode_server_disconnect_client( server, 0 );
+            }
+        }
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( client_num_packets_received >= 10 && server_num_packets_received >= 10 );
+
+    netcode_server_destroy( server );
+
+    netcode_client_destroy( client );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+void test_client_server_ipv4_socket_connect()
+{
+    {
+        double time = 0.0;
+        double delta_time = 1.0 / 10.0;
+
+        struct netcode_client_config_t client_config;
+        netcode_default_client_config( &client_config );
+
+        struct netcode_client_t * client = netcode_client_create( "0.0.0.0:50000", &client_config, time );
+
+        check( client );
+
+        struct netcode_server_config_t server_config;
+        netcode_default_server_config( &server_config );
+        server_config.protocol_id = TEST_PROTOCOL_ID;
+        memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+        struct netcode_server_t * server = netcode_server_create( "127.0.0.1:40000", &server_config, time );
+
+        check( server );
+
+        netcode_server_start( server, 1 );
+
+        NETCODE_CONST char * server_address = "127.0.0.1:40000";
+
+        uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+        uint64_t client_id = 0;
+        netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+        uint8_t user_data[NETCODE_USER_DATA_BYTES];
+        netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+        check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+        netcode_client_connect( client, connect_token );
+
+        while ( 1 )
+        {
+            netcode_client_update( client, time );
+
+            netcode_server_update( server, time );
+
+            if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+                break;
+
+            if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+                break;
+
+            time += delta_time;
+        }
+
+        netcode_server_destroy( server );
+
+        netcode_client_destroy( client );
+    }
+
+    {
+        double time = 0.0;
+        double delta_time = 1.0 / 10.0;
+
+        struct netcode_client_config_t client_config;
+        netcode_default_client_config( &client_config );
+
+        struct netcode_client_t * client = netcode_client_create( "0.0.0.0:50000", &client_config, time );
+
+        check( client );
+
+        struct netcode_server_config_t server_config;
+        netcode_default_server_config( &server_config );
+        server_config.protocol_id = TEST_PROTOCOL_ID;
+        memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+        struct netcode_server_t * server = netcode_server_create_overload( "127.0.0.1:40000", "[::1]:40000", &server_config, time );
+
+        check( server );
+
+        netcode_server_start( server, 1 );
+
+        NETCODE_CONST char * server_address = "127.0.0.1:40000";
+
+        uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+        uint64_t client_id = 0;
+        netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+        uint8_t user_data[NETCODE_USER_DATA_BYTES];
+        netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+        check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+        netcode_client_connect( client, connect_token );
+
+        while ( 1 )
+        {
+            netcode_client_update( client, time );
+
+            netcode_server_update( server, time );
+
+            if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+                break;
+
+            if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+                break;
+
+            time += delta_time;
+        }
+
+        netcode_server_destroy( server );
+
+        netcode_client_destroy( client );
+    }
+
+    {
+        double time = 0.0;
+        double delta_time = 1.0 / 10.0;
+
+        struct netcode_client_config_t client_config;
+        netcode_default_client_config( &client_config );
+
+        struct netcode_client_t * client = netcode_client_create_overload( "0.0.0.0:50000", "[::]:50000", &client_config, time );
+
+        check( client );
+
+        struct netcode_server_config_t server_config;
+        netcode_default_server_config( &server_config );
+        server_config.protocol_id = TEST_PROTOCOL_ID;
+        memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+        struct netcode_server_t * server = netcode_server_create( "127.0.0.1:40000", &server_config, time );
+
+        check( server );
+
+        netcode_server_start( server, 1 );
+
+        NETCODE_CONST char * server_address = "127.0.0.1:40000";
+
+        uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+        uint64_t client_id = 0;
+        netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+        uint8_t user_data[NETCODE_USER_DATA_BYTES];
+        netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+        check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+        netcode_client_connect( client, connect_token );
+
+        while ( 1 )
+        {
+            netcode_client_update( client, time );
+
+            netcode_server_update( server, time );
+
+            if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+                break;
+
+            if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+                break;
+
+            time += delta_time;
+        }
+
+        netcode_server_destroy( server );
+
+        netcode_client_destroy( client );
+    }
+
+    {
+        double time = 0.0;
+        double delta_time = 1.0 / 10.0;
+
+        struct netcode_client_config_t client_config;
+        netcode_default_client_config( &client_config );
+
+        struct netcode_client_t * client = netcode_client_create_overload( "0.0.0.0:50000", "[::]:50000", &client_config, time );
+
+        check( client );
+
+        struct netcode_server_config_t server_config;
+        netcode_default_server_config( &server_config );
+        server_config.protocol_id = TEST_PROTOCOL_ID;
+        memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+        struct netcode_server_t * server = netcode_server_create_overload( "127.0.0.1:40000", "[::1]:40000", &server_config, time );
+
+        check( server );
+
+        netcode_server_start( server, 1 );
+
+        NETCODE_CONST char * server_address = "127.0.0.1:40000";
+
+        uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+        uint64_t client_id = 0;
+        netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+        uint8_t user_data[NETCODE_USER_DATA_BYTES];
+        netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+        check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+        netcode_client_connect( client, connect_token );
+
+        while ( 1 )
+        {
+            netcode_client_update( client, time );
+
+            netcode_server_update( server, time );
+
+            if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+                break;
+
+            if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+                break;
+
+            time += delta_time;
+        }
+
+        netcode_server_destroy( server );
+
+        netcode_client_destroy( client );
+    }
+}
+
+void test_client_server_ipv6_socket_connect()
+{
+    {
+        double time = 0.0;
+        double delta_time = 1.0 / 10.0;
+
+        struct netcode_client_config_t client_config;
+        netcode_default_client_config( &client_config );
+
+        struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+        check( client );
+
+        struct netcode_server_config_t server_config;
+        netcode_default_server_config( &server_config );
+        server_config.protocol_id = TEST_PROTOCOL_ID;
+        memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+        struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+        check( server );
+
+        netcode_server_start( server, 1 );
+
+        NETCODE_CONST char * server_address = "[::1]:40000";
+
+        uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+        uint64_t client_id = 0;
+        netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+        uint8_t user_data[NETCODE_USER_DATA_BYTES];
+        netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+        check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+        netcode_client_connect( client, connect_token );
+
+        while ( 1 )
+        {
+            netcode_client_update( client, time );
+
+            netcode_server_update( server, time );
+
+            if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+                break;
+
+            if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+                break;
+
+            time += delta_time;
+        }
+
+        netcode_server_destroy( server );
+
+        netcode_client_destroy( client );
+    }
+    
+    {
+        double time = 0.0;
+        double delta_time = 1.0 / 10.0;
+
+        struct netcode_client_config_t client_config;
+        netcode_default_client_config( &client_config );
+
+        struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+        check( client );
+
+        struct netcode_server_config_t server_config;
+        netcode_default_server_config( &server_config );
+        server_config.protocol_id = TEST_PROTOCOL_ID;
+        memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+        struct netcode_server_t * server = netcode_server_create_overload( "127.0.0.1:40000", "[::1]:40000", &server_config, time );
+
+        check( server );
+
+        netcode_server_start( server, 1 );
+
+        NETCODE_CONST char * server_address = "[::1]:40000";
+
+        uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+        uint64_t client_id = 0;
+        netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+        uint8_t user_data[NETCODE_USER_DATA_BYTES];
+        netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+        check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+        netcode_client_connect( client, connect_token );
+
+        while ( 1 )
+        {
+            netcode_client_update( client, time );
+
+            netcode_server_update( server, time );
+
+            if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+                break;
+
+            if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+                break;
+
+            time += delta_time;
+        }
+
+        netcode_server_destroy( server );
+
+        netcode_client_destroy( client );
+    }
+    
+    {
+        double time = 0.0;
+        double delta_time = 1.0 / 10.0;
+
+        struct netcode_client_config_t client_config;
+        netcode_default_client_config( &client_config );
+
+        struct netcode_client_t * client = netcode_client_create_overload( "0.0.0.0:50000", "[::]:50000", &client_config, time );
+
+        check( client );
+
+        struct netcode_server_config_t server_config;
+        netcode_default_server_config( &server_config );
+        server_config.protocol_id = TEST_PROTOCOL_ID;
+        memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+        struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+        check( server );
+
+        netcode_server_start( server, 1 );
+
+        NETCODE_CONST char * server_address = "[::1]:40000";
+
+        uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+        uint64_t client_id = 0;
+        netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+        uint8_t user_data[NETCODE_USER_DATA_BYTES];
+        netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+        check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+        netcode_client_connect( client, connect_token );
+
+        while ( 1 )
+        {
+            netcode_client_update( client, time );
+
+            netcode_server_update( server, time );
+
+            if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+                break;
+
+            if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+                break;
+
+            time += delta_time;
+        }
+
+        netcode_server_destroy( server );
+
+        netcode_client_destroy( client );
+    }
+    
+    {
+        double time = 0.0;
+        double delta_time = 1.0 / 10.0;
+
+        struct netcode_client_config_t client_config;
+        netcode_default_client_config( &client_config );
+
+        struct netcode_client_t * client = netcode_client_create_overload( "0.0.0.0:50000", "[::]:50000", &client_config, time );
+
+        check( client );
+
+        struct netcode_server_config_t server_config;
+        netcode_default_server_config( &server_config );
+        server_config.protocol_id = TEST_PROTOCOL_ID;
+        memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+        struct netcode_server_t * server = netcode_server_create_overload( "127.0.0.1:40000", "[::1]:40000", &server_config, time );
+
+        check( server );
+
+        netcode_server_start( server, 1 );
+
+        NETCODE_CONST char * server_address = "[::1]:40000";
+
+        uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+        uint64_t client_id = 0;
+        netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+        uint8_t user_data[NETCODE_USER_DATA_BYTES];
+        netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+        check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+        netcode_client_connect( client, connect_token );
+
+        while ( 1 )
+        {
+            netcode_client_update( client, time );
+
+            netcode_server_update( server, time );
+
+            if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+                break;
+
+            if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+                break;
+
+            time += delta_time;
+        }
+
+        netcode_server_destroy( server );
+
+        netcode_client_destroy( client );
+    }
+}
+
+void test_client_server_keep_alive()
+{
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    network_simulator->latency_milliseconds = 250;
+    network_simulator->jitter_milliseconds = 250;
+    network_simulator->packet_loss_percent = 5;
+    network_simulator->duplicate_packet_percent = 10;
+
+    double time = 0.0;
+    double delta_time = 1.0 / 10.0;
+
+    // connect client to server
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+    check( client );
+
+    struct netcode_server_config_t server_config;
+    netcode_default_server_config( &server_config );
+    server_config.protocol_id = TEST_PROTOCOL_ID;
+    server_config.network_simulator = network_simulator;
+    memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+    struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+    check( server );
+
+    netcode_server_start( server, 1 );
+
+    NETCODE_CONST char * server_address = "[::1]:40000";
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( client, connect_token );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED );
+    check( netcode_client_index( client ) == 0 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    // pump the client and server long enough that they would timeout without keep alive packets
+
+    int num_iterations = (int) ceil( 1.25f * TEST_TIMEOUT_SECONDS / delta_time );
+
+    int i;
+    for ( i = 0; i < num_iterations; ++i )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED );
+    check( netcode_client_index( client ) == 0 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    netcode_server_destroy( server );
+
+    netcode_client_destroy( client );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+void test_client_server_multiple_clients()
+{
+    #define NUM_START_STOP_ITERATIONS 3
+
+    int max_clients[NUM_START_STOP_ITERATIONS] = { 2, 32, 5 };
+
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    network_simulator->latency_milliseconds = 250;
+    network_simulator->jitter_milliseconds = 250;
+    network_simulator->packet_loss_percent = 5;
+    network_simulator->duplicate_packet_percent = 10;
+
+    double time = 0.0;
+    double delta_time = 1.0 / 10.0;
+
+    struct netcode_server_config_t server_config;
+    netcode_default_server_config( &server_config );
+    server_config.protocol_id = TEST_PROTOCOL_ID;
+    server_config.network_simulator = network_simulator;
+    memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+    struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+    check( server );
+
+    int i;
+    for ( i = 0; i < NUM_START_STOP_ITERATIONS; ++i )
+    {
+        // start the server with max # of clients for this iteration
+
+        netcode_server_start( server, max_clients[i] );
+
+        // create # of client objects for this iteration and connect to server
+
+        struct netcode_client_t ** client = (struct netcode_client_t **) malloc( sizeof( struct netcode_client_t* ) * max_clients[i] );
+
+        check( client );
+
+        int j;
+        for ( j = 0; j < max_clients[i]; ++j )
+        {
+            char client_address[NETCODE_MAX_ADDRESS_STRING_LENGTH];
+            sprintf( client_address, "[::]:%d", 50000 + j );
+
+            struct netcode_client_config_t client_config;
+            netcode_default_client_config( &client_config );
+            client_config.network_simulator = network_simulator;
+
+            client[j] = netcode_client_create( client_address, &client_config, time );
+
+            check( client[j] );
+
+            uint64_t client_id = j;
+            netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+            NETCODE_CONST char * server_address = "[::1]:40000";
+
+            uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+            uint8_t user_data[NETCODE_USER_DATA_BYTES];
+            netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+            check( netcode_generate_connect_token( 1, 
+                                                   &server_address, 
+                                                   &server_address, 
+                                                   TEST_CONNECT_TOKEN_EXPIRY, 
+                                                   TEST_TIMEOUT_SECONDS,
+                                                   client_id, 
+                                                   TEST_PROTOCOL_ID, 
+                                                   private_key, 
+                                                   user_data, 
+                                                   connect_token ) );
+
+            netcode_client_connect( client[j], connect_token );
+        }
+
+        // make sure all clients can connect
+
+        while ( 1 )
+        {
+            netcode_network_simulator_update( network_simulator, time );
+
+            for ( j = 0; j < max_clients[i]; ++j )
+            {
+                netcode_client_update( client[j], time );
+            }
+
+            netcode_server_update( server, time );
+
+            int num_connected_clients = 0;
+
+            for ( j = 0; j < max_clients[i]; ++j )
+            {
+                if ( netcode_client_state( client[j] ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+                    break;
+
+                if ( netcode_client_state( client[j] ) == NETCODE_CLIENT_STATE_CONNECTED )
+                    num_connected_clients++;
+            }
+
+            if ( num_connected_clients == max_clients[i] )
+                break;
+
+            time += delta_time;
+        }
+
+        check( netcode_server_num_connected_clients( server ) == max_clients[i] );
+
+        for ( j = 0; j < max_clients[i]; ++j )
+        {
+            check( netcode_client_state( client[j] ) == NETCODE_CLIENT_STATE_CONNECTED );
+            check( netcode_server_client_connected( server, j ) == 1 );
+        }
+
+        // make sure all clients can exchange packets with the server
+
+        int * server_num_packets_received = (int*) malloc( sizeof(int) * max_clients[i] );
+        int * client_num_packets_received = (int*) malloc( sizeof(int) * max_clients[i] );
+
+        memset( server_num_packets_received, 0, sizeof(int) * max_clients[i] );
+        memset( client_num_packets_received, 0, sizeof(int) * max_clients[i] );
+
+        uint8_t packet_data[NETCODE_MAX_PACKET_SIZE];
+        for ( j = 0; j < NETCODE_MAX_PACKET_SIZE; ++j )
+            packet_data[j] = (uint8_t) j;
+
+        while ( 1 )
+        {
+            netcode_network_simulator_update( network_simulator, time );
+
+            for ( j = 0; j < max_clients[i]; ++j )
+            {
+                netcode_client_update( client[j], time );
+            }
+
+            netcode_server_update( server, time );
+
+            for ( j = 0; j < max_clients[i]; ++j )
+            {
+                netcode_client_send_packet( client[j], packet_data, NETCODE_MAX_PACKET_SIZE );
+            }
+
+            for ( j = 0; j < max_clients[i]; ++j )
+            {
+                netcode_server_send_packet( server, j, packet_data, NETCODE_MAX_PACKET_SIZE );
+            }
+
+            for ( j = 0; j < max_clients[i]; ++j )
+            {
+                while ( 1 )             
+                {
+                    int packet_bytes;
+                    uint64_t packet_sequence;
+                    uint8_t * packet = netcode_client_receive_packet( client[j], &packet_bytes, &packet_sequence );
+                    if ( !packet )
+                        break;
+                    (void) packet_sequence;
+                    netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+                    netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+                    client_num_packets_received[j]++;
+                    netcode_client_free_packet( client[j], packet );
+                }
+            }
+
+            for ( j = 0; j < max_clients[i]; ++j )
+            {
+                while ( 1 )             
+                {
+                    int packet_bytes;
+                    uint64_t packet_sequence;
+                    void * packet = netcode_server_receive_packet( server, j, &packet_bytes, &packet_sequence );
+                    if ( !packet )
+                        break;
+                    (void) packet_sequence;
+                    netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+                    netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+                    server_num_packets_received[j]++;
+                    netcode_server_free_packet( server, packet );
+                }
+            }
+
+            int num_clients_ready = 0;
+
+            for ( j = 0; j < max_clients[i]; ++j )
+            {
+                if ( client_num_packets_received[j] >= 1 && server_num_packets_received[j] >= 1 )
+                {
+                    num_clients_ready++;
+                }
+            }
+
+            if ( num_clients_ready == max_clients[i] )
+                break;
+
+            for ( j = 0; j < max_clients[i]; ++j )
+            {
+                if ( netcode_client_state( client[j] ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+                    break;
+            }
+
+            time += delta_time;
+        }
+
+        int num_clients_ready = 0;
+
+        for ( j = 0; j < max_clients[i]; ++j )
+        {
+            if ( client_num_packets_received[j] >= 1 && server_num_packets_received[j] >= 1 )
+            {
+                num_clients_ready++;
+            }
+        }
+
+        check( num_clients_ready == max_clients[i] );
+
+        free( server_num_packets_received );
+        free( client_num_packets_received );
+        
+        netcode_network_simulator_reset( network_simulator );
+
+        for ( j = 0; j < max_clients[i]; ++j )
+        {
+            netcode_client_destroy( client[j] );
+        }
+
+        free( client );
+
+        netcode_server_stop( server );
+    }
+
+    netcode_server_destroy( server );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+void test_client_server_multiple_servers()
+{
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    network_simulator->latency_milliseconds = 250;
+    network_simulator->jitter_milliseconds = 250;
+    network_simulator->packet_loss_percent = 5;
+    network_simulator->duplicate_packet_percent = 10;
+
+    double time = 0.0;
+    double delta_time = 1.0 / 10.0;
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+    check( client );
+
+    struct netcode_server_config_t server_config;
+    netcode_default_server_config( &server_config );
+    server_config.protocol_id = TEST_PROTOCOL_ID;
+    server_config.network_simulator = network_simulator;
+    memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+    struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+    check( server );
+
+    netcode_server_start( server, 1 );
+
+    NETCODE_CONST char * server_address[] = { "10.10.10.10:1000", "100.100.100.100:50000", "[::1]:40000" };
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 3, server_address, server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( client, connect_token );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED );
+    check( netcode_client_index( client ) == 0 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    int server_num_packets_received = 0;
+    int client_num_packets_received = 0;
+
+    uint8_t packet_data[NETCODE_MAX_PACKET_SIZE];
+    int i;
+    for ( i = 0; i < NETCODE_MAX_PACKET_SIZE; ++i )
+        packet_data[i] = (uint8_t) i;
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        netcode_client_send_packet( client, packet_data, NETCODE_MAX_PACKET_SIZE );
+
+        netcode_server_send_packet( server, 0, packet_data, NETCODE_MAX_PACKET_SIZE );
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            uint8_t * packet = netcode_client_receive_packet( client, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            (void) packet_sequence;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            client_num_packets_received++;
+            netcode_client_free_packet( client, packet );
+        }
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            void * packet = netcode_server_receive_packet( server, 0, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            server_num_packets_received++;
+            netcode_server_free_packet( server, packet );
+        }
+
+        if ( client_num_packets_received >= 10 && server_num_packets_received >= 10 )
+        {
+            if ( netcode_server_client_connected( server, 0 ) )
+            {
+                netcode_server_disconnect_client( server, 0 );
+            }
+        }
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( client_num_packets_received >= 10 && server_num_packets_received >= 10 );
+
+    netcode_server_destroy( server );
+
+    netcode_client_destroy( client );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+void test_client_error_connect_token_expired()
+{
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    network_simulator->latency_milliseconds = 250;
+    network_simulator->jitter_milliseconds = 250;
+    network_simulator->packet_loss_percent = 5;
+    network_simulator->duplicate_packet_percent = 10;
+
+    double time = 0.0;
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+    check( client );
+
+    NETCODE_CONST char * server_address = "[::1]:40000";
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, 0, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( client, connect_token );
+
+    netcode_client_update( client, time );
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECT_TOKEN_EXPIRED );
+
+    netcode_client_destroy( client );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+void test_client_error_invalid_connect_token()
+{
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    network_simulator->latency_milliseconds = 250;
+    network_simulator->jitter_milliseconds = 250;
+    network_simulator->packet_loss_percent = 5;
+    network_simulator->duplicate_packet_percent = 10;
+
+    double time = 0.0;
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+    check( client );
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+    netcode_random_bytes( connect_token, NETCODE_CONNECT_TOKEN_BYTES );
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    netcode_client_connect( client, connect_token );
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_INVALID_CONNECT_TOKEN );
+
+    netcode_client_destroy( client );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+void test_client_error_connection_timed_out()
+{
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    network_simulator->latency_milliseconds = 250;
+    network_simulator->jitter_milliseconds = 250;
+    network_simulator->packet_loss_percent = 5;
+    network_simulator->duplicate_packet_percent = 10;
+
+    double time = 0.0;
+    double delta_time = 1.0 / 10.0;
+
+    // connect a client to the server
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+    check( client );
+
+    struct netcode_server_config_t server_config;
+    netcode_default_server_config( &server_config );
+    server_config.protocol_id = TEST_PROTOCOL_ID;
+    server_config.network_simulator = network_simulator;
+    memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+    struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+    check( server );
+
+    netcode_server_start( server, 1 );
+
+    NETCODE_CONST char * server_address = "[::1]:40000";
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( client, connect_token );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED );
+    check( netcode_client_index( client ) == 0 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    // now disable updating the server and verify that the client times out
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTION_TIMED_OUT );
+
+    netcode_server_destroy( server );
+
+    netcode_client_destroy( client );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+void test_client_error_connection_response_timeout()
+{
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    network_simulator->latency_milliseconds = 250;
+    network_simulator->jitter_milliseconds = 250;
+    network_simulator->packet_loss_percent = 5;
+    network_simulator->duplicate_packet_percent = 10;
+
+    double time = 0.0;
+    double delta_time = 1.0 / 10.0;
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+    check( client );
+
+    struct netcode_server_config_t server_config;
+    netcode_default_server_config( &server_config );
+    server_config.protocol_id = TEST_PROTOCOL_ID;
+    server_config.network_simulator = network_simulator;
+    memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+    struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+    check( server );
+
+    server->flags = NETCODE_SERVER_FLAG_IGNORE_CONNECTION_RESPONSE_PACKETS;
+
+    netcode_server_start( server, 1 );
+
+    NETCODE_CONST char * server_address = "[::1]:40000";
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( client, connect_token );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED  )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTION_RESPONSE_TIMED_OUT );
+
+    netcode_server_destroy( server );
+
+    netcode_client_destroy( client );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+void test_client_error_connection_request_timeout()
+{
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    network_simulator->latency_milliseconds = 250;
+    network_simulator->jitter_milliseconds = 250;
+    network_simulator->packet_loss_percent = 5;
+    network_simulator->duplicate_packet_percent = 10;
+
+    double time = 0.0;
+    double delta_time = 1.0 / 60.0;
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+    check( client );
+
+    struct netcode_server_config_t server_config;
+    netcode_default_server_config( &server_config );
+    server_config.protocol_id = TEST_PROTOCOL_ID;
+    server_config.network_simulator = network_simulator;
+    memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+    struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+    check( server );
+
+    server->flags = NETCODE_SERVER_FLAG_IGNORE_CONNECTION_REQUEST_PACKETS;
+
+    netcode_server_start( server, 1 );
+
+    NETCODE_CONST char * server_address = "[::1]:40000";
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( client, connect_token );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED  )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTION_REQUEST_TIMED_OUT );
+
+    netcode_server_destroy( server );
+
+    netcode_client_destroy( client );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+void test_client_error_connection_denied()
+{
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    network_simulator->latency_milliseconds = 250;
+    network_simulator->jitter_milliseconds = 250;
+    network_simulator->packet_loss_percent = 5;
+    network_simulator->duplicate_packet_percent = 10;
+
+    // start a server and connect one client
+
+    double time = 0.0;
+    double delta_time = 1.0 / 10.0;
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+    check( client );
+
+    struct netcode_server_config_t server_config;
+    netcode_default_server_config( &server_config );
+    server_config.protocol_id = TEST_PROTOCOL_ID;
+    server_config.network_simulator = network_simulator;
+    memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+    struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+    check( server );
+
+    netcode_server_start( server, 1 );
+
+    NETCODE_CONST char * server_address = "[::1]:40000";
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( client, connect_token );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED );
+    check( netcode_client_index( client ) == 0 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    // now attempt to connect a second client. the connection should be denied.
+
+    struct netcode_client_t * client2 = netcode_client_create( "[::]:50001", &client_config, time );
+
+    check( client2 );
+
+    uint8_t connect_token2[NETCODE_CONNECT_TOKEN_BYTES];
+
+    uint64_t client_id2 = 0;
+    netcode_random_bytes( (uint8_t*) &client_id2, 8 );
+
+    uint8_t user_data2[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data2, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id2, TEST_PROTOCOL_ID, private_key, user_data2, connect_token2 ) );
+
+    netcode_client_connect( client2, connect_token2 );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_client_update( client2, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( client2 ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED );
+    check( netcode_client_state( client2 ) == NETCODE_CLIENT_STATE_CONNECTION_DENIED );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    netcode_server_destroy( server );
+
+    netcode_client_destroy( client );
+    
+    netcode_client_destroy( client2 );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+void test_client_side_disconnect()
+{
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    // start a server and connect one client
+
+    double time = 0.0;
+    double delta_time = 1.0 / 10.0;
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+    check( client );
+
+    struct netcode_server_config_t server_config;
+    netcode_default_server_config( &server_config );
+    server_config.protocol_id = TEST_PROTOCOL_ID;
+    server_config.network_simulator = network_simulator;
+    memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+    struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+    check( server );
+
+    netcode_server_start( server, 1 );
+
+    NETCODE_CONST char * server_address = "[::1]:40000";
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( client, connect_token );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED );
+    check( netcode_client_index( client ) == 0 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    // disconnect client side and verify that the server sees that client disconnect cleanly, rather than timing out.
+
+    netcode_client_disconnect( client );
+
+    int i;
+    for ( i = 0; i < 10; ++i )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_server_client_connected( server, 0 ) == 0 )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_server_client_connected( server, 0 ) == 0 );
+    check( netcode_server_num_connected_clients( server ) == 0 );
+
+    netcode_server_destroy( server );
+
+    netcode_client_destroy( client );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+void test_server_side_disconnect()
+{
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    // start a server and connect one client
+
+    double time = 0.0;
+    double delta_time = 1.0 / 10.0;
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+    check( client );
+
+    struct netcode_server_config_t server_config;
+    netcode_default_server_config( &server_config );
+    server_config.protocol_id = TEST_PROTOCOL_ID;
+    server_config.network_simulator = network_simulator;
+    memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+    struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+    check( server );
+
+    netcode_server_start( server, 1 );
+
+    NETCODE_CONST char * server_address = "[::1]:40000";
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( client, connect_token );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED );
+    check( netcode_client_index( client ) == 0 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    // disconnect server side and verify that the client disconnects cleanly, rather than timing out.
+
+    netcode_server_disconnect_client( server, 0 );
+
+    int i;
+    for ( i = 0; i < 10; ++i )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_DISCONNECTED );
+    check( netcode_server_client_connected( server, 0 ) == 0 );
+    check( netcode_server_num_connected_clients( server ) == 0 );
+
+    netcode_server_destroy( server );
+
+    netcode_client_destroy( client );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+void test_client_reconnect()
+{
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    network_simulator->latency_milliseconds = 250;
+    network_simulator->jitter_milliseconds = 250;
+    network_simulator->packet_loss_percent = 5;
+    network_simulator->duplicate_packet_percent = 10;
+
+    // start a server and connect one client
+
+    double time = 0.0;
+    double delta_time = 1.0 / 10.0;
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+    check( client );
+
+    struct netcode_server_config_t server_config;
+    netcode_default_server_config( &server_config );
+    server_config.protocol_id = TEST_PROTOCOL_ID;
+    server_config.network_simulator = network_simulator;
+    memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+    struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+    check( server );
+
+    netcode_server_start( server, 1 );
+
+    NETCODE_CONST char * server_address = "[::1]:40000";
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( client, connect_token );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED );
+    check( netcode_client_index( client ) == 0 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    // disconnect client on the server-side and wait until client sees the disconnect
+
+    netcode_network_simulator_reset( network_simulator );
+
+    netcode_server_disconnect_client( server, 0 );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_DISCONNECTED );
+    check( netcode_server_client_connected( server, 0 ) == 0 );
+    check( netcode_server_num_connected_clients( server ) == 0 );
+
+    // now reconnect the client and verify they connect
+
+    netcode_network_simulator_reset( network_simulator );
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( client, connect_token );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED );
+    check( netcode_client_index( client ) == 0 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    netcode_server_destroy( server );
+
+    netcode_client_destroy( client );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+struct test_loopback_context_t
+{
+    struct netcode_client_t * client;
+    struct netcode_server_t * server;
+    int num_loopback_packets_sent_to_client;
+    int num_loopback_packets_sent_to_server;
+};
+
+void client_send_loopback_packet_callback( void * _context, int client_index, NETCODE_CONST uint8_t * packet_data, int packet_bytes, uint64_t packet_sequence )
+{
+    (void) packet_sequence;
+    check( _context );
+    check( client_index == 0 );
+    check( packet_data );
+    check( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+    int i;
+    for ( i = 0; i < packet_bytes; ++i )
+    {
+        check( packet_data[i] == (uint8_t) i );
+    }
+    struct test_loopback_context_t * context = (struct test_loopback_context_t*) _context;
+    context->num_loopback_packets_sent_to_server++;
+    netcode_server_process_loopback_packet( context->server, client_index, packet_data, packet_bytes, packet_sequence );
+}
+
+void server_send_loopback_packet_callback( void * _context, int client_index, NETCODE_CONST uint8_t * packet_data, int packet_bytes, uint64_t packet_sequence )
+{
+    (void) packet_sequence;
+    check( _context );
+    check( client_index == 0 );
+    check( packet_data );
+    check( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+    int i;
+    for ( i = 0; i < packet_bytes; ++i )
+    {
+        check( packet_data[i] == (uint8_t) i );
+    }
+    struct test_loopback_context_t * context = (struct test_loopback_context_t*) _context;
+    context->num_loopback_packets_sent_to_client++;
+    netcode_client_process_loopback_packet( context->client, packet_data, packet_bytes, packet_sequence );
+}
+
+void test_disable_timeout()
+{
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    network_simulator->latency_milliseconds = 250;
+    network_simulator->jitter_milliseconds = 250;
+    network_simulator->packet_loss_percent = 5;
+    network_simulator->duplicate_packet_percent = 10;
+
+    double time = 0.0;
+    double delta_time = 1.0 / 10.0;
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * client = netcode_client_create( "[::]:50000", &client_config, time );
+
+    check( client );
+
+    struct netcode_server_config_t server_config;
+    netcode_default_server_config( &server_config );
+    server_config.protocol_id = TEST_PROTOCOL_ID;
+    server_config.network_simulator = network_simulator;
+    memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+    struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+    check( server );
+
+    netcode_server_start( server, 1 );
+
+    NETCODE_CONST char * server_address = "[::1]:40000";
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, -1, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( client, connect_token );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( client ) == NETCODE_CLIENT_STATE_CONNECTED );
+    check( netcode_client_index( client ) == 0 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    int server_num_packets_received = 0;
+    int client_num_packets_received = 0;
+
+    uint8_t packet_data[NETCODE_MAX_PACKET_SIZE];
+    int i;
+    for ( i = 0; i < NETCODE_MAX_PACKET_SIZE; ++i )
+        packet_data[i] = (uint8_t) i;
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( client, time );
+
+        netcode_server_update( server, time );
+
+        netcode_client_send_packet( client, packet_data, NETCODE_MAX_PACKET_SIZE );
+
+        netcode_server_send_packet( server, 0, packet_data, NETCODE_MAX_PACKET_SIZE );
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            uint8_t * packet = netcode_client_receive_packet( client, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            (void) packet_sequence;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            client_num_packets_received++;
+            netcode_client_free_packet( client, packet );
+        }
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            void * packet = netcode_server_receive_packet( server, 0, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            (void) packet_sequence;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            server_num_packets_received++;
+            netcode_server_free_packet( server, packet );
+        }
+
+        if ( client_num_packets_received >= 10 && server_num_packets_received >= 10 )
+        {
+            if ( netcode_server_client_connected( server, 0 ) )
+            {
+                netcode_server_disconnect_client( server, 0 );
+            }
+        }
+
+        if ( netcode_client_state( client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        time += 1000.0f;        // normally this would timeout the client
+    }
+
+    check( client_num_packets_received >= 10 && server_num_packets_received >= 10 );
+
+    netcode_server_destroy( server );
+
+    netcode_client_destroy( client );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+void test_loopback()
+{
+    struct test_loopback_context_t context;
+    memset( &context, 0, sizeof( context ) );
+
+    struct netcode_network_simulator_t * network_simulator = netcode_network_simulator_create( NULL, NULL, NULL );
+
+    network_simulator->latency_milliseconds = 250;
+    network_simulator->jitter_milliseconds = 250;
+    network_simulator->packet_loss_percent = 5;
+    network_simulator->duplicate_packet_percent = 10;
+
+    double time = 0.0;
+    double delta_time = 1.0 / 10.0;
+
+    // start the server
+
+    struct netcode_server_config_t server_config;
+    netcode_default_server_config( &server_config );
+    server_config.protocol_id = TEST_PROTOCOL_ID;
+    server_config.network_simulator = network_simulator;
+    server_config.callback_context = &context;
+    server_config.send_loopback_packet_callback = server_send_loopback_packet_callback;
+    memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );
+
+    struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, time );
+
+    check( server );
+
+    int max_clients = 2;
+
+    netcode_server_start( server, max_clients );
+
+    context.server = server;
+
+    // connect a loopback client in slot 0
+
+    struct netcode_client_config_t client_config;
+    netcode_default_client_config( &client_config );
+    client_config.callback_context = &context;
+    client_config.send_loopback_packet_callback = client_send_loopback_packet_callback;
+    client_config.network_simulator = network_simulator;
+
+    struct netcode_client_t * loopback_client = netcode_client_create( "[::]:50000", &client_config, time );
+    check( loopback_client );
+    netcode_client_connect_loopback( loopback_client, 0, max_clients );
+    context.client = loopback_client;
+
+    check( netcode_client_index( loopback_client ) == 0 );
+    check( netcode_client_loopback( loopback_client ) == 1 );
+    check( netcode_client_max_clients( loopback_client ) == max_clients );
+    check( netcode_client_state( loopback_client ) == NETCODE_CLIENT_STATE_CONNECTED );
+
+    uint64_t client_id = 0;
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+    netcode_server_connect_loopback_client( server, 0, client_id, NULL );
+
+    check( netcode_server_client_loopback( server, 0 ) == 1 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    // connect a regular client in the other slot
+
+    struct netcode_client_t * regular_client = netcode_client_create( "[::]:50001", &client_config, time );
+
+    check( regular_client );
+
+    NETCODE_CONST char * server_address = "[::1]:40000";
+
+    uint8_t connect_token[NETCODE_CONNECT_TOKEN_BYTES];
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+
+    uint8_t user_data[NETCODE_USER_DATA_BYTES];
+    netcode_random_bytes(user_data, NETCODE_USER_DATA_BYTES);
+
+    check( netcode_generate_connect_token( 1, &server_address, &server_address, TEST_CONNECT_TOKEN_EXPIRY, TEST_TIMEOUT_SECONDS, client_id, TEST_PROTOCOL_ID, private_key, user_data, connect_token ) );
+
+    netcode_client_connect( regular_client, connect_token );
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( regular_client, time );
+
+        netcode_server_update( server, time );
+
+        if ( netcode_client_state( regular_client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        if ( netcode_client_state( regular_client ) == NETCODE_CLIENT_STATE_CONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( netcode_client_state( regular_client ) == NETCODE_CLIENT_STATE_CONNECTED );
+    check( netcode_client_index( regular_client ) == 1 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_client_connected( server, 1 ) == 1 );
+    check( netcode_server_client_loopback( server, 0 ) == 1 );
+    check( netcode_server_client_loopback( server, 1 ) == 0 );
+    check( netcode_server_num_connected_clients( server ) == 2 );
+
+    // test that we can exchange packets for the regular client and the loopback client
+
+    int loopback_client_num_packets_received = 0;
+    int loopback_server_num_packets_received = 0;
+    int regular_server_num_packets_received = 0;
+    int regular_client_num_packets_received = 0;
+
+    uint8_t packet_data[NETCODE_MAX_PACKET_SIZE];
+    int i;
+    for ( i = 0; i < NETCODE_MAX_PACKET_SIZE; ++i )
+        packet_data[i] = (uint8_t) i;
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( regular_client, time );
+
+        netcode_server_update( server, time );
+
+        netcode_client_send_packet( loopback_client, packet_data, NETCODE_MAX_PACKET_SIZE );
+
+        netcode_client_send_packet( regular_client, packet_data, NETCODE_MAX_PACKET_SIZE );
+        
+        netcode_server_send_packet( server, 0, packet_data, NETCODE_MAX_PACKET_SIZE );
+        
+        netcode_server_send_packet( server, 1, packet_data, NETCODE_MAX_PACKET_SIZE );
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            uint8_t * packet = netcode_client_receive_packet( loopback_client, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            (void) packet_sequence;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            loopback_client_num_packets_received++;
+            netcode_client_free_packet( loopback_client, packet );
+        }
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            uint8_t * packet = netcode_client_receive_packet( regular_client, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            (void) packet_sequence;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            regular_client_num_packets_received++;
+            netcode_client_free_packet( regular_client, packet );
+        }
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            void * packet = netcode_server_receive_packet( server, 0, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            (void) packet_sequence;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            loopback_server_num_packets_received++;
+            netcode_server_free_packet( server, packet );
+        }
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            void * packet = netcode_server_receive_packet( server, 1, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            (void) packet_sequence;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            regular_server_num_packets_received++;
+            netcode_server_free_packet( server, packet );
+        }
+
+        if ( loopback_client_num_packets_received >= 10 && loopback_server_num_packets_received >= 10 &&
+             regular_client_num_packets_received >= 10 && regular_server_num_packets_received >= 10 )
+            break;
+
+        if ( netcode_client_state( regular_client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( loopback_client_num_packets_received >= 10 );
+    check( loopback_server_num_packets_received >= 10 );
+    check( regular_client_num_packets_received >= 10 );
+    check( regular_server_num_packets_received >= 10 );
+    check( context.num_loopback_packets_sent_to_client >= 10 );
+    check( context.num_loopback_packets_sent_to_server >= 10 );
+
+    // verify that we can disconnect the loopback client
+
+    check( netcode_server_client_loopback( server, 0 ) == 1 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 2 );
+
+    netcode_server_disconnect_loopback_client( server, 0 );    
+
+    check( netcode_server_client_loopback( server, 0 ) == 0 );
+    check( netcode_server_client_connected( server, 0 ) == 0 );
+    check( netcode_server_num_connected_clients( server ) == 1 );
+
+    netcode_client_disconnect_loopback( loopback_client );
+
+    check( netcode_client_state( loopback_client ) == NETCODE_CLIENT_STATE_DISCONNECTED );
+
+    // verify that we can reconnect the loopback client
+
+    netcode_random_bytes( (uint8_t*) &client_id, 8 );
+    netcode_server_connect_loopback_client( server, 0, client_id, NULL );
+
+    check( netcode_server_client_loopback( server, 0 ) == 1 );
+    check( netcode_server_client_loopback( server, 1 ) == 0 );
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_client_connected( server, 1 ) == 1 );
+    check( netcode_server_num_connected_clients( server ) == 2 );
+
+    netcode_client_connect_loopback( loopback_client, 0, max_clients );
+    
+    check( netcode_client_index( loopback_client ) == 0 );
+    check( netcode_client_loopback( loopback_client ) == 1 );
+    check( netcode_client_max_clients( loopback_client ) == max_clients );
+    check( netcode_client_state( loopback_client ) == NETCODE_CLIENT_STATE_CONNECTED );
+
+    // verify that we can exchange packets for both regular and loopback client post reconnect
+
+    loopback_server_num_packets_received = 0;
+    loopback_client_num_packets_received = 0;
+    regular_server_num_packets_received = 0;
+    regular_client_num_packets_received = 0;
+    context.num_loopback_packets_sent_to_client = 0;
+    context.num_loopback_packets_sent_to_server = 0;
+
+    while ( 1 )
+    {
+        netcode_network_simulator_update( network_simulator, time );
+
+        netcode_client_update( regular_client, time );
+
+        netcode_server_update( server, time );
+
+        netcode_client_send_packet( loopback_client, packet_data, NETCODE_MAX_PACKET_SIZE );
+
+        netcode_client_send_packet( regular_client, packet_data, NETCODE_MAX_PACKET_SIZE );
+        
+        netcode_server_send_packet( server, 0, packet_data, NETCODE_MAX_PACKET_SIZE );
+        
+        netcode_server_send_packet( server, 1, packet_data, NETCODE_MAX_PACKET_SIZE );
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            uint8_t * packet = netcode_client_receive_packet( loopback_client, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            (void) packet_sequence;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            loopback_client_num_packets_received++;
+            netcode_client_free_packet( loopback_client, packet );
+        }
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            uint8_t * packet = netcode_client_receive_packet( regular_client, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            (void) packet_sequence;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            regular_client_num_packets_received++;
+            netcode_client_free_packet( regular_client, packet );
+        }
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            void * packet = netcode_server_receive_packet( server, 0, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            (void) packet_sequence;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            loopback_server_num_packets_received++;
+            netcode_server_free_packet( server, packet );
+        }
+
+        while ( 1 )             
+        {
+            int packet_bytes;
+            uint64_t packet_sequence;
+            void * packet = netcode_server_receive_packet( server, 1, &packet_bytes, &packet_sequence );
+            if ( !packet )
+                break;
+            (void) packet_sequence;
+            netcode_assert( packet_bytes == NETCODE_MAX_PACKET_SIZE );
+            netcode_assert( memcmp( packet, packet_data, NETCODE_MAX_PACKET_SIZE ) == 0 );            
+            regular_server_num_packets_received++;
+            netcode_server_free_packet( server, packet );
+        }
+
+        if ( loopback_client_num_packets_received >= 10 && loopback_server_num_packets_received >= 10 &&
+             regular_client_num_packets_received >= 10 && regular_server_num_packets_received >= 10 )
+            break;
+
+        if ( netcode_client_state( regular_client ) <= NETCODE_CLIENT_STATE_DISCONNECTED )
+            break;
+
+        time += delta_time;
+    }
+
+    check( loopback_client_num_packets_received >= 10 );
+    check( loopback_server_num_packets_received >= 10 );
+    check( regular_client_num_packets_received >= 10 );
+    check( regular_server_num_packets_received >= 10 );
+    check( context.num_loopback_packets_sent_to_client >= 10 );
+    check( context.num_loopback_packets_sent_to_server >= 10 );
+
+    // verify the regular client times out but loopback client doesn't
+
+    time += 100000.0;
+
+    netcode_server_update( server, time );
+
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_client_connected( server, 1 ) == 0 );
+
+    netcode_client_update( loopback_client, time );
+
+    check( netcode_client_state( loopback_client ) == NETCODE_CLIENT_STATE_CONNECTED );
+
+    // verify that disconnect all clients leaves loopback clients alone
+
+    netcode_server_disconnect_all_clients( server );
+
+    check( netcode_server_client_connected( server, 0 ) == 1 );
+    check( netcode_server_client_connected( server, 1 ) == 0 );
+    check( netcode_server_client_loopback( server, 0 ) == 1 );
+
+    // clean up
+
+    netcode_client_destroy( regular_client );
+
+    netcode_client_destroy( loopback_client );
+
+    netcode_server_destroy( server );
+
+    netcode_network_simulator_destroy( network_simulator );
+}
+
+#define RUN_TEST( test_function )                                           \
+    do                                                                      \
+    {                                                                       \
+        printf( #test_function "\n" );                                      \
+        test_function();                                                    \
+    }                                                                       \
+    while (0)
+
+void netcode_test()
+{
+    //while ( 1 )
+    {
+        RUN_TEST( test_queue );
+        RUN_TEST( test_endian );
+        RUN_TEST( test_address );
+        RUN_TEST( test_sequence );
+        RUN_TEST( test_connect_token );
+        RUN_TEST( test_challenge_token );
+        RUN_TEST( test_connection_request_packet );
+        RUN_TEST( test_connection_denied_packet );
+        RUN_TEST( test_connection_challenge_packet );
+        RUN_TEST( test_connection_response_packet );
+        RUN_TEST( test_connection_payload_packet );
+        RUN_TEST( test_connection_disconnect_packet );
+        RUN_TEST( test_connect_token_public );
+        RUN_TEST( test_encryption_manager );
+        RUN_TEST( test_replay_protection );
+        RUN_TEST( test_client_create );
+        RUN_TEST( test_server_create );
+        RUN_TEST( test_client_server_connect );
+        RUN_TEST( test_client_server_ipv4_socket_connect );
+        RUN_TEST( test_client_server_ipv6_socket_connect );
+        RUN_TEST( test_client_server_keep_alive );
+        RUN_TEST( test_client_server_multiple_clients );
+        RUN_TEST( test_client_server_multiple_servers );
+        RUN_TEST( test_client_error_connect_token_expired );
+        RUN_TEST( test_client_error_invalid_connect_token );
+        RUN_TEST( test_client_error_connection_timed_out );
+        RUN_TEST( test_client_error_connection_response_timeout );
+        RUN_TEST( test_client_error_connection_request_timeout );
+        RUN_TEST( test_client_error_connection_denied );
+        RUN_TEST( test_client_side_disconnect );
+        RUN_TEST( test_server_side_disconnect );
+        RUN_TEST( test_client_reconnect );
+        RUN_TEST( test_disable_timeout );
+        RUN_TEST( test_loopback );
+    }
+}
+
+#endif // #if NETCODE_ENABLE_TESTS
diff --git a/netcode.io/netcode.h b/netcode.io/netcode.h
new file mode 100644
--- /dev/null
+++ b/netcode.io/netcode.h
@@ -0,0 +1,282 @@
+/*
+    netcode.io reference implementation
+
+    Copyright © 2017 - 2019, The Network Protocol Company, Inc.
+
+    Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+        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 
+           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 
+           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, 
+    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.
+*/
+
+#ifndef NETCODE_H
+#define NETCODE_H
+
+#include <stdint.h>
+
+#if    defined(__386__) || defined(i386)    || defined(__i386__)  \
+    || defined(__X86)   || defined(_M_IX86)                       \
+    || defined(_M_X64)  || defined(__x86_64__)                    \
+    || defined(alpha)   || defined(__alpha) || defined(__alpha__) \
+    || defined(_M_ALPHA)                                          \
+    || defined(ARM)     || defined(_ARM)    || defined(__arm__)   \
+    || defined(WIN32)   || defined(_WIN32)  || defined(__WIN32__) \
+    || defined(_WIN32_WCE) || defined(__NT__)                     \
+    || defined(__MIPSEL__)
+  #define NETCODE_LITTLE_ENDIAN 1
+#else
+  #define NETCODE_BIG_ENDIAN 1
+#endif
+
+#define NETCODE_PLATFORM_WINDOWS    1
+#define NETCODE_PLATFORM_MAC        2
+#define NETCODE_PLATFORM_UNIX       3
+
+#if defined(_WIN32)
+#define NETCODE_PLATFORM NETCODE_PLATFORM_WINDOWS
+#elif defined(__APPLE__)
+#define NETCODE_PLATFORM NETCODE_PLATFORM_MAC
+#else
+#define NETCODE_PLATFORM NETCODE_PLATFORM_UNIX
+#endif
+
+#define NETCODE_CONNECT_TOKEN_BYTES 2048
+#define NETCODE_KEY_BYTES 32
+#define NETCODE_MAC_BYTES 16
+#define NETCODE_USER_DATA_BYTES 256
+#define NETCODE_MAX_SERVERS_PER_CONNECT 32
+
+#define NETCODE_CLIENT_STATE_CONNECT_TOKEN_EXPIRED              -6
+#define NETCODE_CLIENT_STATE_INVALID_CONNECT_TOKEN              -5
+#define NETCODE_CLIENT_STATE_CONNECTION_TIMED_OUT               -4
+#define NETCODE_CLIENT_STATE_CONNECTION_RESPONSE_TIMED_OUT      -3
+#define NETCODE_CLIENT_STATE_CONNECTION_REQUEST_TIMED_OUT       -2
+#define NETCODE_CLIENT_STATE_CONNECTION_DENIED                  -1
+#define NETCODE_CLIENT_STATE_DISCONNECTED                       0
+#define NETCODE_CLIENT_STATE_SENDING_CONNECTION_REQUEST         1
+#define NETCODE_CLIENT_STATE_SENDING_CONNECTION_RESPONSE        2
+#define NETCODE_CLIENT_STATE_CONNECTED                          3
+
+#define NETCODE_MAX_CLIENTS         256
+#define NETCODE_MAX_PACKET_SIZE     1200
+
+#define NETCODE_LOG_LEVEL_NONE      0
+#define NETCODE_LOG_LEVEL_ERROR     1
+#define NETCODE_LOG_LEVEL_INFO      2
+#define NETCODE_LOG_LEVEL_DEBUG     3
+
+#define NETCODE_OK                  1
+#define NETCODE_ERROR               0
+
+#define NETCODE_ADDRESS_NONE        0
+#define NETCODE_ADDRESS_IPV4        1
+#define NETCODE_ADDRESS_IPV6        2
+
+#ifdef __cplusplus
+#define NETCODE_CONST const
+extern "C" {
+#else
+#if defined(__STDC__)
+#define NETCODE_CONST const
+#else
+#define NETCODE_CONST
+#endif
+#endif
+
+int netcode_init();
+
+void netcode_term();
+
+struct netcode_address_t
+{
+    union { uint8_t ipv4[4]; uint16_t ipv6[8]; } data;
+    uint16_t port;
+    uint8_t type;
+};
+
+int netcode_parse_address( NETCODE_CONST char * address_string_in, struct netcode_address_t * address );
+
+char * netcode_address_to_string( struct netcode_address_t * address, char * buffer );
+
+int netcode_address_equal( struct netcode_address_t * a, struct netcode_address_t * b );
+
+struct netcode_client_config_t
+{
+    void * allocator_context;
+    void * (*allocate_function)(void*,uint64_t);
+    void (*free_function)(void*,void*);
+    struct netcode_network_simulator_t * network_simulator;
+    void * callback_context;
+    void (*state_change_callback)(void*,int,int);
+    void (*send_loopback_packet_callback)(void*,int,NETCODE_CONST uint8_t*,int,uint64_t);
+    int override_send_and_receive;
+    void (*send_packet_override)(void*,struct netcode_address_t*,NETCODE_CONST uint8_t*,int);
+    int (*receive_packet_override)(void*,struct netcode_address_t*,uint8_t*,int);
+};
+
+void netcode_default_client_config( struct netcode_client_config_t * config );
+
+struct netcode_client_t * netcode_client_create( NETCODE_CONST char * address, NETCODE_CONST struct netcode_client_config_t * config, double time );
+
+void netcode_client_destroy( struct netcode_client_t * client );
+
+void netcode_client_connect( struct netcode_client_t * client, uint8_t * connect_token );
+
+void netcode_client_update( struct netcode_client_t * client, double time );
+
+uint64_t netcode_client_next_packet_sequence( struct netcode_client_t * client );
+
+void netcode_client_send_packet( struct netcode_client_t * client, NETCODE_CONST uint8_t * packet_data, int packet_bytes );
+
+uint8_t * netcode_client_receive_packet( struct netcode_client_t * client, int * packet_bytes, uint64_t * packet_sequence );
+
+void netcode_client_free_packet( struct netcode_client_t * client, void * packet );
+
+void netcode_client_disconnect( struct netcode_client_t * client );
+
+int netcode_client_state( struct netcode_client_t * client );
+
+int netcode_client_index( struct netcode_client_t * client );
+
+int netcode_client_max_clients( struct netcode_client_t * client );
+
+void netcode_client_connect_loopback( struct netcode_client_t * client, int client_index, int max_clients );
+
+void netcode_client_disconnect_loopback( struct netcode_client_t * client );
+
+void netcode_client_process_packet( struct netcode_client_t * client, struct netcode_address_t * from, uint8_t * packet_data, int packet_bytes );
+
+int netcode_client_loopback( struct netcode_client_t * client );
+
+void netcode_client_process_loopback_packet( struct netcode_client_t * client, NETCODE_CONST uint8_t * packet_data, int packet_bytes, uint64_t packet_sequence );
+
+uint16_t netcode_client_get_port( struct netcode_client_t * client );
+
+struct netcode_address_t * netcode_client_server_address( struct netcode_client_t * client );
+
+int netcode_generate_connect_token( int num_server_addresses, 
+                                    NETCODE_CONST char ** public_server_addresses, 
+                                    NETCODE_CONST char ** internal_server_addresses, 
+                                    int expire_seconds,
+                                    int timeout_seconds, 
+                                    uint64_t client_id, 
+                                    uint64_t protocol_id, 
+                                    NETCODE_CONST uint8_t * private_key, 
+                                    uint8_t * user_data, 
+                                    uint8_t * connect_token );
+
+struct netcode_server_config_t
+{
+    uint64_t protocol_id;
+    uint8_t private_key[NETCODE_KEY_BYTES];
+    void * allocator_context;
+    void * (*allocate_function)(void*,uint64_t);
+    void (*free_function)(void*,void*);
+    struct netcode_network_simulator_t * network_simulator;
+    void * callback_context;
+    void (*connect_disconnect_callback)(void*,int,int);
+    void (*send_loopback_packet_callback)(void*,int,NETCODE_CONST uint8_t*,int,uint64_t);
+    int override_send_and_receive;
+    void (*send_packet_override)(void*,struct netcode_address_t*,NETCODE_CONST uint8_t*,int);
+    int (*receive_packet_override)(void*,struct netcode_address_t*,uint8_t*,int);
+};
+
+void netcode_default_server_config( struct netcode_server_config_t * config );
+
+struct netcode_server_t * netcode_server_create( NETCODE_CONST char * server_address, NETCODE_CONST struct netcode_server_config_t * config, double time );
+
+void netcode_server_destroy( struct netcode_server_t * server );
+
+void netcode_server_start( struct netcode_server_t * server, int max_clients );
+
+void netcode_server_stop( struct netcode_server_t * server );
+
+int netcode_server_running( struct netcode_server_t * server );
+
+int netcode_server_max_clients( struct netcode_server_t * server );
+
+void netcode_server_update( struct netcode_server_t * server, double time );
+
+int netcode_server_client_connected( struct netcode_server_t * server, int client_index );
+
+uint64_t netcode_server_client_id( struct netcode_server_t * server, int client_index );
+
+struct netcode_address_t * netcode_server_client_address( struct netcode_server_t * server, int client_index );
+
+void netcode_server_disconnect_client( struct netcode_server_t * server, int client_index );
+
+void netcode_server_disconnect_all_clients( struct netcode_server_t * server );
+
+uint64_t netcode_server_next_packet_sequence( struct netcode_server_t * server, int client_index );
+
+void netcode_server_send_packet( struct netcode_server_t * server, int client_index, NETCODE_CONST uint8_t * packet_data, int packet_bytes );
+
+uint8_t * netcode_server_receive_packet( struct netcode_server_t * server, int client_index, int * packet_bytes, uint64_t * packet_sequence );
+
+void netcode_server_free_packet( struct netcode_server_t * server, void * packet );
+
+int netcode_server_num_connected_clients( struct netcode_server_t * server );
+
+void * netcode_server_client_user_data( struct netcode_server_t * server, int client_index );
+
+void netcode_server_process_packet( struct netcode_server_t * server, struct netcode_address_t * from, uint8_t * packet_data, int packet_bytes );
+
+void netcode_server_connect_loopback_client( struct netcode_server_t * server, int client_index, uint64_t client_id, NETCODE_CONST uint8_t * user_data );
+
+void netcode_server_disconnect_loopback_client( struct netcode_server_t * server, int client_index );
+
+int netcode_server_client_loopback( struct netcode_server_t * server, int client_index );
+
+void netcode_server_process_loopback_packet( struct netcode_server_t * server, int client_index, NETCODE_CONST uint8_t * packet_data, int packet_bytes, uint64_t packet_sequence );
+
+uint16_t netcode_server_get_port( struct netcode_server_t * server );
+
+void netcode_log_level( int level );
+
+void netcode_set_printf_function( int (*function)( NETCODE_CONST char *, ... ) );
+
+extern void (*netcode_assert_function)( NETCODE_CONST char *, NETCODE_CONST char *, NETCODE_CONST char * file, int line );
+
+#ifndef NDEBUG
+#define netcode_assert( condition )                                                         \
+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*/, 
+                                  int /*line*/ ) );
+
+void netcode_random_bytes( uint8_t * data, int bytes );
+
+void netcode_sleep( double seconds );
+
+double netcode_time();
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // #ifndef NETCODE_H
