Skip to content
BillKress edited this page May 29, 2012 · 4 revisions

This script will be used to demonstrate the first few features:

@Cmd(value="spawn", description="Spawn a new wild dog near you", permission="dogs.spawn")
def spawnDog(Player sender) {
    player.world.spawnCreature(player.location, WOLF)
}

Easy to deploy and develop

Just save the above text into a file ending in ".groovy" and it will be available instantly to all clients. Modifying and re-saving the file will cause it to be reloaded instantly so that the new code can be tested.

Minimal java language constraints

No package, import or class definitions are needed for most cases, these can be true groovy "Scripts" and can therefore be very small--but the examples in the project may contain package/import statements just because it makes Eclipse happy allowing code completion. Even the imports like "EntityType.*" are added for you ("WOLF"). You will still need import statements for any packages that I didn't include (mostly packages from other plugins you wish to reference).

Also note that as a basic feature of Groovy, many "set/get" calls can be treated as properties. This isn't too big a deal but sometimes it can change `getPlayer().getLocation().getDistance(getOtherPlayer().getLocation())' to 'player.location.getDistance(otherPlayer.location)' which starts to look more manageable. You may also want to look into groovy's list and hash syntax.

Trivial to add new commands

Notice the @Cmd, just by having that line, this will now respond to the "spawn" command. This can be entered as /spawn by a player. Note: Console commands are a little problematic right now, I can't figure out how to tell Bukkit that I'm handling the command and it should suppress the error message. If this bothers you, use "ezp spawn" instead of "spawn" when you are at the console.

Smart command sender syntax

The parameter "Player sender" is actually quite intelligent and flexible. the "Type" passed in can be "Player", "Console", "CommandSender" or you can leave it off completely. If you ask for a "Player" and someone tries the command from the Console or the other way around, the user will get a "wrong location" error message. Specifying CommandSender or nothing at all will allow either location to work (and will send a CommandSender type object)

This eliminates that annoying code that is usually at the top of every command like this:

// NOT NEEDED
if(!sender instanceof Player) {
    sender.sendMessage("Only players can execute this command");
    return;
}
Player player=(Player) sender;

Built-in command index

The description in the annotation above will be displayed if the user types "/ezp" alone. When i figure out how to add commands on the fly they will be added to the normal "/help" display instead of "/ezp"

Easy permission support

the dogs.spawn code is actually the permission node required to execute this command, if your player doesn't have it, he can't execute the command.

Events automatically registered for you

Supplying the following code will mean that this method is called for all "PlayerInteractionEvents"

