Skript Addon Best Practices

This tutorial was written to help explain Skript's poorly documented API and to promote quality standards for Skript addons.

This tutorial is a work-in-progress and will be updated frequently. Feedback is appreciated.

Limit the scope of your addon
Addons should solve a single problem. Unrelated features should be kept in separate addons. Most Minecraft features (features that don't require NMS) should be added to Skript itself, not into a new addon.

Splitting features into multiple addons has quite a few benefits:
  • It's easier for users to remember which addon adds which features.
  • It's easier for developers to contribute to your addon when it's designed to solve a specific issue.
  • Users download less unnecessary content if they don't use your entire addon.
  • Addons are more modular and less likely to conflict.

As a rule of thumb, if the best way to describe your addon is with your name or with some random word, you should consider splitting features into different addons.

Register your addon
[QUOTE=Skript's documentation]This is currently not required for addons to work, but the returned SkriptAddon provides useful methods for registering syntax elements and adding new strings to Skript's localization system (e.g. the required "types.[type]" strings for registered classes).[/QUOTE]
Registering your addon is a prerequisite for many of the practices described later in this tutorial.

Keep a reference to your addon's main class in the constructor.
public class MyMainClass extends JavaPlugin {
private static MyMainClass instance;

public MyMainClass() {
if (instance == null) {
instance = this;
} else {
throw new IllegalStateException();
}
}

public static MyMainClass getInstance() {
if (instance == null) {
throw new IllegalStateException();
}
return instance;
}
}

This is a common pattern used by many Bukkit plugins, allowing you to retrieve a plugin instance from anywhere by calling MyMainClass.getInstance().

Create a field for a SkriptAddon and write a getter that registers your addon when it is called the first time.
public class MyMainClass extends JavaPlugin {
private static MyMainClass instance;
private static SkriptAddon addonInstance;

public MyMainClass() {
if (instance == null) {
instance = this;
} else {
throw new IllegalStateException();
}
}

public static SkriptAddon getAddonInstance() {
if (addonInstance == null) {
addonInstance = Skript.registerAddon(getInstance());
}
return addonInstance;
}

public static MyMainClass getInstance() {
if (instance == null) {
throw new IllegalStateException();
}
return instance;
}
}

You can now call MyMainClass.getAddonInstance() to get your addon's SkriptAddon instance. Note that because MyMainClass.getAddonInstance() has not been called, the addon hasn't been registered yet. The next part of the tutorial will call this method, registering your addon.

Practical Example

Use Skript's class loader
Many addons register new syntax from within the addon's main class or from a utility class. While this approach works, using Skript's class loading utility allows you to keep your registration logic inside the same class as the expression/effect.

In order to start using Skript's class loader, you must call loadClasses() from your main class's onEnable. loadClasses() can throw an IOException, so you must catch wrap the statement in a try-catch block.
@Override
public void onEnable() {
try {
getAddonInstance().loadClasses("path.to.base.package", "subpackage");
} catch (IOException e) {
e.printStackTrace();
}
}

"path.to.base.package" and "subpackage" should refer to the package (folder) containing your registrable classes. For example, if the classes for your syntax are located in com.example.myaddon.skript, "path.to.base.package" should be replaced with "com.example.myaddon" and "subpackage" should be replaced with "skript".

If your classes are split into multiple packages, you can call loadClasses() with multiple subpackages. If your syntax is located in com.example.myaddon.effects and com.example.myaddon.expressions, you can call
getAddonInstance().loadClasses("com.example.myaddon", "effects", "expressions")

This does not need to be called for subclasses (e.g. com.example.myaddon.skript.subpackage is already registered if com.example.myaddon.skript is registered)

When Skript loads, all classes from registered packages will be initialized, executing any static initializers. Simply move your registration code to each class's static initalizer block.
public class MyEffect extends Effect {
static {
Skript.registerEffect(MyEffect.class, "syntax")
}
}


Practical Example

Write user friendly syntax
Your addon's syntax should be consistent with Skript's syntax. Remember, Skript's syntax was designed to be readable and English-like.

Don't prefix your syntax with the name of your addon
Conflicts should not be an issue if you limit the scope of your addon, as explained above.

Use PropertyExpression#register when possible
PropertyExpression#register registers the following expressions:
[the] <property> of %<fromType>%
%<fromType>%'[s] <property>

If the property doesn't contain any other expressions, you can extend SimplePropertyExpression to avoid most of the boilerplate.
public class MyProperty extends SimplePropertyExpression<Player, Entity> {
static {
// you can call this expression like "pet of {_player}" or "{_player}'s pet"
PropertyExpression.register(MyProperty.class, Entity.class, "pet", "players");
}

@Override
protected String getPropertyName() {
return "pet";
}

@Override
public Entity convert(Player player) {
// return the player's pet here
}

@Override
public Class<? extends Entity> getReturnType() {
return Entity.class;
}
}


Use PropertyCondition#register when possible
WIP, this is the same concept as using PropertyExpression

Do not perform expensive operations in expressions
Expensive operations, such as performing file I/O or contacting other servers, must not be performed in expressions. Instead, perform the action asynchronously in an effect and make the result available in a last <name of expression> expression.

The current trigger can be paused (allowing the server and other code to run) by overriding the instance method TriggerItem.walk(Event) and manually calling the static method TriggerItem.walk(TriggerItem, Event) when your action completes.

Practical Example