RAGE Plugin Hook for Red Dead Redemption 2

Preliminary documentation

Introduction

We’re excited to announce that RAGE Plugin Hook is coming to Red Dead Redemption 2, finally living up to its name RAGE Plugin Hook, not GTA V Plugin Hook.

What is RAGE Plugin Hook

RAGE Plugin Hook, as the name suggests, hooks into RAGE engine (Rockstar Advanced Game Engine) based games and allows developers to write plugins for the games in C#, VB.NET or any other .NET language.

That was the original idea behind the name. We started working on RAGE Plugin Hook before GTA V was released, and the original internal hook was made for Max Payne 3 as it was the closest thing we had to GTA V to prepare.

But when GTA V came out, the engine had changed quite a lot, and more importantly, GTA V was 64-bit, while Max Payne 3 was 32-bit. And at the time, we decided to scrap the Max Payne 3 part, and focus on just GTA V, with the hopes that in the future, we’d be able to support multiple games as originally intended.

Well, that time has now come, and a preliminary version of RAGE Plugin Hook is now available for Red Dead Redemption 2.

We also have some stuff to announce about Max Payne 3 at some point in the future ;)

Using RAGE Plugin Hook for non-developers

To use RAGE Plugin Hook. Extract the contents of the downloaded .zip directly into your game folder.

Plugins are installed into the “C#Plugins” folder.

Then launch RAGEPluginHook.exe. On the first run, you can choose whether to load all plugins, specific plugins, or none.

Once the game has fully loaded with RAGE Plugin Hook, you can press F5 (by default) to open the console. Here you can type various commands, like SetTimeOfDay, SetWeather, TeleportToPosition, TeleportToWaypoint, etc. Use the LoadPlugin command to load plugins.

NOTE: If you plan on using the console or developing, we recommend running the game in borderless mode!

NOTE: There’s currently an issue where the game will crash if you attempt to load a save.

What to expect

If you’re not a developer, you can stop reading now, unless you’re interested in technical and development stuff.

It’s been a lot of work making support for multiple games for RAGE Plugin Hook, and we actually started working on multi game support before Red Dead Redemption 2 was even announced for the PC.

It’s not been a “from scratch” kind of thing. We’ve had a massive API and backend that’s been so far tailored to GTA V, and it’s taken time to make this release possible, as unlike when making a piece of software like RPH from scratch for one new game, we’ve had to comment out and modify a lot of the backend code just to get it running for a game it wasn’t originally intended for.

As such, the initial release is a simple native invocation hook (script hook, if you will), but with a few extras, like the vector classes, MathHelper class, etc. and a temporary API class called TempApi which contains a bunch of properties and methods to make things a little easier, until we can get the proper API up and running. There may also be certain console commands that don’t work at all, or error out for now.

The following types are available in the preliminary API:

Rage.TempApi

Rage.Attributes.PluginAttribute

Rage.GameFiber

Rage.Debug

Rage.Model

Rage.MathHelper

Rage.WeatherTypeIndex

Rage.WeaponHash

Rage.Vector2

Rage.Vector3

Rage.Quaternion

Rage.Rotator

If you haven’t used RAGE Plugin Hook for GTA V, take a look at http://docs.ragepluginhook.net/?topic=html/T_Rage_Ped.htm for a glimpse of what the actual API will look like.

And of course, you already get the ability to reload your code without having to restart the game!

RAGE Plugin Hook 2

RAGE Plugin Hook was originally coded in C++/CLI, and as such, it was a nightmare to maintain and extend, as C++ is a horrible language; and unlike plugins, using C++/CLI didn’t allow us to reload RAGE Plugin Hook during development, making development iterations very slow.

For this reason, we decided quite a while ago now to rewrite RAGE Plugin Hook in C#. Not only does this allow us to write faster and more comfortably, but it also allows us to reload RAGE Plugin Hook during development, without having to restart the entire game, allowing us to test new code and additions, thus speeding up development iterations.

C# and the ability to reload RPH also meant that we could completely revamp the console to be a lot nicer.

Unfortunately, RDR2 uses DirectX 12 which is incompatible with all our drawing code, meaning for RDR2, the new console won’t be available. We’re not sure when we can implement the new console into RDR2, if ever.

For RDR2, we’ve included a simple Windows Forms console, so you’re still able to execute and/or add new commands, albeit in a console that isn’t remotely as nice as the one you’ve come to know.

