I’ve been writing a fully custom (from the ground up) server in C#. In doing so, I noticed just how painful the event system is in FiveM.
Issues I have with it:
-
It uses “msgpack-cli” to serialize the event arguments. While not exactly “bad” (MsgPack protocol is quite good), it’s a very slow version of the protocol serializer.
-
FiveM’s handling of “event invocation” is pretty craptastic at best. When the event arguments are deserialized, basic types are “fine”. (Int, string, byte, char, etc) However, complex types (y’know, objects?) get deserialized to ExpandoObject, which is a pain in the ass to use in general. This is C#! We want strongly typed objects.
-
The API for registering/unregistering events doesn’t lend well to a “component” style implementation, and you’re never quite sure if you have the arguments correct for that random event you’re trying to trigger on the server (or client)
-
[FromSource]Player is a nightmare. It’s always forgotten somehow, somewhere. Really, FiveM should just default the first argument of the callback to a “FromSource” player if it exists.
So I set out to create a strongly-typed easy-to-use server<->client “call” mechanism. (That’s all events are, calls from the client to the server, or the server to the client. Typical RPC requests)
A couple simple examples:
Client:
public NetworkMethod<MapMarker> AddMarkerToServer { get; set; }
public NetworkMethod<List<MapMarker>> GetServerMarkerList { get; set; }
public void Initialize()
{
GetServerMarkerList = new NetworkMethod<List<MapMarker>>("GetServerMarkerList", OnGetServerMarkerList);
AddMarkerToServer = new NetworkMethod<MapMarker>("AddMarkerToServer", OnNewMarkerAddedToServer);
}
...
private void OnNewMarkerAddedToServer(MapMarker m) => Markers.Add(m.Id, m);
// Store markers in dictionary format, for easier lookup from action sequences.
private void OnGetServerMarkerList(List<MapMarker> ms) => Markers = ms.ToDictionary(m => m.Id, m => m);
Server:
public NetworkMethod<MapMarker> AddMarkerToServer { get; set; }
public NetworkMethod<List<MapMarker>> GetServerMarkerList { get; set; }
/// <inheritdoc />
public void Initialize()
{
GetServerMarkerList = new NetworkMethod<List<MapMarker>>("GetServerMarkerList", OnGetServerMarkerList);
AddMarkerToServer = new NetworkMethod<MapMarker>("AddMarkerToServer", OnAddMarkerToServer);
}
private void OnAddMarkerToServer(Player player, MapMarker marker)
{
ServerDbContext.Instance.Markers.AddOrUpdate(marker);
ServerDbContext.Instance.SaveChanges();
// Now update all clients to see the new markers :)
AddMarkerToServer.Invoke(null, marker);
}
private void OnGetServerMarkerList(Player player, List<MapMarker> unused)
{
GetServerMarkerList.Invoke(player, ServerDbContext.Instance.Markers.ToList());
}
To ensure that we can pass complex types back and forth, I am making use of JSON.Net’s JSON serializer. This can easily be switched to MsgPack + Base64 strings if that’s the route you wish to go, but I prefer JSON.Net, as it’s much simpler to work with, with roughly the same performance overhead.
NetworkMethods don’t register your callback directly, instead they pass through a serialization callback first. This ensures that we can deserialize all the objects necessary (if required; see code), and make sure that pesky “[FromSource] Player” exists on all server NetworkMethods. (You are enforced to take a Player as the first argument to your callback, to ensure you always have a reference to the “source” of the event.)
Calling the method is trivial, just call Invoke, and pass it the arguments!
For server NetworkMethods, you can pass “null” for the first argument (the source), to trigger the event for all players on the server.
The actual implementation is as follows (Gist links because of character limits):
TypeCache.cs (Used on both Server and Client)
Client NetworkMethod.cs
Server NetworkMethod.cs
Feel free to leave any comments.
PS; No, I didn’t manually type out all the NetworkEvent wrappers. You think I’m a masochist?!