Adding New Properties And Tools
You should already be familiar with how to use tags and mechanisms within scripts. This page will walk you through creating your own custom tags, mechanisms, and properties in Java.
Taking Tag Input
Let's mess with the player's UUID a bit more. We'll be editing our uuid_uppercase tag to take a boolean input that represents whether the UUID should be repeated once with a space in between.
We can use the attribute parameter to check if the tag has input with hasParam(). If so, we can get the input as an element with getParamElement(). The ElementTag class has a variety of methods for returning different primitive values depending on its internal value; we can get the boolean input by calling asBoolean().
The above logic can be turned into one if statement like so:
if (attribute.hasParam() && attribute.getParamElement().asBoolean()) {
// ...
}
We can then move our UUID string to a variable and modify it if the condition passes (concatenation is fine). After that, we just return it as an element like before.
tagProcessor.registerTag(ElementTag.class, "uuid_uppercase", (attribute, object) -> {
String uuid = object.getUUID().toString().toUpperCase();
if (attribute.hasParam() && attribute.getParamElement().asBoolean()) {
uuid = uuid + " " + uuid;
}
return new ElementTag(uuid);
});
With this setup, the tag input is optional; you don't even need to use []. If you test it and input true, however, it should return two uppercase UUIDs separated by a space.
Creating Mechanisms
NOTE: This section is subject to change in the future when mechanisms are updated to use a registration system akin to the tag registration system.
Unlike modern tags, mechanisms undergo a series of checks to "match" the mechanism. This is done in the class definition's adjust method, which takes a mechanism parameter. Mechanisms have a variety of methods to match and check input, which we'll see shortly.
To match a mechanism, use the matches(String) method and provide the mechanism name. To require a specific input type, you can make use of the methods starting with require, such as requireBoolean() and requireObject(Object). Wrap these methods in an if statement to start a mechanism block.
if (mechanism.matches("some_mechanism") && mechanism.requireBoolean()) {
// do stuff
}
Let's make a mechanism for an ItemTag called wrap_brackets that takes an integer as input. It'll wrap its display name in brackets with the specified amount of spaces in between. We can make use of the requireInteger() method for the input.
The ItemTag class has an Item instance variable named item. Since adjust isn't a static method, we can use instance variables directly inside of mechanism code. The display name, lore, enchantments, etc. all fall under "item meta," which we can check exists with hasItemMeta() and get with getItemMeta(). In the case there isn't any existing item meta, we'll throw an error.
Generally, you should try to check for reasonably possible invalid inputs and give a clean error to explain the user. This is not strictly necessary, but it is encouraged to help ensure that Denizen remains friendly to new scripters, as unchecked errors may be hard to track down and fix for the inexperienced.
There is an echoError("...") method for both attribute in tags and mechanism in mechanisms. That doesn't stop the code, however, so you'll need to return in both cases. Since you need to return something in a tag, you'll return null, as that is understood by the tag system to indicate the tag was invalid.
if (mechanism.matches("wrap_brackets") && mechanism.requireInteger()) {
if (getItemMeta() == null || !getItemMeta().hasDisplayName()) {
mechanism.echoError("This item doesn't have a display name!");
return;
}
}
Now that we're free of errors, we can use the getValue() method, which returns the input as an ElementTag. Building off of the Taking Tag Input section, we can use asInt() for the amount of brackets. After that, we can use some basic StringBuilder logic to repeat spaces.
if (mechanism.matches("wrap_brackets") && mechanism.requireInteger()) {
// ...
int amount = mechanism.getValue().asInt();
StringBuilder spaces = new StringBuilder(amount);
for (int i = 0; i < amount; i++) {
spaces.append(" ");
}
}
It's time we talk about NMS (short for net.minecraft.server, the core Minecraft server package). "NMS" code in Denizen is generally used any time the Spigot API does not support a feature, or code must be written differently depending on the Minecraft version. In the case of item display names, the Spigot API has a method for display names that improperly handles advanced text features (for example, alternate fonts) and as such a special NMS assisted implementation is used instead in Denizen.
Denizen provides NMS tooling through the class NMSHandler and its attached sub-classes. The specific class we want to access is ItemHelper, which can be accessed via NMSHandler.getItemHelper(). We can then use getDisplayName(ItemTag) and setDisplayName(ItemTag, String) to achieve what we want.
Going back to the item mechanism - it's a simple call to those two methods, and some string concatenation. Here's our final implementation:
if (mechanism.matches("wrap_brackets") && mechanism.requireInteger()) {
if (getItemMeta() == null || !getItemMeta().hasDisplayName()) {
mechanism.echoError("This item doesn't have a display name!");
return;
}
int amount = mechanism.getValue().asInt();
StringBuilder spaces = new StringBuilder(amount);
for (int i = 0; i < amount; i++) {
spaces.append(" ");
}
String newName = "[" + spaces + NMSHandler.getItemHelper().getDisplayName(item) + spaces + "]";
NMSHandler.getItemHelper().setDisplayName(this, newName);
}
Remember that you'll need to use the inventory command to adjust an item in your inventory. The item will also need to have its display property set beforehand (that's what the error check is for).
Mechanisms also have meta entries! Try to fill out one on your own based on this template:
// <--[mechanism]
// @object ObjectTag
// @name mech_name
// @input ObjectTag
// @description
// This is the description of the mechanism.
// @tags
// <ObjectTag.tag_name>
// -->
Properties?
Many objects in Denizen can be described by some core type, combined with a set of specific details about that object. Each specific detail that is necessary to accurately define the identity of an object is called a Property.
For example, MaterialTag, when used to identify block types, has a core type - the Material enum value - and various specific datapoints about the block data. For example, MaterialTag.half is the "half" value of a material like a bed - the head half, or the foot half. The head-half of a bed is a different precise block-type than the foot-half of a bed. This shows up in Denizen like red_bed[half=head]. When this material is read by Denizen, it creates an instance of MaterialTag with the material type red_bed, and the adjusts the half mechanism with a value of head to produce the final valid object. When identify() is called on the instance, it reads through all properties and includes them in the output.
Each Property in Denizen is defined in its own class that implements the Property interface. A valid property must necessarily have a name, a mechanism, and a value getter that corresponds to the mechanism. Properties should almost always have one or more tags to read the relevant data directly as well.
Tags are registered in the static registerTags method, and mechanisms are applied in the adjust method. You might recognize that this is exactly how it works in the tag type classes!
Here are the methods a property needs to implement:
getPropertyString- the current value of the property as a String, formatted to be input directly back into the mechanism. For simple properties such as boolean values, this can return"true"or"false". However, with more complex tag outputs, you can use the relevant object type'sidentify()method. This can returnnullin cases where a property's value is a default or 'unset' state. It can also returnnullfor extension properties (further explanation below).getPropertyId- the property's name. This must be the same as the mechanism name. For example,halfinMaterialTag.half.describes- this is a static class called through reflection by the property engine and bygetFrom. It takes an ObjectTag and determines if it can be used for the property. This must both check the tag type and validate that the object's specific sub-type can be described by the property. For example,leaf_sizeis only relevant tobambooblocks, soMaterialLeafSizechecks if the material provided is bamboo.getFrom- this is a static class called through reflection by the property engine. It must return an instance of the property if valid to, ornullif not. This method's body in practice is almost always just copy/pasted from reference implementations.
Currently, when creating mechanisms, you need to add the mechanism name to the handledMechs array. There used to be the equivalent for tags, but that was deprecated in favor of the registry system. If your mechanism isn't recognized, chances are you probably forgot to put it in the array.
For a property to be recognized, it itself needs to be registered. The properties package holds all the packages for the different types of properties as well as the PropertyRegistry class. To register a property, use PropertyParser.registerProperty() and pass in the property class and the tag type it applies to. Make sure to place this call in alphabetical order with the other properties of the same type.
Take a look at the existing property classes for examples!
Extension Properties
An additional usage of the property system that comes up is as an extension to existing object types. Notably, the Spigot implementation of Denizen has a few extensions of core types such as BukkitElementProperties. Also, Depenizen heavily utilizes extension properties to add external-plugin-relevant features to Spigot object types like PlayerTag.
At a technical level, an extension property:
implements
Propertylike any normal property wouldusually does not have any checks in the
describesmethod other than object typealways returns null from
getPropertyStringhas a generic name (usually the class name) as the return from
getPropertyIddoes not necessarily have any mechanism(s)
exists in a separate project from the one that defines the relevant object