Now, as we started working on RAGE Plugin Hook 2, it wasn’t with RDR2 in mind, but a replacement for RAGE Plugin Hook 1 for GTA V. Despite that, RDR2 will be the first game for which RAGE Plugin Hook 2 will be released.

Rewriting RAGE Plugin Hook was also a way for us to change some things we didn’t like about RAGE Plugin Hook 1; so you’ll see some slight changes in the API with RDR2 once it’s released (and also GTA V once we release for that too).

For example, seat indices are now zero based. Script functions that take a seat index, take the passenger seat index; that is, 0 is the first passenger seat, and -1 would then be the driver’s seat. This however, is only a thing with script functions. Internally in the RAGE engine, seat indices are 0 based, where 0 is the driver’s seat. This makes logically a lot more sense, and we wanted to reflect this in the API.

The API can and WILL change without notice. Be sure to update your plugins.

Creating a plugin

To write plugins for RAGE Plugin Hook 2, you must have .NET Framework 4.7.2 installed, and preferably an IDE. We recommend Visual Studio 2019.

Create an empty Class Library and add a reference to SDK\RagePluginHook2.dll.

Before your plugin can be loaded, we need to set up a few basic things.

First of all, you must have an entry point. A static method that will be executed once RAGE Plugin Hook has loaded your plugin.

namespace MyFirstPlugin
{
        internal
static class EntryPoint
        {
                
/// <summary>
                
/// Called when the plugin has been loaded.
                
/// </summary>
                
private static unsafe void OnEntry()
                
{
                }
        }
}

The entry point method can be called anything, but RAGE Plugin Hook will look for a method named Main with no parameters. If you, like we have above, have named it something other than Main you must tell RAGE Plugin Hook where to find the entry point.

This is done in the assembly attribute Rage.Attributes.PluginAttribute.

Whether you have a custom named entry point or not, this attribute must be present, or RAGE Plugin Hook will refuse to load it. You can place the assembly attribute in any .cs file outside all classes. (Eg. top of the file).

[assembly: Rage.Attributes.Plugin
(
   name:
"My First Plugin",
   Author =
"You!",
   EntryPoint =
"MyFirstPlugin.EntryPoint.OnEntry",
   ExitPoint =
"MyFirstPlugin.EntryPoint.OnUnloading",
   PrefersSingleInstance =
true
)]

The name parameter is required. The remaining are optional named arguments.

But since we have a custom entry point, we must let RAGE Plugin Hook know where to find it in the PluginAttribute.

Here we’ve also specified the exit point which RAGE Plugin Hook will execute when your plugin gets unloaded, allowing you to clean up if need be, eg. delete entities you’ve spawned.

PrefersSingleInstance indicates to RAGE Plugin Hook that, by default, it shouldn’t attempt to load multiple instances of your plugin. More on that below.

Execution Model

Plugins in RAGE Plugin Hook work much like regular programs do. They’re isolated and they have an entry point, which they execute to end, and then they get unloaded.

private static unsafe void OnEntry()
{
        TempApi.KillEntity(TempApi.GetPlayerPed());
}

The plugin above, once loaded, simply kills the player, then gets unloaded.

But just like regular programs, simply doing one thing, then ending is not always desirable.

Fibers

Plugins in RAGE Plugin Hook run on fibers. Like threads, they’re used for multi-tasking. However, unlike threads, that are scheduled to run for a certain amount of time by the operating system before being preemptively interrupted to allow another thread to run, fibers run until they interrupt themselves. This ensures synchronization between fibers and the game.

The entry point of your plugin is also executed in a fiber. It’ll continue to run until it returns, or until it yields itself by calling GameFiber.Yield().

Once it does, all other fibers will then run, one by one, until each one has yielded itself. Finally the game will tick, and the process restarts.

Here’s a visualization of this concept:

You can also use GameFiber.Sleep() to yield for more than 1 tick.

The following example plugin runs forever, and kills the player every time they hit K on their keyboard.

private static unsafe void OnEntry()
{
   
// Loop forever.
   
while (true)
   {
       
// Did the player start pressing K this tick?
       
if (TempApi.WasKeyJustPressed(Keys.K))
       {
           
// If so, kill them.
           TempApi.KillEntity(TempApi.GetPlayerPed());
       }

       
// Yield execution, letting other fibers (and the game) execute.
       
// THIS IS IMPORTANT! If you don't yield in an infinite loop, you will freeze the game forever!
       GameFiber.Yield();
   }
}

