Creating your first script in C#

Getting started with scripting for FiveM might be a tad overwhelming, given the wide range of possibilities and the sparsely spread documentation. In this quick and simple guide, we’ll try to show you how to get started with a quick resource in C#. We will be implementing a car spawner through a command.

Prerequisites

Before creating your first script with C#, there are a couple of things you will need to set up and understand.

Writing code

Now that you have set up your C# project and environment, you will have two projects; MyResourceNameClient and MyResourceNameServer.

Any C# class that handles FiveM scripting-related events must inherit from the BaseScript class. Lets do this by going to Class1.cs in your client project. At the same time, we will also define a constructor, which we will use further on. Make sure you have a using directive to CitizenFX.Core.

using CitizenFX.Core;

namespace MyResourceNameClient
{
    public class Class1 : BaseScript
    {
        public Class1()
        {

        }
    }
}

Easy right? But what about adding functionality? We will start by adding a command using various FiveM scripting concepts.

using System;
using System.Collections.Generic;
using CitizenFX.Core;
using static CitizenFX.Core.Native.API;

namespace MyResourceNameClient
{
    public class Class1 : BaseScript
    {
        public Class1()
        {
            EventHandlers["onClientResourceStart"] += new Action<string>(OnClientResourceStart);
        }

        private void OnClientResourceStart(string resourceName)
        {
            if (GetCurrentResourceName() != resourceName) return;

            RegisterCommand("car", new Action<int, List<object>, string>((source, args, raw) =>
            {
                // TODO: make a vehicle! fun!
                TriggerEvent("chat:addMessage", new
                {
                    color = new[] {255, 0, 0},
                    args = new[] {"[CarSpawner]", $"I wish I could spawn this {(args.Count > 0 ? $"{args[0]} or" : "")} adder but my owner was too lazy. :("}
                });
            }), false);
        }
    }
}

You might be overwhelmed at this point, but don’t worry. We will go through everything bit by bit.

EventHandlers["onClientResourceStart"] += new Action<string>(OnClientResourceStart);

In the constructor we’ve added an event handler for the onClientResourceStart event. It takes one argument; a string with the name of the resource that was started. It also has a delegate method OnClientResourceStart, which we defined beneath the constructor. Once the resource has started, FiveM will trigger this event and invoke the method.

if (GetCurrentResourceName() != resourceName) return;

This if statement makes use of the native GetCurrentResourceName(). In short, natives, which has nothing to do with indigenous people, is actually a R* label for ‘game-defined script functions’. We can access these natives through the CitizenFX.Core.Native.API class. You will be using other natives later when spawning a vehicle. In this snippet, GetCurrentResourceName() returns the name of the resource that our script is running. We compare this to the resourceName argument to make sure that we only call the rest of the method once. If we don’t do this check, the rest of the method will run every time any resource has started.

RegisterCommand("car", new Action<int, List<object>, string>((source, args, raw) =>
{
    // TODO: make a vehicle! fun!
    TriggerEvent("chat:addMessage", new
    {
        color = new[] {255, 0, 0},
        args = new[] {"[CarSpawner]", $"I wish I could spawn this {(args.Count > 0 ? $"{args[0]} or" : "")} adder but my owner was too lazy. :("}
    });
}), false);

To start, we see a call to a function. We did not define that function. Well, we (as in, the FiveM team) did, but not when guiding you, the reader, through this wondrously written marvel of a guide. That means it must come from somewhere else!

And, guess what, it’s actually REGISTER_COMMAND! Click that link, and you’ll be led to the documentation for this native. It looks a bit like this:

// 0x5fa79b0f
// RegisterCommand
void REGISTER_COMMAND(char* commandName, func handler, BOOL restricted);

