DanielWillett.UnturnedStandardModuleNetworking 1.0.0-prerelease01

This is a prerelease version of DanielWillett.UnturnedStandardModuleNetworking.
dotnet add package DanielWillett.UnturnedStandardModuleNetworking --version 1.0.0-prerelease01
                    
NuGet\Install-Package DanielWillett.UnturnedStandardModuleNetworking -Version 1.0.0-prerelease01
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DanielWillett.UnturnedStandardModuleNetworking" Version="1.0.0-prerelease01" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DanielWillett.UnturnedStandardModuleNetworking" Version="1.0.0-prerelease01" />
                    
Directory.Packages.props
<PackageReference Include="DanielWillett.UnturnedStandardModuleNetworking" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DanielWillett.UnturnedStandardModuleNetworking --version 1.0.0-prerelease01
                    
#r "nuget: DanielWillett.UnturnedStandardModuleNetworking, 1.0.0-prerelease01"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DanielWillett.UnturnedStandardModuleNetworking@1.0.0-prerelease01
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DanielWillett.UnturnedStandardModuleNetworking&version=1.0.0-prerelease01&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=DanielWillett.UnturnedStandardModuleNetworking&version=1.0.0-prerelease01&prerelease
                    
Install as a Cake Tool

Unturned Standard Module Networking

Unturned Standard Module Networking (or USMN) is a project that aims to standardize how modules implement server/client communication in a way that doesn't break other modules, and cleanly handles situations such as connecting to a vanilla server with a module, or connecting to a server which has a module you don't, and other situations like that which can cause the traditional NetMessages hack to not be reliable.

<PackageReference Include="UnturnedStandardModuleNetworking" Version="*"/>

How to use

  • Install via NuGet
  • Call NetMessagesHook.Initialize() on startup and NetMessagesHook.Unload() on shutdown.
  • Define a message listener class (see below).
  • Create an instance of it in your module and call MessageListenerManager.AddMessageListener(Listener) on startup.
  • Deregister your listener on shutdown using MessageListenerManager.RemoveMessageListener(Listener).

Message Listeners

All module messages are dispatched using Message Listeners. MessageListenerManager handles the lifetime of listeners.

Message Listeners can define a Version so different versions of modules can be backwards-compatible.

Each client and the server keep a local ID for each listener. The first time they invoke the listener on a remote client, they send the full type name of the listener, along with their ID. The next time, they just send their ID. This is done with an optional header.

For the most basic implementation of a listener, inherit MessageListener. If you want some things done for you consider one of the following implementations instead:

MessageListener<TServerMessage, TClientMessage>

Abstract subclass of MessageListener which implements a message table using two enum types, for serverside and clientside messages. These can be the same type, although it wastes bits if messages go unused on either side.

Both enums need to have the [NetEnum] attribute on them.

Implementations should call AddServerMessage and AddClientMessage in the constructor to add message handlers.

public sealed class SampleMessageListener : MessageListener<ServerMessage, ClientMessage>
{
    protected override uint Version => 0;

    public SampleMessageListener()
    {
        AddServerMessage(ServerMessage.Connect, HandleClientConnectRequest);
        AddClientMessage(ClientMessage.Confirm, HandleServerConfirmedConnection);
    }

    public void ClientConnect(string hello)
    {
        SendMessageToServer(ServerMessage.Connect, ENetReliability.Reliable, writer =>
        {
            writer.WriteString(hello);
        });
    }

    private void HandleClientConnectRequest(ITransportConnection transportConnection, NetPakReader reader)
    {
        // invoked on server

        reader.ReadString(out string hello);

        // implementation not shown
    }

    public void ServerConfirm(ITransportConnection c)
    {
        SendMessageToClient(ClientMessage.Confirm, ENetReliability.Reliable, c, writer =>
        {
            writer.WriteInt32(1);
        });
    }

    private void HandleServerConfirmedConnection(NetPakReader reader)
    {
        // invoked on client

        reader.ReadInt32(out int v);

        // implementation not shown
    }
}

RpcMessageListener

Abstract subclass of MessageListener<,> which adds support for vanilla-style generated net methods using the ProjectLiberator.NetGen build-only package.

Optionally, use RpcMessageListener<TServerMessage, TClientMessage> to add extra messages that don't use RPCs. One of each must be allocated to the 'InvokeMethod' action, which is assigned via the base constructor.

Call AddRpcsFromAssembly or AddRpcsFromAssemblies in the constructor to register your RPCs.

public sealed class SampleMessageListener : RpcMessageListener
{
    protected override uint Version => 0;

    public SampleMessageListener()
    {
        AddRpcsFromAssembly(typeof(SampleMessageListener).Assembly);
    }
}

// requires that the 'ProjectLiberator.NetGen' package is installed
public static class PlayerExtensions
{
    private static readonly ClientStaticRpc<string> SendAddPlayer
        = ClientStaticRpc<string>.Get(ReceiveAddPlayer);

    private static readonly ServerInstanceRpc<float> SendFlyRequest
        = ServerInstanceRpc<float>.Get<Player>(p => p.ReceiveFlyRequest);

    extension(Player player)
    {
        public void ClientRequestFly(float speed)
        {
            SendFlyRequest.Invoke(player.GetNetId(), ENetReliability.Unreliable, speed);
        }