You can start as many fibers as you desire to perform things concurrently. The following example spawns 5 characters at 2 second intervals, and for each character, spawns a fiber that will kill them 5 seconds after they’ve spawned:

private static unsafe void OnEntry()
{
        
for (int i = 0; i < 5; i++)
        {
                
// Spawn a ped(estrian) 5 meters in front of the player.
                Vector3 position = TempApi.GetEntityOffsetPositionFront(TempApi.GetPlayerPed(),
5f);
                
uint ped = TempApi.SpawnPed("A_M_M_BiVRoughTravellers_01", position, 0f, true, true);

                
// Verify it was created.
                
if (TempApi.Exists(ped))
                {
                        
// Start a new fiber that will run concurrently to the one spawning the peds.
                        
uint pedToKill = ped;
                        GameFiber.StartNew(
delegate
                        {
                                
// Yield this fiber for 5 seconds.
                                GameFiber.Sleep(
5000);

                                
// Kill the ped if it's still alive.
                                
if (TempApi.Exists(pedToKill) && !TempApi.IsEntityDead(pedToKill))
                                {
                                        TempApi.KillEntity(pedToKill);
                                }
                        });

                        
// Wait 2 seconds before spawning the next ped.
                        GameFiber.Sleep(
2000);
                }
        }
}

Your plugin remains loaded until its last fiber ends.

Make sure to check out the GameFiber class for other useful methods to yield or execute code concurrently or for a certain amount of time, etc.

Also make sure to look through TempApi and MathHelper as well.

Executing Plugins

Once you’ve compiled your plugin as a .dll, copy it to the “C#Plugins” folder in the game folder. Note, this is a working name for now, and whether you’ve coded your plugin in C#, VB.NET or another .NET language, it should go in the “C#Plugins” folder.

Once you’ve done that, switch to the game (Make sure you’ve loaded RAGE Plugin Hook), then hit the console key (by default F5), and type LoadPlugin “MyFirstPlugin.dll” replacing MyFirstPlugin.dll with whatever you’ve called your file.

RAGE Plugin Hook will then load and execute your plugin.

If you make changes to your code, you can test your changes by recompiling, copying the .dll into “C#Plugins” again, and then in the console type: ReloadPlugins

Instead of that, you can also type UnloadPlugin “MyFirstPlugin.dll”; LoadPlugin “MyFirstPlugin.dll” to reload just your plugin.

You can also type ReloadAllPlugins to reload all loaded plugins.

If your plugin isn’t running again, it could be that it had unloaded at the time you typed ReloadPlugins in which case you’ll need to use LoadPlugin.

It could also be that you have a runtime exception. Check the console for errors.

You can also use LoadPlugin “MyFirstPlugin.dll” true”; to load the plugin and mark it for automatic reload when modified. If you then recompile and copy the .dll into “C#Plugins”, RAGE Plugin Hook will detect this, and reload the plugin automatically.

We suggest adding Build Events to your C# project that copies the compiled .dll automatically. You can use the below code as a template. Make sure to correct the path to your game folder.

copy "$(TargetDir)$(TargetName).dll" "C:\Games\RDR2\C#Plugins\$(TargetName).dll"
copy "$(TargetDir)$(TargetName).pdb" "C:\Games\RDR2\C#Plugins\$(TargetName).pdb"

Invoking Natives

The RAGE engine contains its own custom scripting system containing thousands of functions to allow gameplay developers to script the missions, etc. that you encounter in the game. RAGE Plugin Hook hooks into this system and allows you to invoke these functions from your C# (or other .NET language) code.

Until the proper API is ready, here’s how to invoke natives with the temporary API.

There are 2 ways.

  1. Method TempApi.CallNative()
  2. Property TempApi.Natives

TempApi.CallNative() expects the hash or the name of the native to invoke, as well as the return type, and the arguments to pass to the function.

public static object CallNative(ulong hash, Type returnType, params NativeArgument[] arguments)

The hash, is the original hash of the native, not the current. This is to ensure that calls to natives will still work in future game patches where the hashes may change. CAUTION: For natives that are present in both RDR2 and GTAV (which is a lot), the original hash is the original hash from GTA V, not from RDR2. That is, while GET_ENTITY_HEALTH’s hash in the first version of RDR2 is 0x82368787EA73C0F7, the native already existed in GTA V, and thus you must use the hash 0xEEF059FAD016D209 from GTA V. This simplifies compatibility between the two games.

The returnType must be a struct (or string) and should be one of the following: void, sbyte, byte, bool, short, ushort, int, uint, long, ulong, float, string, Vector2, Vector3, Quaternion, Rotator.

