-
Notifications
You must be signed in to change notification settings - Fork 1
3.0 ‐ Hooks (method calls on events)
For those of you who don't have a lot of experience with programming, you may be unfamiliar with what a hook is. To clarify, a hook is a mechanism that allows us to intercept and interact with event handlers, enabling us to execute our own code when a specific event occurs.
Why would you want this? There are a variety of reasons-- If you'd like to see when a player joins, leaves, chats, dies, kills, builds, and more, this is why a hook is useful. On top of that, it goes for more than just player events. There are world events, NPC events, almost anything you can think of. While initially, this page may not be filled with content, we are hoping to document each hook individually as an end goal.
Continuing from 2.0, where we added our own custom command, our code looks like this:
Code from previous chapter
using Terraria;
using Terraria.ID;
using TerrariaApi.Server;
using TShockAPI;
namespace TShockTutorials
{
[ApiVersion(2, 1)]
public class TShockTutorialsPlugin : TerrariaPlugin
{
public override string Author => "Average";
public override string Name => "TShock Tutorial Plugin";
public override string Description => "A sample plugin for educating aspiring TShock developers.";
public override Version Version => new(1, 0);
public TShockTutorialsPlugin(Main game) : base(game)
{
}
public override void Initialize()
{
// new Command("permission.nodes", "add.as.many", "as.you.like", ourCommandMethod, "these", "are", "aliases");
Commands.ChatCommands.Add(new Command("tutorial.command", TutorialCommand, "tutorial", "tcmd"));
}
private void TutorialCommand(CommandArgs args)
{
// retrieve our player from the CommandArgs object
var player = args.Player;
// if the player doesn't exist, return (this is done to prevent errors)
// guards like this should be implemented frequently to avoid exceptions
if (player == null) return;
// make sure the player is active and not being run by the server
if (player.Active && player.RealPlayer) return;
// create a new Random class, for random number generation
Random rand = new();
// create a new empty item object
Item randItem = new();
// get a random item id
// you will need to add 'using Terraria.ID' to use ItemID
int randItemID = rand.Next(0, ItemID.Count);
// turn our newly created item class into our desired item
randItem.SetDefaults(randItemID);
// we can also specify a prefix and stack size if we so desire:
randItem.stack = 128;
randItem.prefix = PrefixID.Legendary;
// give our player the item
player.GiveItem(randItem.type, randItem.stack, randItem.prefix);
// tell our player
player.SendSuccessMessage($"You have been given {randItem.stack}x {randItem.Name} ({Lang.prefix[randItem.prefix].Value}!");
}
}
}
We will be registering our hooks in the Initialize()
method, and can begin by deciding which event we would like to hook into. There are a variety of hooks we can utilize. In Visual Studio, we can see a list of available hooks within classes using IntelliSense. Example:
This is a really easy way to check out what kind of hooks are available. Otherwise, there are a few resources to find available hooks:
- TShock Hooks - Direct Github source
- More TShock Hooks w/ GetDataHandlers - Direct Github source
- OTAPI Hooks - OTAPI3 Hooks - Most hooks are listed on the first link, however some methods have been renamed since this was created, refer to second link for discrepancies.
- TSAPI Hooks - Direct Github source
When you have found an event you want, you can begin to register the hook. Depending on which hook you utilize, the process is different from the classes being used.
This section is for TShock related events like accounts, player logins, reloading, and region hooks. These hooks don't really have anything to do with Terraria itself and is fully related to the TShock API. We will look at two different hooks.
public override void Initialize()
{
// new Command("permission.nodes", "add.as.many", "as.you.like", ourCommandMethod, "these", "are", "aliases");
Commands.ChatCommands.Add(new Command("tutorial.command", TutorialCommand, "tutorial", "tcmd"));
GeneralHooks.ReloadEvent += OnServerReload;
PlayerHooks.PlayerPostLogin += OnPlayerLogin;
}
So, what's happening here is we are setting up event handlers for specific events in the TShock API, specifically the reload and player login events. When these events occur, the corresponding methods (OnServerReload
and OnPlayerLogin
) will be executed. This allows the plugin to respond to and perform custom actions when the server reloads or when a player logs into the game. For the sake of this tutorial, I added TShockAPI.Hooks...
, however if you have a using directive for TShockAPI.Hooks, this is not necessary.
If you've entered the code yourself, you'll notice there is an error that appears. This is because we haven't actually created the methods we are assigning. Use Visual Studio's quick actions to generate these for you:
The two methods should have generated and look like this, if you are not using Visual Studio feel free to copy this:
private void OnPlayerLogin(PlayerPostLoginEventArgs e)
{
}
private void OnServerReload(ReloadEventArgs e)
{
}
Let's practice some code conventions and give a better name for our arguments than e
, we'll call them eventArgs
, but args
will also do. Now, let's implement something to these events. We are going to add to our OnPlayerLogin event, which looks like this right now:
private void OnPlayerLogin(PlayerPostLoginEventArgs eventArgs)
{
// when the player logs in, code inside this method will be executed
}
We will implement a message thanking the player for logging in and give them some nice buffs. We can implement what we want like so:
private void OnPlayerLogin(PlayerPostLoginEventArgs eventArgs)
{
// retrieve player from args
var player = eventArgs.Player;
// normally i'd recommend checking if the player is valid anytime a player is accessed
// however, since we know the player just logged in we can assume they are real &
// should be logged in
// send the player a nice little greeting
player.SendMessage($"Welcome to our server, {player.Name}, thanks for logging in!" +
$"Here are some nice buffs :)", Color.Aquamarine);
// apply some buffs to the player
player.SetBuff(BuffID.Swiftness);
player.SetBuff(BuffID.Regeneration, 1200);
}
Just a note here, you may notice we are not using SendSuccessMessage()
. Instead, we are simply using SendMessage(), which as well as a message to send requires a Microsoft.Xna.Framework.Color
, make sure you add a using directive for Microsoft.Xna.Framework
if you'd like to use this.
So, there we go that's an example for our player login hook. Now let's focus our attention on our server reload event... I mention this one specifically because it is incredibly useful. Users expect /reload to reload your configuration and whatnot, basically allowing new information to be retrieved without restarting the server.
Here's an example of something like this:
private void OnServerReload(ReloadEventArgs eventArgs)
{
var playerReloading = eventArgs.Player;
try
{
// pretend we are loading a config here...
// something like Config.Load();
playerReloading.SendSuccessMessage("[TutorialPlugin] Config reloaded!");
}
catch(Exception ex)
{
playerReloading.SendErrorMessage("There was an issue loading the config!");
TShock.Log.ConsoleError(ex.ToString());
}
}
By using a try-catch statement here, if something goes wrong while reloading our config, it will catch the error, notify the player and output the error to the console and TShock log. Since we do not have a configuration file, nothing will actually happen. It will always just tell the player the config has loaded.
There are a few TShock hooks, but I recommend to play around with them yourself. Using these instructions you should be able to get a good idea of how they work.
Terraria's netcode works by sending packets back and forth between the clients and the server. Using TShock's built in GetDataHandlers class, we can retrieve information about these packets, which are usually sent when an event fires. Personally, I dislike using these and would rather retrieve the packets in a different way, these are available to you as well.
In this example, we are going to try and intercept the ChestOpen packet. Looking at the multiplayer packet structure provided by TShock, we can understand we should be retrieving tile coordinates of X and Y, along with the player opening the chest. We can use this to retrieve the Chest
. We can register
public override void Initialize()
{
GetDataHandlers.ChestOpen += OnChestOpen;
}
Now, like with the standard TShockAPI hooks, we need to define the method that this hook will execute. However, this is done a bit differently. I've come to find Visual Studio cannot generate these methods with its quick actions, so we will have to type it ourselves. Follow this format for GetDataHandlers
hooks:
public void OnChestOpen(object _, GetDataHandlers.ChestOpenEventArgs args)
{
}
If you are using a different hook, replace the ChestOpenEventArgs
with the appropriate event you are hooking into. The _
keyword in C# is used to discard variables. Since we will not be using the object
argument, we can get rid of it safely.
Let's say for this example, when a player opens a chest, the sixth item will be removed and replaced by air. We can do this with the following code:
public void OnChestOpen(object _, GetDataHandlers.ChestOpenEventArgs args)
{
// find the chest at the coordinates
Chest chest = Main.chest.FirstOrDefault(x => x.x == args.X && x.y == args.Y);
// if we cannot find the chest, return the method (stop executing any further)
if (chest == null) return;
// set the sixth item in the chest to an empty item
chest.item[5] = new Item();
// send appropriate packet to update chest item for all players
TSPlayer.All.SendData(PacketTypes.ChestItem, "", Main.chest.ToList().IndexOf(chest), 5, 0, 0, new Item().netID);
}
When we do certain things like updating things on the server, we need to send a new packet informing player's clients of the new changes. That is the function of the bottom line.
At this point, we've implemented our hook and added some functionality to it. Feel free to check out the rest of GetDataHandler events and play with them on your own time!
OTAPI Hooks are great, because they allow you to be very specific in what you want, and work the same way as other hooks. Let's register one in our Initialize()
method, like so:
public override void Initialize()
{
Hooks.NPC.DropLoot += DropLoot;
}
Like previously, we have not yet created our DropLoot method. You can use Visual Studio's quick actions to generate one. It should look like this:
private void DropLoot(object _, Hooks.NPC.DropLootEventArgs e)
{
}
Notice how I have changed the first argument to _, which means it will be discarded and not usable. We can safely do this because we will not be using it.
Let's say we want to cancel the loot drop for the NPC, and announce it to every player in the server:
private void DropLoot(object _, Hooks.NPC.DropLootEventArgs e)
{
TSPlayer.All.SendMessage($"Cancelled loot drop for: {e.Npc.FullName}", Color.IndianRed);
e.Result = HookResult.Cancel;
}
In this code, we are sending every player a message by using TSPlayer.All
, we also output the NPC's name within this same message. Along with this, we cancelling the event to prevent loot being dropped from this NPC.
ServerApi hooks are my absolute favourite and the ones I use the most often when I can. They are the easiest to work with and typically everything you need is provided. However, ServerApi hooks are registered slightly differently, like so:
public override void Initialize()
{
ServerApi.Hooks.ServerLeave.Register(this, OnPlayerDisconnect);
}
In this, we are registering a method to the ServerLeave event handler, along with providing the plugin main class to the ServerApi (that's what the this
keyword refers to)
Let's create our OnPlayerDisconnect
method. We can also use Visual Studio's quick actions to generate one for us:
private void OnPlayerDisconnect(LeaveEventArgs args)
{
}
When a player leaves, let's kill every other player. Just for fun.. haha. We can implement it like so:
private void OnPlayerDisconnect(LeaveEventArgs args)
{
// retrieve our player
var player = Main.player[args.Who];
// let's see if we can access the player
if(player == null) return;
// let's announce to everyone they are about to die
TSPlayer.All.SendMessage($"{player.name} has left, so you will all die! For some reason...", Color.Tomato);
// kill every player
foreach(TSPlayer plr in TShock.Players)
{
// skip player if cant be accessed
if(plr == null) continue;
// kill valid players muahahaah
plr.KillPlayer();
}
}
You might be wondering why we are using a foreach statement for this. This is to check if a player is valid, because sometimes if you do not, an exception will get thrown due to the player in the array being unassigned or uninitialized. Anyway, that's that. Definitely look into the ServerApi Hooks, as they will be your best friend!
At the end of all this, your code should look like this:
using OTAPI;
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.ID;
using TerrariaApi.Server;
using TShockAPI;
using TShockAPI.Hooks;
namespace TShockTutorials
{
[ApiVersion(2, 1)]
public class TShockTutorialsPlugin : TerrariaPlugin
{
public override string Author => "Average";
public override string Name => "TShock Tutorial Plugin";
public override string Description => "A sample plugin for educating aspiring TShock developers.";
public override Version Version => new(1, 0);
public TShockTutorialsPlugin(Main game) : base(game)
{
}
private void OnPlayerLogin(PlayerPostLoginEventArgs eventArgs)
{
// retrieve player from args
var player = eventArgs.Player;
// normally i'd recommend checking if the player is valid anytime a player is accessed
// however, since we know the player just logged in we can assume they are real &
// should be logged in
// send the player a nice little greeting
player.SendMessage($"Welcome to our server, {player.Name}, thanks for logging in!" +
$"Here are some nice buffs :)", Color.Aquamarine);
// apply some buffs to the player
player.SetBuff(BuffID.Swiftness);
player.SetBuff(BuffID.Regeneration, 1200);
}
private void OnServerReload(ReloadEventArgs eventArgs)
{
var playerReloading = eventArgs.Player;
try
{
// pretend we are loading a config here...
// something like Config.Load();
playerReloading.SendSuccessMessage("[TutorialPlugin] Config reloaded!");
}
catch (Exception ex)
{
playerReloading.SendErrorMessage("There was an issue loading the config!");
TShock.Log.ConsoleError(ex.ToString());
}
}
public override void Initialize()
{
// new Command("permission.nodes", "add.as.many", "as.you.like", ourCommandMethod, "these", "are", "aliases");
Commands.ChatCommands.Add(new Command("tutorial.command", TutorialCommand, "tutorial", "tcmd"));
GeneralHooks.ReloadEvent += OnServerReload;
PlayerHooks.PlayerPostLogin += OnPlayerLogin;
Hooks.NPC.DropLoot += DropLoot;
GetDataHandlers.ChestOpen += OnChestOpen;
ServerApi.Hooks.ServerLeave.Register(this, OnPlayerDisconnect);
}
private void OnPlayerDisconnect(LeaveEventArgs args)
{
// retrieve our player
var player = Main.player[args.Who];
// let's see if we can access the player
if(player == null) return;
// let's announce to everyone they are about to die
TSPlayer.All.SendMessage($"{player.name} has left, so you will all die! For some reason...", Color.Tomato);
// kill every player
foreach(TSPlayer plr in TShock.Players)
{
// skip player if cant be accessed
if(plr == null) continue;
// kill valid players muahahaah
plr.KillPlayer();
}
}
private void DropLoot(object _, Hooks.NPC.DropLootEventArgs e)
{
TSPlayer.All.SendMessage($"Cancelled loot drop for: {e.Npc.FullName}", Color.IndianRed);
e.Result = HookResult.Cancel;
}
public void OnChestOpen(object _, GetDataHandlers.ChestOpenEventArgs args)
{
// find the chest at the coordinates
Chest chest = Main.chest.FirstOrDefault(x => x.x == args.X && x.y == args.Y);
// if we cannot find the chest, return the method (stop executing any further)
if (chest == null) return;
// set the sixth item in the chest to an empty item
chest.item[5] = new Item();
// send appropriate packet to update chest item for all players
TSPlayer.All.SendData(PacketTypes.ChestItem, "", Main.chest.ToList().IndexOf(chest), 5, 0, 0, new Item().netID);
}
private void TutorialCommand(CommandArgs args)
{
// retrieve our player from the CommandArgs object
var player = args.Player;
// if the player doesn't exist, return (this is done to prevent errors)
// guards like this should be implemented frequently to avoid exceptions
if (player == null) return;
// make sure the player is active and not being run by the server
if (player.Active && player.RealPlayer) return;
// create a new Random class, for random number generation
Random rand = new();
// create a new empty item object
Item randItem = new();
// get a random item id
// you will need to add 'using Terraria.ID' to use ItemID
int randItemID = rand.Next(0, ItemID.Count);
// turn our newly created item class into our desired item
randItem.SetDefaults(randItemID);
// we can also specify a prefix and stack size if we so desire:
randItem.stack = 128;
randItem.prefix = PrefixID.Legendary;
// give our player the item
player.GiveItem(randItem.type, randItem.stack, randItem.prefix);
// tell our player
player.SendSuccessMessage($"You have been given {randItem.stack}x {randItem.Name} ({Lang.prefix[randItem.prefix].Value}!");
}
}
}