DanielWillett.UnturnedStandardModuleNetworking
1.0.0-prerelease01
dotnet add package DanielWillett.UnturnedStandardModuleNetworking --version 1.0.0-prerelease01
NuGet\Install-Package DanielWillett.UnturnedStandardModuleNetworking -Version 1.0.0-prerelease01
<PackageReference Include="DanielWillett.UnturnedStandardModuleNetworking" Version="1.0.0-prerelease01" />
<PackageVersion Include="DanielWillett.UnturnedStandardModuleNetworking" Version="1.0.0-prerelease01" />
<PackageReference Include="DanielWillett.UnturnedStandardModuleNetworking" />
paket add DanielWillett.UnturnedStandardModuleNetworking --version 1.0.0-prerelease01
#r "nuget: DanielWillett.UnturnedStandardModuleNetworking, 1.0.0-prerelease01"
#:package DanielWillett.UnturnedStandardModuleNetworking@1.0.0-prerelease01
#addin nuget:?package=DanielWillett.UnturnedStandardModuleNetworking&version=1.0.0-prerelease01&prerelease
#tool nuget:?package=DanielWillett.UnturnedStandardModuleNetworking&version=1.0.0-prerelease01&prerelease
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 andNetMessagesHook.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.
Legal
Licensed under the MIT license.
Copyright © 2026 Daniel Willett
| Product | Versions 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. |
-
.NETStandard 2.1
- ProjectLiberator.NetGen (>= 1.0.0-rc1)
- System.Runtime.CompilerServices.Unsafe (>= 4.5.0)
- Unturned.MSBuild (>= 1.0.1)
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.