The arguments array, is the arguments to pass to the function. Each element in the array is an instance of the class NativeArgument. However, this class has implicit operators for each supported type, so you don’t have to explicitly create an instance. Thus, the following:

int playerHealth = (int)TempApi.CallNative(0xEEF059FAD016D209, typeof(int), new NativeArgument(TempApi.GetPlayerPed()));

Can be simplified to just:

int playerHealth = (int)TempApi.CallNative(0xEEF059FAD016D209, typeof(int), TempApi.GetPlayerPed());

IntPtr as well as pointers to any of the primitive types are also supported:

TempApi.CallNative("DELETE_ENTITY", typeof(void), &pedHandle);

NOTE: To call a native by name, you must use its proper name (names that don’t start with underscore). If a native is named with a leading underscore, it’s a guessed name, and you must invoke it using its hash instead.

Another way to specify the return type, is by using the generic overload CallNative<T>(). Then you also don’t have to cast the return value.

int playerHealth = TempApi.CallNative<int>(0xEEF059FAD016D209, TempApi.GetPlayerPed());

(For void calls, using the generic overload, you can simply specify int as the return type, and then discard the result.)

Another way to invoke natives, is using the dynamic object returned by TempApi.Natives.

Being dynamic, this is slower, performance wise than using the CallNative methods, but it’s a lot more convenient for you as a developer.

The dynamic object returned has no members, however, you can type any method name, and it will compile, whether that method exists on the object or not.

Instead, the availability of said method is checked at runtime instead. RAGE Plugin Hook uses this to figure out which native to invoke.

You specify the return type by adding 1 generic type. For example, to call GET_ENTITY_HEALTH, you can do:

int health = TempApi.Natives.GET_ENTITY_HEALTH<int>(TempApi.GetPlayedPed());

When the call is executed, RAGE Plugin Hook resolves the native to call from the name you specified (“GET_ENTITY_HEALTH”) and invokes it with the passed arguments.

For prettier code, you can also type the name in PascalCase like so:

int health = TempApi.Natives.GetEntityHealth<int>(TempApi.GetPlayedPed());

RAGE Plugin Hook will parse this as GET_ENTITY_HEALTH and invoke that.

Another benefit of using the TempApi.Natives property, is that it supports C#’s ref and out keywords for passing pointers to natives. For example:

TempApi.Natives.DeleteEntity(ref pedHandle);

You can use both ref and out for all pointers, but if you use out, make sure the native isn’t reading the value. Eg. ref should be used with DELETE_ENTITY, as it both reads and writes the pointer. out can be used with eg. GET_POSIX_TIME:

TempApi.Natives.GetPosixTime(out int year, out int month, out int day, out int hour, out int minute, out int second);

When in doubt, use ref. The only technical difference is that with out you don’t have to assign the variable first.

If for whatever reason, you need to pass an address to a native using the TempApi.Natives property, but without using ref and out, you can still use a regular pointer, but you must cast it to IntPtr as pointers aren’t directly supported by dynamic code:

TempApi.Natives.DeleteEntity((IntPtr)(&pedHandle));

For natives taking a pointer to an array of characters (i.e. char* in C++), it is not necessary to use pointers. Instead, simply pass a regular string, and the API will take care of marshalling it to the native. This applies to both arguments and return types (For both TempApi.Natives and TempApi.CallNative()).

Some natives take a position, that is, x, y and z coordinates, as 3 separate arguments.

When using the TempApi.Natives property to invoke natives, you can pass a Vector3 in the call, and RAGE Plugin Hook will automatically split its x, y and z component as 3 separate arguments. This only works for Vector3 specifically, though, and is done for convenience. The actual native gets 3 separate arguments. That is, the following two statements are equivalent.

TempApi.Natives.SetEntityCoordsNoOffset(entityHandle, position.X, position.Y, position.Z, false, false, false); // Passing 7 arguments.
TempApi.Natives.SetEntityCoordsNoOffset(entityHandle, position,
false, false, false); // Still passing 7 arguments.

You can also call a native by hash using the TempApi.Natives property by using the x prefix followed by the hexadecimal representation of the hash. Eg.

TempApi.Natives.x25ACFC650B65C538(ped, 1.25f); // Set ped scale.

The Temporary API

As mentioned in the introduction, this first release will not have the full API, but require you to invoke script functions (natives).

