Skip to content

4.0 ‐ Plugin Configuration

average edited this page Oct 27, 2023 · 4 revisions

4.0 - Plugin Configuration

Hello everyone! To follow this guide, it's helpful to have some prior knowledge about how configurations function. We'll be utilizing JSON, which is the typical format for TShock plugins. While you could employ alternative file formats like YAML, this would necessitate additional investigation on your end.

JSON functions by converting object data into a human-readable structure. This is known as serialization.

Example of basic JSON and its representation:

{
  "name": "John Doe",
  "age": 30,
  "city": "New York"
}

In this example, the JSON data represents information about an individual named John Doe, including their name, age, and city.

We can then deserialize this JSON object into a proper C# object, let's say we had a C# class called 'Person', this json data could be represented as the following in C#:

public class Person
{
    public string Name { get; set; } = "John Doe";
    public int Age { get; set; } = 30;
    public string City { get; set; } = "New York";
}

Now that you have a brief understanding of JSON, let's put it into action.

4.1 - Creating our configuration class

We first of all need to define some values we want the user to change, for this let's create a new class. It's good practice to separate new classes into separate files. Let's do it by right clicking our project in the Solution Explorer and creating a new class. I will be naming mine PluginSettings.cs, but feel free to name it whatever you want.

namespace TShockTutorials
{
    public class PluginSettings
    {
    }
}

As of now, we have a blank class that contains no values. Let's add some values we want the user to be able to configure.

    public class PluginSettings
    {
        public string GameName { get; set; } = "Weird Fact of the Day";
        public List<string> WeirdFacts { get; set; } = new List<string>()
        {
            "Octopuses have three hearts, two pump blood to the gills, and one pumps it to the rest of the body",
            "Honey never spoils; archaeologists have found pots of honey in ancient Egyptian tombs that are over 3,000 years old.",
            "Bananas are berries, but strawberries are not.",
            "The world's largest desert is not the Sahara; it's Antarctica.",
            "A group of flamingos is called a 'flamboyance.'",
            "The Eiffel Tower can be 6 inches taller during the summer due to the expansion of iron in the heat.",
            "Wombat feces are cube-shaped.",
            "There's a town in Norway called 'Hell,' and it occasionally freezes over.",
            "In Switzerland, it is illegal to own just one guinea pig because they get lonely.",
            "The oldest known sample of the smallpox virus was found in the teeth of a 17th-century child buried in Lithuania."
        };

        public int ChosenNumber { get; set; } = 666;

    }

Here, we have created three properties, which are defined by adding a getter and setter, i.e { get; set; }, these are JSON compatible. If we want to change what the JSON property name will be, we can add an annotation like so:

        [JsonProperty("CoolestNumber")]
        public int ChosenNumber { get; set; } = 666;

Now, when this JSON property gets serialized, it will be represented by CoolestNumber rather than ChosenNumber. We can also change the order at which it is displayed:

        [JsonProperty("CoolestNumber", Order = 3)]
        public int ChosenNumber { get; set; } = 666;

Now we are making sure CoolestNumber is displayed last in the JSON file.

Anyway, enough screwing around. Let's add a static property instance of our config. This is where our config will be held, loaded and saved to. Add this as the first property in the class.

    public class PluginSettings
    {
        public static PluginSettings Config { get; set; } = new();

        ...
    }

Cool! Now why are we making this static? Well, we only want one instance of our config, and now we can reference it with PluginSettings.Config anywhere we want. However, in our current state we have a simple class with default values, no config yet.

Let's create a new static method underneath all our properties, called Save():

        public static void Save()
        {

        }

Let us also add a new variable, right above our Instance property, this one will be for our filepath at which we store the config:

        private static string filePath = Path.Combine(TShock.SavePath, "tutorial.json");

This is static, so it will be accessible to our static methods, but also private because we will not need access to this variable outside of our config class. Following proper C# naming conventions, we are using camelCase rather than PascalCase due to it being private.

Now, let's populate our Save() method with some code so it actually functions:

        public static void Save()
        {
            string json = JsonConvert.SerializeObject(Config, Formatting.Indented);
            File.WriteAllText(filePath, json);
        }