        [SteamCall(ESteamCallValidation.ONLY_FROM_OWNER)]
        public void ReceiveFlyRequest(float speed)
        {
            player.StartFlying(flySpeed: speed);
        }

        public static void ServerAddPlayer(string name)
        {
            SendAddPlayer.InvokeAndLoopback(ENetReliability.Reliable, Provider.GatherRemoteClientConnections(), name);
        }

        [SteamCall(ESteamCallValidation.ONLY_FROM_SERVER)]
        public static void ReceiveAddPlayer(string name)
        {
            Provider.clients.Add(new Player(name));
        }
    }
}

How it works

Patching Unturned's NetCode

Detection

USMN uses a 'marker message' to detect whether USMN is present on the remote side.

When a client connects to a server, it first invokes the vanilla GetWorkshopFiles message handler which is the client's way of asking about the server's workshop items and map name.

The server then responds with a DownloadWorkshopFiles message, which contains various info about the server, including workshop items and map name.

USMN writes the marker message at the end of both of these messages, and the receiving side checks to see if that marker message is present.

This is all done in the Patches/InjectMarkerPatches.cs file.

Hooking into the NetMessages Class

USMN adds a few internal message types to NetMessages.clientReadCallbacks and NetMessages.serverReadCallbacks.

To do this, the module has to extend the amount of bits written by the generated NetPak enum functions for any users that are running the module, but write it normally for those that aren't.

Since detection uses GetWorkshopFiles and DownloadWorkshopFiles, these are always written at their original bit size. Any internal messages are mapped to numbers that don't share the first bits with these values. That way when reading the message, we read the original bit size, see if it is one of those messages, and if not we can check to see if the user is running USMN, then if so we read the remaining bits.

When writing messages, we just check to see if the other side has the module, and write with the original bit count if they don't.

This is done in the Patches/MessageNetEnumPatches.cs file.

"Invoke Message Listener" Message

This message invokes a listener remotely with a payload.

Server → Client

If client already knows the server's ID, it just sends that. Otherwise, it sends the server ID, version, and assembly-qualified type name of the listener.

[ Has_Listener_Definition: 1 bit ]
  if (Has_Listener_Definition)
  {
    [ Serverside_ID_Bit_Count: <MessageListenerManager.IdBitCountBitCount> bits ]
  }
[ Listener_ID: <ID_Bit_Count> bits ]
  if (Has_Listener_Definition)
  {
    [ Listener_Type_Name: String with <MessageListenerManager.TypeNameBitCount> bits ]
    [ Listener_Version: 32 bits ]
  }
[ payload ... ]
Client → Server

If the client knows the server's ID, it just sends that. If the client has already sent it's ID mapping but hasn't received the server's ID yet, it sends it's client ID. Otherwise, the client sends it's ID, version, and the type name of the listener.

[ Has_Client_ID: 1 bit ]
  if (Has_Client_ID)
  {
    [ Has_Listener_Definition: 1 bit ]
    [ Clientside_Listener_ID: 32 bits ]
      if (Has_Listener_Definition)
      {
        [ Listener_Type_Name: String with <MessageListenerManager.TypeNameBitCount> bits ]
        [ Listener_Version: 32 bits ]
      }
  }
  else
  {
    [ Serverside_Listener_ID: <Serverside_ID_Bit_Count> bits ]
  }
[ payload ... ]

"Negotiate Listener Details" Message

This message is used to communicate listener info to either side.

Server → Client
[ mode: <MessageListenerManager.NegotiateListenerDetailsModeBitCount> bits ] (enum NegotiateListenerDetailsMode)
  if (mode == TellClientMissingListener)
  {
    [ Clientside_Listener_ID: 32 bits ]
  }
  else if (mode == TellClientListenerId)
  {
    [ Clientside_Listener_ID: 32 bits ]
    [ Serverside_Listener_ID: <Serverside_ID_Bit_Count> bits ]
    [ Listener_Version: 32 bits ]
    [ Listener_Type_Name: String with <MessageListenerManager.TypeNameBitCount> bits ]
  }
Client → Server
[ mode: <MessageListenerManager.NegotiateListenerDetailsModeBitCount> bits ] (enum NegotiateListenerDetailsMode)
  if (mode == TellServerMissingListener)
  {
    [ Serverside_Listener_ID: <Serverside_ID_Bit_Count> bits ]
  }
  else if (mode == RequestServerListenerId)
  {
    [ Clientside_Listener_ID: 32 bits ]
    [ Listener_Version: 32 bits ]
    [ Listener_Type_Name: String with <MessageListenerManager.TypeNameBitCount> bits ]
  }
Modes

This message has a few different sub-messages.

TellClientMissingListener

Server → Client

Tells a client the server is missing a listener and to not send any more messages for it.

Can be a response to an invoke message or RequestServerListenerId.

TellClientListenerId

Server → Client

Tells a client the server's ID for a listener.

Usually a response to RequestServerListenerId.

TellServerMissingListener

Client → Server

Tells the server that this client does't have this listener and any messages for this listener shouldn't be sent.

Usually a response to an invoke message.

RequestServerListenerId

Client → Server

Asks the server to send it's ID for a listener.

Licensed under the MIT license.

Copyright © 2026 Daniel Willett

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-prerelease01 69 9/18/2026

Added analyzer to check for accidental usage of vanilla RPC classes.