[Code] C# NetworkMethod - Easy Server<->Client Event Handling

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:

  1. 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.

  2. 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.

  3. 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)

  4. [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?!

1 Like

iirc the elements said this is done exactly for this reason. Because this is not just c#. This is used in C#, lua and also needs to work with javascript (for NUI).

seems to me like that’d be a developer’s problem (forgetting to add arguments) rather than something fivem needs to enforce… pretty sure this would break every current resource that hasn’t had this implemented if this was changed. :thinking:

again, looks like that’d break the current setup and cross-language compatibility (using -1 with lua, and null with c#). I’m not sure how this would be implemented exactly, but without some major work i doubt this would be easy to change and keep current resources from breaking and continuing cross-language support the way it currently is.

edit: nvm.

Interesting release. Implementing something like this is indeed the recommended way to use strongly-typed events across environments, except for the caveat that JSON isn’t exactly the most bandwidth-optimized format to encode to for network usage.

I believe the choice for implementing a custom layer on top of msgpack-cli’s serialization/deserialization layer back in 2014 was either motivated by issues with getting msgpack-cli’s runtime type-specific deserializer to work inside of the Mono sandbox, or by performance issues.

I agree with JSON not being the most bandwidth-optimized. However, if you’re firing server<->client events in their current FiveM incarnation, as if they were normal TCP/UDP traffic, you’re definitely doing something wrong anyway.

That said, there aren’t many other alternatives that I can think of that will not only run within the sandbox, but also allow us to use strongly-typed objects across environments.

I don’t think that’s the issue, as the objects are passed into the EventHandlerDictionary as "ExpandoObject"s before there’s any chance to even attempt to resolve a runtime type.

More specifically, any properties that don’t have special handling (list, delegate, etc) just get thrown into a dictionary, instead of attempting to pack the object directly.

This code release isn’t meant to be a proper wrapper for Lua-based events. (You certainly can use it that way, but you’ll have to decode the JSON in Lua, and/or use simple types) And yes, this is just C#. I don’t see any Lua code :slight_smile:

As for “forgetting to add arguments”, it’s not so much the argument, as it is the attribute on the first argument. Argument attributes are a fairly rare thing outside of TDD and Marshaling/COM interop.

It seems like you don’t understand C# very well, so I won’t continue to your last part (… seriously?), but that said, this isn’t changing anything. It’s simply an abstraction over the annoying-to-use client/server events from C#.

It’s rather I completely misinterpreted the goal of this topic. That’s all. So don’t mind my comment above.

Fair enough, no problem. :slight_smile:

Can we utilize Callbacks from Client>Server or vise-versa using this code? I’ve tried desperately to figure it out using the standard method but fail on each attempt. And if you have any examples - greatly appreciated!

This event handling system looks promising !

But I get the following error when triggering event from client :

[   5854594] Exception during executing Post callback: System.Security.VerificationException: Error verifying Felwyrm.Client.NetworkMethod`1:Invoke (T1): Could not merge stack at depth 4, types not compatible: string (Complex) X T1 ([boxed] Complex) at 0x0023
[   5854594]   at Felwyrm.Client.SpawnManager+<SpawnPlayer>d__5.MoveNext () [0x00150] in <ded8822dc6084dac814fb1c1824c8d90>:0 
[   5854594] --- End of stack trace from previous location where exception was thrown ---
[   5854609]   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in <f0e9de1592254c6a8c7c298c474f20de>:0 
[   5854609]   at System.Runtime.CompilerServices.AsyncMethodBuilderCore+<>c.<ThrowAsync>b__6_0 (System.Object state) [0x00000] in <f0e9de1592254c6a8c7c298c474f20de>:0 
[   5854609]   at CitizenFX.Core.CitizenSynchronizationContext+<>c__DisplayClass1_0.<Post>b__0 () [0x00000] in C:\gl\builds\edf06b9b\0\cfx\fivem\code\client\clrcore\CitizenTaskScheduler.cs:20 
[   5854609]   at CitizenFX.Core.CitizenSynchronizationContext.Tick () [0x0003e] in C:\gl\builds\edf06b9b\0\cfx\fivem\code\client\clrcore\CitizenTaskScheduler.cs:38 

I trigger the event like this client-side :

    public NetworkMethod<object> E_PlayerSpawn { get; set; } // In class def
...
    E_PlayerSpawn = new NetworkMethod<object>("PlayerSpawn", OnPlayerSpawn); // In constructor
...
    Client.GetInstance().E_PlayerSpawn.Invoke( DateTime.Now ); // From another client script (spawnmanager.cs) 

Somehow, it seems that handled type is always string in the error ( string (Complex) X T1 ([boxed] Complex) )
Maybe it’s the JSON string though.

edit: i misread sorry

Hey, this is something I really needed.
I personally hate the current way of server<->client communication in C# too, and this is much simpler.
As an improvement, if you’re still updating this, I suggest you to replace Json with Bson, basically a binary version of Json, and it can be used to reduce the bytes sent. It is included in the Newtonsoft.Json.Bson.

1 Like

I can’t undersand the part “ServerResource.Instance”, what is ServerResource and why is it a singleton? Is it part of CitizenFX.Core? Because I can’t get it to work