LSPDFR Callout Tutorials
Please credit me if you found these resources useful for your own development. Thanks!
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();
}
}
}