This tutorial is incomplete. I aim to complete it and add more details/updates when I have some more free time. I have been informed that it still works and is a good starting point for development, please refer to the LSPDFR forums and Discord servers for further assistance and resources.

Introduction

I originally wasn't going to make any formal tutorial series on LSPDFR Development. Despite being very old, Albo1125's videos were exceptional tutorials on getting started with the LSPDFR API and creating callouts, and this is how I learned. Unfortunately, the videos appear to be removed from YouTube, and even though his code remains open source and an excellent resource, it's always much easier to learn through a video. I'm going to try and finish at least one basic tutorial on callouts by the end of this summer (2021).

I've also open-sourced a few of my own callouts if you're curious on learning more. I have not open-sourced all of my callouts yet because many are very poorly written, and so would not be suitable for use as a resource. Of course you could always decompile them, but I wouldn't recommend doing that.

Much of this tutorial has been adapted from Albo's Guides on the LSPDFR API. I've tried to make it a bit easier to understand and updated, but it remains an excellent resource. As always, huge thanks to Albo!

Prerequisites 

First, you'll need a working knowledge of general programming concepts. Callouts are written in C#, so as long as you have an understanding of any CLI Language, that should be alright. If you don't have any programming experience or need a refresher, there is more information on the Development Resources page. You'll also want to be familiar with the basics of object-oriented programming - almost everything that we'll be doing will at least somewhat be object-oriented.

Intro + Main Class

Installing Visual Studio 2019

The IDE we'll be using is Visual Studio 2019. Download it here: visualstudio.microsoft.com/downloads/




Then, choose the .NET Desktop Development Environment under "Desktop and Mobile." Then, choose "Install" in the bottom right.

Choose the Theme. I'd strongly recommend Dark Mode. Then, Start Visual Studio.

Under Create a new Project, select Class Library for C#.

Name your plugin. I'll call mine "FirstLSPDFRPlugin."

Under Additional Information, select .NET Core 3.1 (Long-Term Support). Then, create.

Note: .NET Framework >= 5.0 is NOT compatible with RPH and will not work!

Referencing the LSPDFR & RPH API's

Now that we've set up our project, we have to reference both RPH and LSPDFR. To do this, first ensure you have the most recent version of LSPDFR, and have downloaded the RagePluginHook SDK from ragepluginhook.net

Under Project, select Add Project Reference.

First, choose browse. Locate either where you downloaded LSPDFR manual, or where LSPD First Response is located in your Plugins folder. Select LSPD First Response.dll and click Add.

Do the same where you downloaded the RagePluginHook SDK and add RagePluginHookSDK.dll.

Starting off our Main Class

Our Main.Cs class is how we'll register our callout plugin with LSPDFR and tell it when to load/unload our plugin and register the callouts. We let LSPDFR know that we are creating an LSPDFR plugin by using  public class Main:Plugin.

By default, a class called "Class1.cs" will be created for you. You can keep it like this, however as a habit I like to rename it to "Main.cs." To do this, simply right-click on Class1.cs in the Solution Explorer on the right, and select Rename.

After that, you can copy/paste this example code of a Main class into yours. I'd recommend reading through it first so you can understand how it works.


using System;

using LSPD_First_Response.Mod.API;  //Reference LSPDFR

using Rage; //Reference RPH


namespace FirstLSPDFRPlugin  //The name of our Plugin

{

    public class Main : Plugin  //Let LSPDFR know this is an LSPDFR Plugin (inherited from the Plugin Class of LSPDFR)

    {

        public static String Version = "1.0.0"; //The version of our Plugin (we're not using it for anything right now)


        //Player Goes on Duty, our Plugin is Initailized by LSPDFR.

        public override void Initialize()

        {

            Functions.OnOnDutyStateChanged += OnOnDutyStateChangedHandler;  //if the player goes on or off duty, use the OnOnDutyStateChangedHandler

            Game.LogTrivial("FIRSTLSPDFRPLUGIN: First LSPDFR Plugin Loaded.");  //Log that our plugin has been loaded.

        }

        //LSPDFR clean-up

        public override void Finally()  

        {

            Game.LogTrivial("First LSPDFR Plugin Finished Cleaning Up.");   //Just log it

        }

        //Check to see if the player is on duty (subscribed from Functions.OnOnDutyStateChanged in the Initailize() Method)

        private static void OnOnDutyStateChangedHandler(bool OnDuty)

        {

            if (OnDuty) //if the player is on duty

            {

                Game.DisplayNotification("~b~First LSPDFR Plugin~w~ Version ~g~"+Version+"~w~ by ~y~YobB1n~w~ Loaded ~b~Successfully!~g~ Enjoy!");  //message at the start, note the use of colors.

                Game.LogTrivial("Player Went on Duty. Registering Callouts...");

                RegisterCallouts(); //call Register Callouts method if we're on duty

            }

        }

        //Method called if the player is on duty to register all our callouts

        private static void RegisterCallouts()

