Skip to content

4.5 ‐ Let's get organized!

average edited this page Oct 28, 2023 · 4 revisions

4.5 - So why do we need to be organized?

You may likely have noticed the growing complexity of our primary plugin class. As it stands, we've already accumulated nearly 200 lines of code, and yet, our plugin's functionality remains quite limited.

Recognizing the significance of maintaining an organized codebase is essential, particularly as your plugin expands in scope – and believe me, it will. This principle of organization is not only a cornerstone of effective TShock plugin development but also a fundamental element of sound coding practices.

As your project evolves, adhering to these practices will greatly facilitate maintenance and collaboration while ensuring the scalability and efficiency of your codebase. This is why this chapter is 4.5, while not necessary, it is advised. If you have already been practising organization already, you can likely skip this one.

4.6 - Abstracting our events

To enhance the organization and flexibility of our code, we'll be optimizing our events and making them more versatile. To begin, let's create a new folder within our solution, named Models, where we'll store external C# classes. Inside this folder, we will create a file named Event.cs.

In this process, we will transform the Event class into an abstract class, denoting that it cannot be instantiated. Its primary purpose is to serve as a blueprint for other classes to inherit from, which promotes clean and organized code. Additionally, this approach allows us to enable or disable specific hooks as needed. The methods Enable() and Disable() will be responsible for registering and unregistering the hook, respectively.

These methods will require an instance of our TShockTutorialsPlugin class since some hooks may rely on a reference to the main plugin class. The EventMethod() method will be invoked when the hook is triggered. Should we require different parameters or return types, we can easily create new methods to accommodate those specific needs.

public abstract class Event // <--- This class is now declared as abstract
{
    public abstract void Enable(TerrariaPlugin plugin);
    public abstract void Disable(TerrariaPlugin plugin);

    public void EventMethod() { }
}

Let's create a new folder, call it Events. This is where we'll create our modular hooks, inheriting our previously made Event abstract base class. I will run you through the creation of one hook, the rest are for you to do. For this run-through, we will be using the OnPlayerLogin hook.

Create a file in the folder called OnPlayerLogin.cs, and inherit Event:

using TShockTutorials.Models;
namespace TShockTutorials.Events
{
    public class OnPlayerLogin
    {
    }
}

You can use Visual Studio's quick actions to generate the methods we require:

image

Make sure to remove the throw statements, as will be replacing the code as follows:

using Microsoft.Xna.Framework;
using Terraria.ID;
using TerrariaApi.Server;
using TShockAPI.Hooks;
using TShockTutorials.Models;

namespace TShockTutorials.Events
{
    public class OnPlayerLogin : Event
    {
        public override void Enable(TerrariaPlugin plugin)
        {
            PlayerHooks.PlayerPostLogin += ExecuteMethod;
        }
        public override void Disable(TerrariaPlugin plugin)
        {
            PlayerHooks.PlayerPostLogin -= ExecuteMethod;
        }

        private void ExecuteMethod(PlayerPostLoginEventArgs e)
        {
            // retrieve player from args
            var player = e.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);
        }
    }
}

You can pretty much just register the hook as you normally would, while also providing a way to de-register, see the example above. You can copy your previous code from the main plugin class and paste it in ExecuteMethod() like I did. You can remove the old method from the main plugin class.

Do this for every event that we have previously created, making it's own Event, with its appropriate methods.

After you're done, let's begin creating the EventManager, create a new file called EventManager.cs in the root folder of your plugin. In here, let's create a region with #region and #endregion:

Recording 2023-10-27 023728

As you can see, we can give our region a name and collapse and uncollapse in most IDEs. Now, let's create a bunch of static members for our newly created Events:

using TShockTutorials.Events;

namespace TShockTutorials
{
    public class EventManager
    {
        #region Events
        public static OnChestOpen OnChestOpen = new();
        public static OnDropLoot OnDropLoot = new();
        public static OnPlayerDisconnect OnPlayerDisconnect = new();
        public static OnPlayerLogin OnPlayerLogin = new();
        public static OnServerReload OnServerReload = new();
        #endregion

    }
}

We have now created static instances of all our events, allowing us to disable the hooks using EventManager.OurEvent.Disable() at any time. However, these hooks are not yet registered with the server. To accomplish this, let's first create a static list containing all of our events:

public static List<Event> Events = new List<Event>()
{
    OnChestOpen,
    OnDropLoot, 
    OnPlayerDisconnect,
    OnPlayerLogin, 
    OnServerReload,
};

