Towny Advanced: How to use Metadata for Skript and Kotlin

townypersistentmetadata.jpg

Hey there!

If you searched for how to easily use your own custom data with Towny, which can actually store it persistently for you and link the data to a Town or TownBlock object, then you found the right tutorial!

I'm writing this little post because I couldn't find any explanation how this works or what it does, besides the pull request on Github. So I'm sure there are at least some people that don't know how to use it with Skript or Kotlin and will search for it. By using this solution instead a custom solution, you can save a lot of time.


In this short tutorial, you will learn:

  1. What kind of data can be stored in Towny Metadata
  2. How to register a CustomDataField on load
  3. Getting a Towny TownBlock object from Towny
  4. Storing custom metadata into a TownBlock

1. What kind of data can be stored in Towny Metadata


At the moment of this writing, Towny allows to store 4 different types of data. These are: Boolean, Decimal, Integer and String. If you really wanted, you are able to convert your objects and data into a String and then store it. But keep in mind that Towny will lazy load the data into the memory once the part of the town is used in some way, so make sure to use it reasonable.


2. Register a CustomDataField on load


Now, we know which types can be used, we need to properly register the custom data field to Towny.
This is important, since Towny will have a name for each custom data field and each has to have a unique key so we have to register it first.

First of all, you need to create a data field object you want to register.

Skript:

import:
  com.palmergames.bukkit.towny.object.metadata.BooleanDataField
  com.palmergames.bukkit.towny.object.metadata.DecimalDataField
  com.palmergames.bukkit.towny.object.metadata.IntegerDataField
  com.palmergames.bukkit.towny.object.metadata.StringDataField

on load:
  set {_booleanDataField} to new BooleanDataField("Name1")
  set {_decimalDataField} to new DecimalDataField("Name2")
  set {_integerDataField} to new IntegerDataField("Name3")
  set {_stringDataField} to new StringDataField("Name4")

Kotlin:

import com.palmergames.bukkit.towny.object.metadata.BooleanDataField
import com.palmergames.bukkit.towny.object.metadata.DecimalDataField
import com.palmergames.bukkit.towny.object.metadata.IntegerDataField
import com.palmergames.bukkit.towny.object.metadata.StringDataField

val booleanDataField = BooleanDataField("Name1")
val decimalDataField = DecimalDataField("Name2")
val integerDataField = IntegerDataField("Name3")
val stringDataField = StringDataField("Name4")

Now, we got multiple custom data fields with different names that we can register. This should be done while starting the server, so keep in mind to register it or you may not be able to access it later on. This is how you can register the custom data fields, I only show how you register the BooleanDataField, but it is the same for each other field.

Skript:

import:
  com.palmergames.bukkit.towny.TownyAPI

on load:
  TownyAPI.getInstance().registerCustomDataField({_booleanDataField})

Kotlin:

import com.palmergames.bukkit.towny.TownyAPI

TownyAPI.getInstance().registerCustomDataField(booleanDataField)

This is all we have to do to register custom data fields, so they can be used later on.

3. Getting a Towny TownBlock object from Towny


If you already know how you get to a TownBlock, then you can skip this part.
Now, we need a object in which we want to store the data. The reason why this is used anyway is to have the data stored in the Town or TownBlock object, so we don't have to handle how to store the data for each object properly.

In the following code box, I show you how to get the TownBlock object, since this is probably the most interesting use case.

Skript:

import:
  com.palmergames.bukkit.towny.object.WorldCoord

set {_loc} to player's location
set {_townblock} to try WorldCoord.parseWorldCoord({_loc}).getTownBlock()

Kotlin:

import com.palmergames.bukkit.towny.object.WorldCoord

val townBlock = WorldCoord.parseWorldCoord(player.getLocation()).getTownBlock()

Now, we may have a TownBlock, keep in mind, that not every Chunk is also a TownBlock. Chunks, that are not within a town can be null or not set, if you request them, you have to check for that to prevent any errors.


4. Setting and getting custom metadata into a TownBlock

Finally, we can get to the most important thing: Actually setting and getting data into/from the metadata storage of Towny. To repeat it what I written above already: This is no dumpster for all your data you have to store. It is perfectly fine to store small amounts of data, like I do for custom flags of each plot, so the users can add unique flag settings to their TownBlock. I keep it there at a minimum and use booleans to use less memory.

This is how you can set a value, these functions can set a Boolean, but you can change them so they can store any types that are available:

Skript:

function setPlotMetaBoolean(loc: Location, key: Text, bool: boolean):
  set {_townblock} to try WorldCoord.parseWorldCoord({_loc}).getTownBlock()
  loop ...{_townblock}.getMetadata():
    if {_key} is loop-value.getKey():
      {_townblock}.removeMetaData(loop-value)
      stop loop
  set {_bdf} to new BooleanDataField({_key}, {_bool})
  try {_townblock}.addMetaData({_bdf})

Kotlin:

fun setPlotMetaBoolean(loc: Location, key: String, bool: Boolean) {
  try {
    val townBlock = WorldCoord.parseWorldCoord({_loc}).getTownBlock()
    townBlock.forEach {
      if (key == it.key) {
        townBlock.removeMetaData(it)
      }
    }
    val bdf = BooleanDataField(key, bool)
    townBlock.addMetaData(bdf)
  } catch (e: Exception) { println(e) }
}

Now we got a function to store a Boolean to a specific key that we previously registered. In this case, we registered the BooleanDataField with the key "Name1", so we can now set booleans with the key Name1 to either true or false into Town or Townblock objects.

Let's get the data back out of a Towny object like Town or TownBlock:
Skript:

function getPlotMetaBool(loc: Location, key: Text) :: boolean:
  set {_townblock} to try WorldCoord.parseWorldCoord({_loc}).getTownBlock()
  loop ...{_townblock}.getMetadata():
    if {_key} is loop-value.getKey():
      return "%loop-value.getValue()%" parsed as boolean

Kotlin:

fun getPlotMetaBool(loc: Location, key: String): Boolean? {
  try {
    WorldCoord.parseWorldCoord(loc).getTownBlock()?.let { townBlock ->
      townBlock.getMetadata().forEach {
        if (key == it.key) {
          return it.value
        }
      }
    }
  } catch (e: Expection) { 
    println(e)
    return null
  }
}

If you have reached this part and it is working for you, congratulations! You can now store data using Towny into a Town, TownBlock or even more Towny objects that allow it. Towny will lazy load the data as needed and make it easy for you to use it later in the game. So I hope this helped you.

My use case will be a custom flag system where players can toggle between specific events that are allowed or disallowed within a specific TownBlock. Users asked to enable or disable the use of homes within their plots for other players. Now, the users can toggle that for each TownBlock and I didn't have to introduce a complex lazy loading system to handle all the data, perfect!

If you got any questions, feel free to ask your questions in the comments, I help anyone who got questions. On Discord, you can also ask further questions or ideas for future topics.

H2
H3
H4
3 columns
2 columns
1 column
1 Comment
Ecency