[This is preliminary documentation and is subject to change.]
Most of the calls made to the game need to be synchronized. For this reason, plugins run in fibers, not threads (although the fibers have an underlying thread).
Plugins in RAGE Plugin Hook work much like regular programs, in the sense that they have an entry point, which executes, and once it runs out of scope, the plugin is unloaded (Unless it has other fibers running).
The entry point is specified in the PluginAttribute assembly attribute (See "Making a plugin").
If no entry point has been specified, RAGE Plugin Hook will use the first parameterless static Main method it finds. To ensure it doesn't pick the wrong entry point, you should always indicate the explicit entry point in the attribute.
The example below, is a simple plugin that, when loaded, kills the player, then unloads.
[assembly: Rage.Attributes.Plugin("Example Plugin", Description = "Kills the player")] public static class EntryPoint { public static void Main() { Rage.Game.LocalPlayer.Character.Kill(); } }
Fibers are represented by the GameFiber class.
Plugins can have one or more fibers. The entry point is executed in a fiber.
Most calls to the API requires the code to be executing in a fiber; so while you can create and use regular threads, you cannot make most API calls from them.
Unlike threads, fibers yield themselves to let other fibers run, including the game.
Fibers can yield themselves by calling Yield, Sleep or Wait.
The following example will freeze the game forever:
[assembly: Rage.Attributes.Plugin("Example Plugin", Description = "Freezes the game, forever")] public static class EntryPoint { public static void Main() { while (true) { } } }
To prevent this, the fiber must yield, to let other plugins run, as well as the game.
[assembly: Rage.Attributes.Plugin("Example Plugin", Description = "Does nothing, forever")] public static class EntryPoint { public static void Main() { while (true) { Rage.GameFiber.Yield(); } } }
Fibers will keep executing until they yield themselves. Plugins can also sleep (yield for a duration) to add delays (Note: Do not call Thread.Sleep() as it'll freeze the game for the duration of the sleep).
using Rage; [assembly: Rage.Attributes.Plugin("Example Plugin", Description = "Makes a character go into a car, drive away, then blow up.")] public static class EntryPoint { public static void Main() { Ped ped = null; Vehicle vehicle = null; try { // Get a position 15 meters in front of the player. Vector3 spawnPosition1 = Game.LocalPlayer.Character.GetOffsetPositionFront(15f); // Get a position 7 meters in front of the player. Vector3 spawnPosition2 = Game.LocalPlayer.Character.GetOffsetPositionFront(7f); // Create a character (Pedestrian) 15 meters in front of the player. ped = new Ped(spawnPosition1); // Prevent the character from doing something we haven't told it to do (Eg. fleeing from gunfire). ped.BlockPermanentEvents = true; // Yield for 3 real time seconds. GameFiber.Sleep(3000); // Create a vehicle 7 meters in front of the player. vehicle = new Vehicle("FUTO", spawnPosition2); // Yield for 1 game time seconds. // Wait() is affected by the time scale. Sleep() is not. // Sleep(1000) will always sleep 1 second, regardless of the time scale. // The Wait(1000) below, will sleep for 3 seconds, because we set the time scale to a third before. Game.TimeScale = 0.333333f; GameFiber.Wait(1000); Game.TimeScale = 1f; // Since we've yielded, letting the game and other plugins run, our character and vehicle may have been deleted in the mean time. // The character may also have died, or the vehicle may have blown up. // Let's verify that's not the case. if ((!ped.Exists() || ped.IsDead) || (!vehicle.Exists() || vehicle.IsDead)) { // Inform the user. Game.DisplayNotification("The character or vehicle was killed or deleted prematurely."); return; } // Make the ped enter the vehicle on the driver's seat (Second parameter is the passenger seat index, thus -1 is the driver's seat). Task task = ped.Tasks.EnterVehicle(vehicle, -1); // Yield the fiber until the ped has gotten into the vehicle. while (true) { // Did the character stop existing or die? if (!ped.Exists() || ped.IsDead) { // Stop waiting. break; } // Is the character in the vehicle? if (ped.IsInVehicle(vehicle, false)) { // Stop waiting. break; } GameFiber.Yield(); } // Since we've yielded, letting the game and other plugins run, our character and vehicle may have been deleted in the mean time. // Let's verify that's not the case. if ((!ped.Exists() || ped.IsDead) || (!vehicle.Exists() || vehicle.IsDead)) { // Inform the user. Game.DisplayNotification("The character or vehicle was killed or deleted prematurely."); return; } // Make the ped drive the vehicle at 180 km/h (50 m/s). ped.Tasks.CruiseWithVehicle(50f, VehicleDrivingFlags.Emergency); const float FiftyKilometersPerHour = 13.8888889f; // 50km/h is 13.8888 m/s. // Inform the user that the car will blow at 50 km/h. Game.DisplayNotification($"The car will explode once it reaches {MathHelper.ConvertMetersPerSecondToKilometersPerHour(FiftyKilometersPerHour)} km/h."); // Wait until the vehicle reaches the desired speed. while (true) { // Verify the vehicle still exists. if (!vehicle.Exists()) { // The vehicle has been deleted. Nothing more to do. return; } // Check if the vehicle has reached the desired speed. float speed = vehicle.Speed; if (speed >= FiftyKilometersPerHour) { // If so, break the loop. break; } // Let's implement a fail safe, in case the ped dies or leaves the vehicle. if (!ped.Exists() || ped.IsDead || !ped.IsInVehicle(vehicle, false)) { // Break the loop and blow up the vehicle. Game.DisplayNotification("Driver died or left the vehicle!"); break; } // Let the user know how fast the vehicle is currently going. float speedInKmh = MathHelper.ConvertMetersPerSecondToKilometersPerHour(speed); Game.DisplaySubtitle($"Speed: {speedInKmh} km/h", 100); // Yield, to let other fibers and the game process. // Without yielding, the game would be unable to process, so // the character and vehicle wouldn't move, and the speed would // never reach the desired value (and the game would be frozen). GameFiber.Yield(); } // Verify the vehicle still exists. if (vehicle.Exists()) { // Inform the user that the car has blown up. Game.DisplayNotification("BOOM!"); // Blow up the vehicle. vehicle.Explode(); // Yield the fiber for 3 seconds. GameFiber.Sleep(3000); } } finally { // Either, all went well, or an exception occurred. In either case, clean up. if (ped.Exists()) { ped.Delete(); ped = null; } if (vehicle.Exists()) { vehicle.Delete(); vehicle = null; } } } }
New fibers can be started from existing fibers, or from regular threads.
using Rage; using Rage.Native; [assembly: Rage.Attributes.Plugin("Example Plugin", Description = "Spawns 10 characters and makes them run a random distance, then blow up.")] public static class EntryPoint { public static void Main() { // Teleport the player to the air strip in the Grand Senora Desert. Ped playerPed = Game.LocalPlayer.Character; playerPed.Position = new Vector3(1357.146f, 3124.359f, 40.9087f); playerPed.Heading = 270f; // Start 10 new fibers, that'll each spawn a character, make it run a random distance, then blow itself up. for (int i = 0; i < 10; i++) { // Start a new fiber, that'll execute the HandleSuicideBomber method. GameFiber.StartNew(EntryPoint.HandleSuicideBomber); } // Plugins will unload when all their fibers have ended. // When 'Main' runs out of scope, its fiber will end, but the plugin will not unload, as we still have the 10 fibers we started above running. } public static void HandleSuicideBomber() { // Spawn a new character 50 meters in front of the player. Ped ped = new Ped(Game.LocalPlayer.Character.GetOffsetPositionFront(50f)); ped.BlockPermanentEvents = true; try { // Get a random position between 25 to 50 meters in a random direction. Vector3 targetPosition = ped.Position.Around2D(MathHelper.GetRandomSingle(25f, 50f)); // Try to adjust the position to 1 meter above the ground. float z; if (NativeFunction.Natives.GetGroundZFor3dCoord<bool>(targetPosition.X, targetPosition.Y, targetPosition.Z, out z, false)) { targetPosition.Z = z + 1f; } ped.Tasks.FollowNavigationMeshToPosition(targetPosition, MathHelper.GetRandomSingle(0f, 360f), 3f); // Go there. // Wait for the ped to reach the position. // The following loop will yield the plugin until the character has reached its destination. // Before this yield, this fiber executed all the lines above, until the call below, // without any other plugins or the game being able to execute any code. // The call below will make this fiber yield itself, so the next fiber and the game can execute code, and so on. while (true) { // If the character has ceased to exist, or has died... if (!ped.Exists() || ped.IsDead) { // .. break and blow it up. break; } // Is the character 3 meters or less away from the target position? Vector3 pedPosition = ped.Position; if (pedPosition.DistanceTo2D(targetPosition) <= 3f) { // If so, break out of the loop. break; } // To help us debug, let's draw a line from the character's current position, to the target position. NativeFunction.Natives.DrawLine(pedPosition.X, pedPosition.Y, pedPosition.Z, targetPosition.X, targetPosition.Y, targetPosition.Z, 255, 0, 0, 255); GameFiber.Yield(); } // The character has reached the destination (Or the task was aborted). Blow it up. if (ped.Exists()) { World.SpawnExplosion(ped.Position, 0, 10f, true, false, 0.1f); } } finally { if (ped.Exists()) { ped.Delete(); ped = null; } } // Once this method returns, the fiber will end. // Once all 10 fibers have ended, the plugin will be automatically unloaded. } }
Whenever a fiber yields, all other fibers in the plugin will run; then all fibers in all other plugins will run; then the game will run; then the process repeats.