It's pretty simple, we are simply serializing our current instance values (which are defaults) and putting it into a string. Formatting.Indented is useful for serialization because otherwise the JSON would be displayed all in one line rather than on multiple lines. While it makes no difference for a computer, it makes it much easier to read for humans.

Then, we take our newly created string and write to our filepath with the text contents. That's all for our Save() method.

While this allows us to write to our config, how about reading from it and replacing our values with their respective values? Well, let's create a new public static method called Load():

       public static void Load()
        {

        }

Let's add our logic for loading our configuration from the filepath!

       public static void Load()
        {
            if (File.Exists(filePath))
            {
                string json = File.ReadAllText(filePath);
                try
                {
                    Config = JsonConvert.DeserializeObject<PluginSettings>(json);
                }
                catch (Exception ex)
                {
                    TShock.Log.ConsoleError("Config could not load: " + ex.Message);
                    TShock.Log.ConsoleError(ex.StackTrace);
                }
            }
            else
            {
                Save();
            }
        }

With this code, we are checking first if the file exists at all, if it doesn't we call our previously created Save() method which will create one for us. If a file is found, we will read all contents from it and save it as a string. We then use a try-catch statement for deserializing our Config object, and assigning it to our instance of the config. We are wrapping it in a try-catch, because if the deserialization method encounters an exception we will log it in the console and provide the stack trace. This usually only happens if there is an error in the JSON file or if it can't serialize/deserialize a given object type.

4.2 - Plugin interaction w/ config

Now that we have that setup, let's head to our plugin main class. At the top of our class, outside of any methods, we will add the following auto implemented property:

public static PluginSettings Config => PluginSettings.Config;

Now, in our Initialize method, let's load our config. This will read our config at server start and assign the respective values to our config instance object. We can do it like this:

        public override void Initialize()
        {
            // register our config
            PluginSettings.Load();
           
            ...
        }

And now, we can access any of our properties directly.

Oh, but let's not forget something fairly important. We want us to be able to reload the config without us having to restart the server. How can we do this? Well, it's fairly easy.

Remember when we registered this hook?

GeneralHooks.ReloadEvent += OnServerReload;

Let's head to the method definition of OnServerReload():

        private void OnServerReload(ReloadEventArgs eventArgs)
        {
            var playerReloading = eventArgs.Player;

            try
            {
                // pretend we are loading a config here... <----------- we're doing something with this
                // 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());
            }
        }

Replace our comment (you probably don't have one) with a simple call to PluginSettings.Load() (or whatever you named your config class)

Your code should now look like this:

        private void OnServerReload(ReloadEventArgs eventArgs)
        {
            var playerReloading = eventArgs.Player;

            try
            {
                PluginSettings.Load();
                playerReloading.SendSuccessMessage("[TutorialPlugin] Config reloaded!");
            }
            catch (Exception ex)
            {
                playerReloading.SendErrorMessage("There was an issue loading the config!");
                TShock.Log.ConsoleError(ex.ToString());
            }
        }

Awesome, now when one of your admins types /reload, our config will also be reloaded with any new values that may have been entered or modified.

4.3 - Using our config values

Here is an examplar command, with references to our config:

        private void ConfigExemplarCommand(CommandArgs args)
        {
            // retrieve our player
            var player = args.Player;

            // retrieve all of our fun facts
            var funFacts = Config.WeirdFacts;

            // retrieve one fun fact randomly
            var funFact = funFacts[Main.rand.Next(0, funFacts.Count)];

            // send the player the fact
            player.SendMessage($"{Config.GameName}", Color.LightCoral);
            player.SendMessage($"{funFact}", Color.PaleGoldenrod);

            // give the player a zenith with a quantity of our "cool" number
            player.GiveItem(ItemID.Zenith, Config.ChosenNumber, PrefixID.Annoying);
        }

Reading through this code, it becomes obvious we can work with our config values as if they are normal variables, however in tshock/yourconfig.json these values can be manipulated and loaded to suit whatever your usecase may be. The pivotal takeaway here is: use configs whenever you can!