@EventHandler
public void playerEatsSugar(PlayerInteractEvent event) {
    def player=event.player
    // In this case we are only interested in trying to right-click nothing (eat)
    if(event.action != RIGHT_CLICK_AIR)
        return

    // The "Eat" attempt is only valid for sugar.
    if(event.item?.type == SUGAR) {
        def amount=event.item.amount
    ...
}

Saves your variables for you if you ask...

@Field @Persist int value

The @Persist annotation will ensure that every time your script is started it has the save value as the last time it exited. (The @Field annotation is Groovy's way of saying that a variable in a script is at the "Class" level and should be available to all methods in the script)

The variables persisted will only be accessible from this script. This is keyed of the name of the script (whatever.groovy) so if you change "whatever" to another name, your variables will reset.

Complex objects (hashes) may be saved as well.

Allows you to access other scripts

Each script is a running instance--to make that instance available to other scripts, you can use @Inject like this:

file: Balance.groovy

@Inject @Field Account account
@Cmd("balance")
public void testCommand(CommandSender sender, String[] args) {
    int bal = account.modifyBalance(sender.name, 0)
    print sender.name + " has a current balance = " + bal
}

After creating all the script instances, EzPlugin goes through and sets values like "account" to the instance of the account script (which must be stored in a file named "Account.groovy". Here is the Account file (Which is just a stand-alone class/script, it doesn't interact directly with Bukkit because there is no @Cmd or @EventHandler annotation:

file:Account.groovy

@Field @Persist def HashMap<String, Integer> balance = new HashMap<String, Integer>()

public int modifyBalance(String user, int difference) {
    Integer balanceInteger=balance.get(user)
    int i=balanceInteger == null ? 0 : balanceInteger
    i+=difference
    balance.put(user, i)
    return i;
}

This is a good way to share variables, you cannot share "@Persisted" variables because each class gets their own, so here "Account" persists a hash and anything @Injecting it can then get at the values. Also remember you can make full classes if you like, these don't have to be scripts. Back-end stuff like "Account" might be clearer if written as a class.

Per User Persistence

So far this is the least comfortable syntax I've come up with for this project--I don't love it but there are some technical problems that I haven't been able to overcome keeping me from making it prettier.

So here is the example code:

@Cmd(
    value="testup",
    description="Test user persistence"
)
public void testUserPersistenceCommand(sender, args) {
    persistScope(sender.name) { p ->
        if( p.count == null )
            p.count = 0
        else
            p.count++

        sender.sendMessage "You executed this command " +p.count + " times"
        log sender.getName()+" executed this command " + p.count +" times"
    }
}

Okay, everything should look good up until the persistScope thing--it is a Closure which looks like a function that takes a code block as a parameter. This "Function' takes a username and returns a hash that belongs to the user ("p" in the above example, although you can put any name before the -> arrow).

At first that hash is empty so all the values will return "Null", this forces us to check with the if(p.count == null) to see if we need to initialize it or use it.

The only nice thing about this is when you hit the end of the closure, the values are saved for you, otherwise you might as well just @Persist a hash of your own (Which you are welcome to do!).

The trick here is that where @Persist variables are always the same value, variables stored in the "p" hash above are swapped out for each user. You can even nest these calls to persistScope with different users if you need to let user-data interact.

This data is per user AND per script however, so you won't be able to access another script's data, use @Inject for that!

onEnable and onDisable supported

Called even when a script is modified, the old one will be disabled then the new one enabled.

Local (magic) variables available to all scripts

plugin or getPlugin() will get EzPlugin which you need to pass to many Bukkit methods as your "Plugin". logger or getLogger() will return the logger object. "log" is a method that takes a string and logs at info level as in log "This is sent to the console and longfile".

FULL POWER

These "Scripts" can do absolutely anything that a java plugin can do and should not be much slower since Groovy scripts are compiled into class files just like Java.

Prototyping!

If you are a plugin coder and enjoy java, you might try using this for rapid prototyping, groovy understands most .java syntax directly so you can code it in groovy with Java syntax and then just give it to Java to run--but avoid my @Inject, @Persist and @Cmd annotations because they won't exist when you change the project to java. The @EventHandler annotation is fine because it IS Bukkit's annotation, I just hand each class over to Bukkit to scan for @EventHandlers.

TODO (Feel free to help!)

  • Create a .yaml file when first run
  • Place directory scan interval into the .yaml file
  • Allow changing of the scan interval through the /ezpz commandj
  • Fix commands so they don't need to be /ezp command
  • Fix scripts so that I get a chance to scan them before an @EventHandler is invoked to simplify user-based persistence.
  • Clean up logging
  • Test that an exception thrown inside persistScope causes the variables not to be saved but doesn't mess anything else up.
  • Event Permissions--I'd like to support permissions for events the same way that @Cmd permissions are set up, but I have the same problem as I do with user-based persistence--I can't seem to cut in before the groovy class is called.
  • Submit plugin to Bukkit for approval (I believe I have to convert it to Maven first)
  • Timed saves of persisted fields in case of crash
  • simplify timer events with closure Every (1,second, "optUnqStr") {do something} if unique string is included, persist closure so it will remember last time and invoke for missed periods. It = time stamp event would have taken place.

Scripts that should be put with examples

  • Play sound or tweet on login or chat