[Request] Shared .NET/C# scripts (as in shared_script directive)

Currently the client and server side reference assemblies on NuGet have only minor differences in the API. It’d be nice to have a reference that is a superset of both APIs. That would allow using .NET scripts with shared_script directive in the manifest, with side checking done at runtime using IsDuplicityVersion function.

Note that while it’s possible to reference both assemblies, in some cases (usually when only shared functions are used, e.g. an empty BaseScript) the compiler decides to link only one assembly (the last -r argument).

Search for #if’s that depend on either IS_FXSERVER or GTA_FIVE shows that in

  • client/clrcore/BaseScript.cs: protected property LocalPlayer must be stubbed on the server, lines 86–103
  • client/clrcore/BaseScript.cs: TriggerServerEvent/TriggerClientEvent and TriggerLatentServerEvent/TriggerLatentClientEvent must be stubbed on server/client respectively, lines 217–L279

So that part is straightforward, e.g. something like the following patch:

diff.patch
diff --git a/code/client/clrcore/BaseScript.cs b/code/client/clrcore/BaseScript.cs
index 887392d27..e700cba27 100644
--- a/code/client/clrcore/BaseScript.cs
+++ b/code/client/clrcore/BaseScript.cs
@@ -100,6 +100,11 @@ namespace CitizenFX.Core
 				return m_player;
 			}
 		}
+#elif IS_FXSERVER && !IS_RDR3
+		protected Player LocalPlayer
+		{
+			get => null;
+		}
 #endif
 
 #if !IS_RDR3
@@ -214,26 +219,37 @@ namespace CitizenFX.Core
 			TriggerEventInternal(eventName, argsSerialized, false);
 		}
 
-#if !IS_FXSERVER
 		[SecuritySafeCritical]
 		public static void TriggerServerEvent(string eventName, params object[] args)
 		{
+#if !IS_FXSERVER
 			var argsSerialized = MsgPackSerializer.Serialize(args);
 
 			TriggerEventInternal(eventName, argsSerialized, true);
+#else
+			throw new NotSupportedException();
+#endif
 		}
 
 		[SecuritySafeCritical]
 		public static void TriggerLatentServerEvent(string eventName, int bytesPerSecond, params object[] args)
 		{
+#if !IS_FXSERVER
 			var argsSerialized = MsgPackSerializer.Serialize(args);
 
 			TriggerLatentServerEventInternal(eventName, argsSerialized, bytesPerSecond);
-		}
 #else
+			throw new NotSupportedException();
+#endif
+		}
+
 		public static void TriggerClientEvent(Player player, string eventName, params object[] args)
 		{
+#if IS_FXSERVER
 			player.TriggerEvent(eventName, args);
+#else
+			throw new NotSupportedException();
+#endif
 		}
 
 		/// <summary>
@@ -243,6 +259,7 @@ namespace CitizenFX.Core
 		/// <param name="args">Arguments to pass to the event.</param>
 		public static void TriggerClientEvent(string eventName, params object[] args)
 		{
+#if IS_FXSERVER
 			var argsSerialized = MsgPackSerializer.Serialize(args);
 
 			unsafe
@@ -252,11 +269,18 @@ namespace CitizenFX.Core
 					Function.Call(Hash.TRIGGER_CLIENT_EVENT_INTERNAL, eventName, "-1", serialized, argsSerialized.Length);
 				}
 			}
+#else
+			throw new NotSupportedException();
+#endif
 		}
 
 		public static void TriggerLatentClientEvent(Player player, string eventName, int bytesPerSecond, params object[] args)
 		{
+#if IS_FXSERVER
 			player.TriggerLatentEvent(eventName, bytesPerSecond, args);
+#else
+			throw new NotSupportedException();
+#endif
 		}
 
 		/// <summary>
@@ -266,6 +290,7 @@ namespace CitizenFX.Core
 		/// <param name="args">Arguments to pass to the event.</param>
 		public static void TriggerLatentClientEvent(string eventName, int bytesPerSecond, params object[] args)
 		{
+#if IS_FXSERVER
 			var argsSerialized = MsgPackSerializer.Serialize(args);
 
 			unsafe
@@ -275,8 +300,10 @@ namespace CitizenFX.Core
 					Function.Call(Hash.TRIGGER_LATENT_CLIENT_EVENT_INTERNAL, eventName, "-1", serialized, argsSerialized.Length, bytesPerSecond);
 				}
 			}
-		}
+#else
+			throw new NotSupportedException();
 #endif
+		}
 
 #if !IS_FXSERVER
 		[SecurityCritical]

I don’t know what changes are needed in natives codegen though. In addition to that, we’d need to set up CI scripts for these reference assemblies.

I’m not sure that’d work well enough regarding method compilation granularity, lots of potential gotchas in accidentally hitting a missing method. :confused:

Now that I think about it, you are right. I was hopping to build a single DLL with both client and server side code, but with the current API adding all these duplicity version checks and ensuring that methods won’t be compiled on the wrong version is too much hassle.

What do you think about adding ServerScript and ClientScript classes that inherit BaseScript and only get instantiated in the correct environment? As I’ve mentioned in the last post edit, turns out the compiler is smart enough to reference both assemblies though sometimes it outsmarts itself. Adding ServerScript in Server.dll and ClientScript in Client.dll should make it happy.

Regarding the missing methods, since now I’m not proposing adding new reference assembly, that shouldn’t be an issue. That is unless deliberately referencing both client and server assemblies, in which case I think it’s reasonable to expect gotchas.

In this scenario it feels like API itself might be an inherently obvious problem already.

This’d probably need further restructuring of reference assemblies and the API.* implementation to actually make full sense. There were some plans for this at some point, but there’s infinite unexplored complexity in all this, and it’s not made much easier by the fact that the current Mono runtime is barely reproducible at all (it apparently took a lot of hell to get pre-CoreFX-merge code to have the SL5 security attributes applied already).

I’ve seen your(?) PR, but will have to defer checking it to a later moment, probably during the weekly PR batch.

I’ve seen your(?) PR, but will have to defer checking it to a later moment, probably during the weekly PR batch.

Yes, that’s me! Thanks, looking forward to that.

I’m new to the project and community, and you’ve mentioned “weekly PR batch”. Do we have any release or code review (not necessarily strict) schedule?

Not directly, but at the start of every week we try to handle as many PRs as possible that are not trivial enough for immediate review.