However, we’ve provided a TempApi class that contains a bunch of properties and methods to help with common stuff, and some stuff that aren’t available through natives, like getting all peds, getting specific key presses, etc.

Below is a quick description of each member of this class:

Gets the build number of the game, eg. 1207.

P:BuildNumber

Gets whether a modifier key is down at the time of the call to the property.

P:IsControlDown

P:IsShiftDown

P:IsAltDown

Gets whether a modifier key started being pressed down on the tick the property was called; false on any subsequent call.

P:WasControlJustPressed

P:WasShiftJustPressed

P:WasAltJustPressed

Gets whether a modifier key started not being pressed down on the tick the property was called; false on any subsequent call.

P:WasControlJustReleased

P:WasShiftJustReleased

P:WasAltJustReleased

Gets whether a key started being pressed down on the tick the property was called; false on any subsequent call.

P:WasKeyJustPressed()

Gets whether a key started not being pressed down on the tick the property was called; false on any subsequent call.

P:WasKeyJustReleased()

Gets the scaled (IE. affected by timescale) game time in milliseconds.

P:GameTime

Gets the time it took to render the most recent frame in seconds.

P:FrameTime

Gets the world simulation time multiplier.

P:TimeScale

Returns a dynamic object that can be used to invoke natives. See below.

P:Natives

Gets the game's implementation of Jenkin's One-At-A-Time hashing algorithm.

M:GetHashKey

Logs text to the log and console.

M:Log

Same as Log() but calls to this method are only compiled when the DEBUG conditional compilation symbol is defined.

M:LogDebug

Gets or sets the amount of money the player has, in dollars.

P:PlayerMoney

Gives the player the specified amount of money, in dollars.

M:GivePlayerMoney

Takes away the specified amount of money from the player, in dollars.

M:TakePlayerMoney

Gets the distance between two entities and/or positions.

M:GetDistanceBetween

M:GetDistanceBetween2D

Calls one of the games' script functions given its hash or name (See below).

M:CallNative

Loads and waits for collision at the given position. Returns the ground position at that position.

M:LoadAndWaitForCollision

Teleports an entity to a position, loading the world and placing the entity on the ground there.

M:TeleportEntity

Gets the position of the currently player placed waypoint, or null if none is active.

M:GetWaypointPosition

Clears help messages.

M:ClearHelp

Displays a message in a box in the top left corner of the screen.

M:DisplayHelp

Displays a message at the bottom center of the screen.

M:DisplaySubtitle

Displays a message on the left side of the screen.

M:DisplayNotification

Returns new position that is the specified entity's position + an offset relative to the entity's orientation.

M:GetEntityOffsetPositionFront

M:GetEntityOffsetPositionRight

M:GetEntityOffsetPositionUp

M:GetEntityOffsetPosition

Sets the current weapon for the specified ped. If the equip parameter is set to true, the ped will equip it. If not, it’ll remain in the inventory, but will be the weapon it pulls out when needed.

M:SetCurrentPedWeapon

If true, prevents the specified Ped from performing actions on its own, eg. fleeing.

M:BlockPermanentEvents

If true, the entity will not be despawned by the game. Setting to false or calling Dismiss() will allow the game to despawn this entity once it leaves the area.

M:SetEntityPersistent

Puts the specified ped instantly into the specified vehicle on the specified seat. The seat index is zero based where zero is the first seat.

M:WarpIntoVehicle

Puts the specified ped instantly onto the specified horse on the specified seat. The seat index is zero based where zero is the first seat.

M:WarpOntoHorse

Gets whether the specified entity handle still represents a valid entity (IE. it hasn't been deleted).

M:Exists

Returns an array of all characters currently in the world.

M:GetAllPeds

The temporary API currently does not have a method to get all vehicles, only all peds.

For now, this method can be used to get all entities that have been exposed to the game's scripting system; either because a script has spawned it, or because a script function has returned it at some point.

M:GetAllScriptAwareEntities

The remaining methods are hopefully self explanatory.

You can find an example project for the current temporary API and native invocation here: http://ragepluginhook.net/RPH2PreDoc/RDR2PreliminaryExample.zip

We’ll be updating RPH and releasing the proper API as soon as possible.

If you need help developing for RPH, you can join our Discord:

https://discord.gg/0v9TP1BOmfwZms7y

You can also find us on our website http://ragepluginhook.net

Or Twitter https://twitter.com/RAGEPluginHook

Enjoy!

The RAGE Plugin Hook Developer Team

MulleDK19 & LMS