Next, we'll add a new static method called RegisterAll inside EventManager:

public static void RegisterAll(TerrariaPlugin plugin)
{
    foreach(Event _event in Events) 
    {
        _event.Enable(plugin);
    }
}

The purpose of this code is to iterate through the list of events and enable them.

To complete this process, we need to make the following adjustments to the Initialize() method in our main plugin class:

public override void Initialize()
{
    ....

    // Register our hooks
    EventManager.RegisterAll(this);   // <--- Add this line

}

With these changes, the events will be registered when the server starts, and our main plugin class will remain concise with only about 90 lines of code. This practice of simplifying your code is akin to deep cleaning your house, and it's why we refer to it as "housekeeping."

Next up, we're gonna tackle the commands...

4.7 - Creating a command manager

We are going to employ some similar techniques for a command manager. First, let's begin by creating a new Model, we'll call the file Command.cs, our class will be abstract. The code will be as follows:

using TShockAPI;

namespace TShockTutorials.Models
{
    public abstract class Command
    {
        public abstract string[] Aliases { get; set; }
        public abstract string PermissionNode { get; set; }
        public abstract void Execute(CommandArgs args);
        public static implicit operator TShockAPI.Command(Command cmd) => new(cmd.PermissionNode,
            cmd.Execute, cmd.Aliases);
    }
}

For most of this it's pretty self-explanatory, however, the last method might be a bit confusing to some. What this method does is overrides the implicit casting of our Command class to TShock's command class, essentially allowing us to use the two interchangeably.

Let's create a new file in our root folder of our solution. We'll call this Permissions.cs -- this will be a class where we store all of our permission nodes.

namespace TShockTutorials
{
    public class Permissions
    {
        public static readonly string TutorialCommand = "tutorial.command";
        public static readonly string ConfigExemplarCommand = "tutorial.configex";
    }
}

We are making each permission node static and readonly, this is almost identical to a constant, but it's safer to use a static readonly. Now, let's create a new folder called Commands, and begin creating our command classes as follows:

using Terraria;
using Terraria.ID;
using TShockAPI;

namespace TShockTutorials.Commands
{
    public class TutorialCommand : Models.Command
    {
        public override string[] Aliases { get; set; } = { "tutorial", "tcmd" };
        public override string PermissionNode { get; set; } = Permissions.TutorialCommand;

        public override void Execute(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}!");
        }
    }
}

Do this for each one of your commands until we have them all in separate classes. Now after we've done that, let's create our CommandManager.cs class, this can be placed in the root folder and will feature a similar structure as our EventManager:

using TShockTutorials.Commands;
using TShockTutorials.Models;

namespace TShockTutorials
{
    public class CommandManager
    {
        #region Commands
        public static TutorialCommand TutorialCommand = new();
        public static ConfigExemplarCommand ConfigExemplarCommand = new();
        #endregion

        public static List<Command> Commands = new()
        {
            TutorialCommand,
            ConfigExemplarCommand
        };

        public static void RegisterAll()
        {
            foreach (Command cmd in Commands)
            {
                TShockAPI.Commands.ChatCommands.Add(cmd);
            }
        }
    }
}

Just to elaborate on what's going on here, we are creating new instances of our commands and adding them to a list, called Commands, we then have a static method RegisterAll() that iterates through each command from the list and adds them to TShock. Because we overrode the implicit conversion operator, we do not need to use any casts. Keep in mind, for each new command you create, it must be added to the Commands list.

We're pretty much done, just add the following line in our main plugin classes Initialize():

        public override void Initialize()
        {

            // register our commands
            CommandManager.RegisterAll();

        }

Make sure to get rid of any old code that we are no longer using, your main class should now look like this:

using Terraria;
using TerrariaApi.Server;

namespace TShockTutorials
{
    [ApiVersion(2, 1)]
    public class TShockTutorialsPlugin : TerrariaPlugin
    {
        public static PluginSettings Config => PluginSettings.Config;
        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()
        {
            // register our config
            PluginSettings.Load();

            // register our commands
            CommandManager.RegisterAll();

            // register our hooks
            EventManager.RegisterAll(this);
        }
    }
}

Awesome! We've turned our main class from a file with almost two hundred lines to one with only 31! Not to mention, everything is much more organized and done more sensibly, with easy access to everything you may need.