        {

            Functions.RegisterCallout(typeof(Callouts.FirstCallout));   //register our first callout

            //we can register an infinite amount of callouts

        }

    }

}

Methods

This section was adapted from Albo's Old Tutorial Series.

Initialize() Method

The Initialize() Method is the first of LSPDFR's methods, which is called when our LSPDFR plugin is created. LSPDFR Plugins are Initialized when the player goes on duty. The Functions.OnOnDutyStateChanged Event will notify our plugin of when a change in the player's on duty state occurs. That is, whenever the player either goes on or off duty. Then, OnOnDutyStateChangedHandler will be an Event Handler that we will subscribed to this Function.

You'll also notice that we use Game.LogTrivial to log this to the RagePluginHook.Log File. You'll want to make sure you log all relevant information to this file for testing and bug reporting purposes!

Finally() Method

This Method is called when our plugin is cleaned up, so when the player goes off duty. For now, we'll just log this.

OnOnDutyStateChangedHandler() Event Handler

We subscribed this Event Handler to the Functions.OnOnDutyStateChanged Event in the Initialize() Method. As mentioned, this will notify if the player has changed their OnDuty state, but not if they are actually on duty or not. To determine this, we check to see if the player is on duty or not, and if so, will display a notification message (like how you're probably spammed with notifications when you go on duty), log it to the RPH log, and register our callouts.

RegisterCallouts() Method

This void method simply registers the callouts in our pack if it is called from the previous event (so they player goes on duty). You can have an infinite amount of callouts you can register here. For now, Functions.RegisterCallout(typeof(Callouts.FirstCallout)); will throw an error because we have not made the callout itself yet, however we'll get there soon. Also note that the RegisterCallout function.

Making a Callout

More to come later.


using System;

using Rage; //reference RPH

using LSPD_First_Response.Mod.API;  //reference LSPDFR

using LSPD_First_Response.Mod.Callouts;

using System.Drawing;   //for Colors

using System.Windows.Forms; //for Keys


namespace FirstLSPDFRPlugin.Callouts    //namespace depending on the plugin name

{

    [CalloutInfo("FirstCallout", CalloutProbability.High)]  //name of the Callout (as it appears in the RPH console) and probability of it being chosen

    class FirstCallout : Callout    //inherited from LSPDFR Callout Class

    {

        //first thing - location

        private Vector3 MainSpawnPoint; //declare the main spawn point for this call


        private Ped player = Game.LocalPlayer.Character;    //declare the player's character ped "player" (makes it easier to type)

        private Ped Suspect;    //declare the suspect ped


        private Blip SuspectBlip;   //declare the suspect blip


        private Vehicle SuspectVehicle; //declare the suspect vehicle


        private LHandle MainPursuit;    //declare the pursuit for the end


        //before callout displayed to user

        public override bool OnBeforeCalloutDisplayed()

        {

            Game.LogTrivial("=====FirstCallout Start=====");    //log the start

            MainSpawnPoint = World.GetNextPositionOnStreet(player.Position.Around(500));    //get the closest street position that is exactly 500 metres from the player's current position

            ShowCalloutAreaBlipBeforeAccepting(MainSpawnPoint, 25f);    //flashing blip on minimap 25m in radius from this position, before the callout is created

            AddMinimumDistanceCheck(60, MainSpawnPoint);    //if the player is <= 60 metres from the spawnpoint, abort the callout

            Functions.PlayScannerAudio("ATTENTION_ALL_UNITS_01 CRIME_GRAND_THEFT_AUTO_01"); //play these two audio files in succession

            CalloutMessage = "First Callout";   //name of the callout as it appears in the advisory notification

            CalloutPosition = MainSpawnPoint;   //position of the callout so LSPDFR knows which world zone

            CalloutAdvisory = "This is ~b~My First Callout~w~.";    //additional advisory for more info


            return base.OnBeforeCalloutDisplayed(); //return

        }


        //callout accepted by user

        public override bool OnCalloutAccepted()

        {

            Game.LogTrivial("First Callout Accepted by user."); //log to file that the callout has been accepted

            Game.DisplayNotification("Respond ~r~Code 3."); //respond code 3 notification upon callout accepted


            SuspectVehicle = new Vehicle("INFERNUS", MainSpawnPoint);   //spawn a new infernus at this spawnpoint

            SuspectVehicle.IsPersistent = true; //infernus is persistent (won't automatically be cleaned up (deleted) by gta


            Suspect = SuspectVehicle.CreateRandomDriver();  //the Suspect will be a random driver in the suspectvehicle

            Suspect.IsPersistent = true;    //again the suspect is persistent

            Suspect.BlockPermanentEvents = true;    //block events to this suspect i.e. getting spooked (don't want them running away before we get there)


            String[] Weapons = new string[3] { "weapon_heavypistol", "weapon_specialcarbine", "weapon_combatmg" };  //new array of 3 weapons

            System.Random rng = new System.Random();    //assign rng as a new rng

            int WeaponModel = rng.Next(0, Weapons.Length);  //WeaponModel will be a random number between 0, and LESS than the length of the Weapons array (3)


            Suspect.Inventory.GiveNewWeapon(Weapons[WeaponModel], -1, true);    //give the suspect a new weapon from the array, using the index we got with rng, infinite ammo (-1), equipnow = true

            SuspectBlip = Suspect.AttachBlip(); //SuspectBlip will be attached to the suspect

            SuspectBlip.IsRouteEnabled = true;  //GPS route enabled to this blip

            SuspectBlip.Color = Color.Red;  //blip is red


            Callout();  //call callout method

            return base.OnCalloutAccepted();    //return

        }

        

        //callout not accepted

        public override void OnCalloutNotAccepted()

        {

            Game.LogTrivial("First Callout Not Accepted by User."); //iff the callout is not accepted, log then end it

            base.OnCalloutNotAccepted();

        }

        

        //callout itself

        private void Callout()

        {

            GameFiber.StartNew(delegate //start a new gamefiber for our callout

            {

                while (player.DistanceTo(Suspect) > 20) GameFiber.Wait(0); //while the player is > 20 metres away from the suspect, wait the game fiber indefinitely


                Game.DisplaySubtitle("Dispatch, we are ~b~on Scene!", 2500);    //a subtitle, 2.5 seconds long

                Suspect.Tasks.LeaveVehicle(Suspect.CurrentVehicle, LeaveVehicleFlags.LeaveDoorOpen).WaitForCompletion();    //give the suspect the task of leaving their current vehicle, sleep the gamefiber until they're done

                Suspect.Tasks.FightAgainst(player, 5000).WaitForCompletion();   //give the suspect the task of fighting against the player for 5 seconds, sleep the fiber until they're done

                Suspect.Tasks.EnterVehicle(SuspectVehicle, -1).WaitForCompletion(); //give the suspect the task of re-entering their vehicle, the drivers seat, sleep the fiber until they're done

                Suspect.Tasks.CruiseWithVehicle(15, VehicleDrivingFlags.Emergency); //give the suspect the task of driving with their vehicle, speed 15 m/s, emergency driving flag


                GameFiber.Wait(2000);   //wait 2 seconds

                Game.LogTrivial("Suspect Pursuit Event Started.");  //log it

                if (SuspectBlip.Exists()) SuspectBlip.IsRouteEnabled = false;   //if the suspect blip exists, disable the route in the GPS


                MainPursuit = Functions.CreatePursuit();    //instantiate a new pursuit

                Functions.PlayScannerAudio("CRIME_SUSPECT_ON_THE_RUN_01");  //play this audio

                Game.DisplayNotification("Suspect is ~r~Evading!"); //notifcation

                try   //it's important to try/catch this in case the user made a mistake in an LSPDFR config file and doesn't have a valid backup unit for this

                {

                    Functions.RequestBackup(player.Position, LSPD_First_Response.EBackupResponseType.Pursuit, LSPD_First_Response.EBackupUnitType.LocalUnit);   //try to request backup (player pos, pursuit backup, local backup unit)

                }

                catch

                {

                    Game.LogTrivial("First Callout Crash prevented after attempting to spawn backup."); //log that a crash was prevented

                    Game.DisplayNotification("First Callout ~r~Crash~w~ prevented after attempting to ~o~spawn backup.");   //notify the user

                }

                Functions.SetPursuitIsActiveForPlayer(MainPursuit, true);   //set MainPursuit as active for the player

                Functions.AddPedToPursuit(MainPursuit, Suspect);    //add the suspect to MainPursuit


                while (Functions.IsPursuitStillRunning(MainPursuit)) GameFiber.Wait(0); //while MainPursuit is still running, wait.


                //there are only two options once the pursuit is over - the suspect is arrested or killed.

                if (Suspect.Exists()) { //always important to make sure the Ped Exists.

                    if (Suspect.IsDead) Game.DisplayNotification("Dispatch, Suspect is ~r~Dead.");  //if dead

                    else Game.DisplayNotification("Dispatch, Suspect is Under ~g~Arrest."); //if they're not dead, they'd be arrested

                }   //NOTE: StopThePed likes to mess this logic up!

                GameFiber.Wait(2000);   //wait 2 s

                Functions.PlayScannerAudio("REPORT_RESPONSE_COPY_02");  //play the copy audio

                GameFiber.Wait(2000);   //wait 2 more s


                End();  //end the call

            }

            );

        }


        //cleanup the callout at the end

        public override void End()

        {

            Game.LogTrivial("First Callout Finished, Cleaning Up Now.");    //log it


            Functions.PlayScannerAudio("WE_ARE_CODE_4");  //some sort of audio to specify that the callout has finished

            if (SuspectBlip.Exists()) SuspectBlip.Delete(); //check to see if the suspect blip exists, if it does, delete it

            if (Suspect.Exists()) Suspect.Dismiss();    //same as Suspect.ispersistent = false;


            Game.LogTrivial("First Callout Finished Cleaning Up."); //log it

            base.End();

        }

    }

}