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:
- What kind of data can be stored in Towny Metadata
- How to register a
CustomDataField
on load - Getting a Towny
TownBlock
object from Towny - 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.