We’ll mainly care about the name on the second line (RegisterCommand, as used in the C# code above), and the arguments.

As you can see, the first argument is the command name. The second argument is a function (represented by the Action delegate in our example) that is the command handler, and the third argument is a boolean that specifies whether or not it should be a restricted command.

The function itself gets an argument that is the source, which only really matters if you’re running on the server (it’ll be the client ID of the player that entered the command, a really useful thing to have), and a List of args which are basically what you enter after the command like /car zentorno making args end up being new List<object>{ "zentorno" } or /car zentorno unused being new List<object>{ "zentorno", "unused" }.

But what about TriggerEvent()? That’s also defined by us. It’s used to call the event chat:addMessage, which is part of the chat resource. In our written example, we send the author name [CarSpawner] in red and a message as arguments.

At this point, you can build your client project, add/move it to your resource and run it. When typing /car in the chat box, you will see our command returning the chat message we defined. screenshot-1

Hey! It’s complaining in the chat box that you were too lazy to implement this. We’ll show them that you’re absolutely not lazy, and actually implement this now.

Implementing a car spawner

You may have followed the Lua tutorial on creating your first script and remember that there was a lot of boilerplate code that might looked overwhelming. Fear not, FiveM provides an easy to use C# wrapper that will allow us to reduce the code.

RegisterCommand("car", new Action<int, List<object>, string>(async (source, args, raw) =>
{
    // account for the argument not being passed
    var model = "adder";
    if (args.Count > 0)
    {
        model = args[0].ToString();
    }

    // check if the model actually exists
    // assumes the directive `using static CitizenFX.Core.Native.API;`
    var hash = (uint) GetHashKey(model);
    if (!IsModelInCdimage(hash) || !IsModelAVehicle(hash))
    {
        TriggerEvent("chat:addMessage", new 
        {
            color = new[] { 255, 0, 0 },
            args = new[] { "[CarSpawner]", $"It might have been a good thing that you tried to spawn a {model}. Who even wants their spawning to actually ^*succeed?" }
        });
        return;
    }

    // create the vehicle
    var vehicle = await World.CreateVehicle(model, Game.PlayerPed.Position, Game.PlayerPed.Heading);
    
    // set the player ped into the vehicle and driver seat
    Game.PlayerPed.SetIntoVehicle(vehicle, VehicleSeat.Driver);

    // tell the player
    TriggerEvent("chat:addMessage", new 
    {
        color = new[] {255, 0, 0},
        args = new[] {"[CarSpawner]", $"Woohoo! Enjoy your new ^*{model}!"}
    });
}), false);

This uses some natives and C# wrapper methods. We’ll link a few of them and explain the hard parts.

Step 1: Validation

We started with checking the model. We set it to adder. If there are any arguments, we set the model to the first argument and cast it to a string.

Then, we check if the vehicle is in the CD image using IS_MODEL_IN_CDIMAGE. This basically means ‘is this registered with the game’. We also check if it’s a vehicle using IS_MODEL_A_VEHICLE. If either check fails, we tell the player and return from the command.

There may be C# wrapper here, but it’s important to reify the use of natives as you will use them a lot when scripting. Make sure you have the using static CitizenFX.Core.Native.API; directive in your class.

Step 2: Creating the vehicle

Using the client side C# wrapper class World, we call the CreateVehicle method which takes a model, Vector3 position, and float heading as arguments. This is the great thing about C#. You have access to a method supplied by us such that you don’t have to request and load a model like you would in Lua. This method returns us a Vehicle object. If you have experience with ScriptHookV.NET you may recognize these classes. The C# wrapper of FiveM is very similar.

Step 3: Setting the player into the vehicle

Since we have our ped and a vehicle now, using the C# wrapper with the Game.PlayerPed object, we can set ourselves into the vehicle’s driver seat.

Running this

Build your project and make sure the latest MyResourceNameClient.net.dll is in the folder of your resource. In your server console, type restart mymode (or whatever you named your resource), and try /car voltic2 in the game client (which should by now be really bored of respawning). You’ll now have your very own Rocket Voltic!

Enabling Debug Information

To ensure Mono can load debug symbols and display full debug information (e.g., line numbers and file paths) in stack traces during runtime, configure your Visual Studio project’s debug build to embed debug information. If not set correctly, stack traces may show error lines as 0, like the ones shown below, indicating missing debug information.

MainThrd/ Failed to run a tick for Client: System.NullReferenceException: Object reference not set to an instance of an object.
MainThrd/ at Client.Services.Player.MyService+<RunTasks>d__22.MoveNext () [0x0015d] in <62554d04181b4b90986523fd33deef4c>:0

Why Embedded PDBs work in FiveM

Embedded portable PDBs are the most reliable way to ensure accurate stack traces in FiveM because .NET Framework 4.7.1 and up supports embedding portable PDBs, and Mono (the runtime used by FiveM) can natively extract this data from the DLL’s metadata to provide line numbers and file paths in stack traces — even without separate .pdb files present.

Follow these steps to enable embedded debug information in Visual Studio:

  1. Set Build Configuration to “Debug”

    • In Visual Studio, select Debug from the configuration dropdown in the top toolbar.
  2. Open Project Properties

    • Right-click your project in Solution Explorer and select Properties.
    • Navigate to the Build tab.
  3. Configure Debug Settings

    • In the Build tab, ensure the following:
      • Define DEBUG constant is checked.
      • Define TRACE constant is checked.
      • Optimize code is unchecked.
    • These settings enable full debug information in the build.
  4. Enable Embedded Debug Information

    • In the Build tab, click Advanced….
    • On the pop-up, set Debug Info to Embedded to include portable PDB data in the assembly.
    • Click OK.
  5. Rebuild the Project

    • Press Ctrl + Shift + B to rebuild the project.
    • The resulting DLL will include embedded debug information.

Using Portable PDBs Instead (as separate .pdb files)

Portable PDBs can also be used in FiveM without embedding — just make sure the .pdb files are copied alongside your DLLs.

To generate separate portable PDBs instead of embedding them, follow the same steps above, but in step 4, select Portable instead of Embedded under Debug Info.

The resulting .pdb file will be written next to the DLL on build — just be sure to deploy it with your resource.

You should also add the .pdb files to your files array (or as commonly known in Lua, table) in your resource manifest file — here’s an example (adjust as needed):

files {
    'Client.net.pdb',
    'Server.net.pdb',
}

Server scripts

You’ll probably also want to write scripts that interact with the server. This section is still to be written. :-(