Scammander - Simple commands in Scala

Scammander

A simle, typesafe command library for Minecraft

Scammander is a command library for Scala that focues on typesafe and simple commands for Minecraft. A command is just a name with a bunch of arguments. The arguments are then parsed, validated, and then used in the command. Often that process is tedious and boring, and takes away from the real meat of what a command is really doing. Scammander aims to remove that problem by reduce boilerplate and keeping things nice and easy.

Big thanks to the Sponge library for some of the ideas, designs and code that are used in Scammander. If you know how to use one, using the other will hopefully not be too hard.

Basic Usage

While Scammander is still early in development, you can try Scammander by adding one of these to your build.sbt.

//Core library. Use this if you want to create an implmentation for a new platform
libraryDependencies += "net.katsstuff" %% "scammander"         % "0.1.0"
libraryDependencies += "net.katsstuff" %% "scammander-sponge7" % "0.1.0" //Sponge API 7 platform
libraryDependencies += "net.katsstuff" %% "scammander-bukkit"  % "0.1.0" //Bukkit platform

So, how do you use Scammander. Let’s go over how to use it with Sponge. Usage for the other platforms should be somewhat similar. All examples here assumes.

import net.katsstuff.scammander.sponge._

Commands

Forms most cases you’ll be able to just use Command.simple and Command.withSender. If you do need more complex commands you can extend Command itself, but I won’t be covering that here.

First, let’s create the most basic echo command.

//Make sure to supply the type annotation for the argument(the string)
Command
  .simple { (sender, _, arg: String) =>
    sender.sendMessage(Text.of(s"ECHO: $string"))
    Command.success()
  }
  .register(myPlugin, Alias("echo"))

Simple enough, as you can see, in Scammander, the parameter type of a command lives in the type level. As long as you tell it what the type of your parameter is, Scammander takes care of the rest.

Let’s now look at a more complex command called spam. It takes a player, or if no player is given, the command source is used, a amount of times to send a message, and the remaining arguments as a string.

//We can use case classes and sealed trait hierarchies as parameters
case class MyParam(player: Player Or Source, count: Int, message: RemainingAsString)

//We use the withSender method if we want a custom sender. Make sure to supply
//type annotation for the sender too.
Command
  .simple{ (sender, _, param: MyParam) =>
    //We can use deconstructors to get all the values from the argument
    val MyParam(Or(player), count, RemainingAsString(message)) = param
    for (i <- 0 until count) playerr.sendMessage(Text.of(s"Spam $message"))
    CmdResult.success()
  }
  //Here we supply extra stuff when registering the command
  .register(
    myPlugin,
    Alias("spam"),
    Permission("foo.bar.baz"),
    Description(Text.of("Spam yourself to death")),
  )

As you can see, we can ust ADTs as parameters for our commands. A case class works like seq in the Sponge commands API, while sealed traits work like firstOf.

Finally, let’s look at how to refine the command source into a better type. Often times you only want to let certain types of command sources use a command. That’s where Command.withSender comes in. When using Command.withSender, you add a type annotation of what you need to the sender too. Let’s create a command named whereami which prints the location of a source. It can be used by all things which have a location.

Command
  .withSender{ (location: Location[World], _, _: Unit) =>
    sender.sendMessage(Text.of(s"Your coordinates are: x=${location.getX}, y=${location.getY}, z=${location.getZ}"))
    Command.success()
  }
  .register(myPlugin, Alias("whereami"))

Parameters

While parameters are derived for case classes and the like, you can also define your own by making sure there is an implicit of type Parameter[<MyType>] in scope. Additionally, if you’re making a parameter which has some name (think catalogetype’s id, a world’s name, a plugin’s id, and so on), and it’s representable in a collection, there are even easier ways to make a parameter. First you make a typeclass for that type which exposes this name called HasName. You can then call Parameter.mkNamed("<parameterName>", <by name argument to get all values of the given type>).

For example, the plugin parameter in Scammander is implemented like this

Parameter.mkNamed("plugin", Sponge.getPluginManager.getPlugins.asScala)

More complex uses

So, having gone over the basics, let’s looks at some complex uses.

UserValidator

If you want to define your own conversions for a command source, you can create provide a UserValidator typeclass for that type.

Extending universe traits

Scammander is designed to not be tied down to one specific platform. You can use it with Sponge, Bukkit, Forge, or whatever other custom stuff you have. To do so, just extend ScammanderUniverse, supply it the root sender types, in addition to classes which holds info for commands when their used (if there is any), and the command return type, and go from there. You can also extend one of the existing universe traits if you want to add more implicits without cluttering up your imports.

WIP

Scammander is still heavily a work in progress project. As such there are many things that are either not working currently, or working poorly. Under you can see some of those, and plans for them.

  • [x] Sponge implementation
  • [x] Bukkit implementation
  • [ ] Bukkit implementation with internal/NMS
  • [ ] Forge implementation
  • [ ] Child commands
  • [x] Flags
  • [ ] Better error messages for non-Sponge
  • [x] A way to handle X or else source
  • [ ] Even less boilerplate
  • [ ] More stuff I can’t remember now

Why another library?

Creating a library for Bukkit or Forge might be obvious, as doing commands there is already tedious, and while this library does support those platforms, it was initially made for Sponge in mind. So, the question is then why? There are two main answers to that question.

The first one is about the shortcomings of the Sponge API. Don’t get me wrong, I think the command API for Sponge is wonderful, but it also has some shortcomings that are hard to fix in Java without doing lots of mucky reflection. Many of the same shortcomings that can be easily fixed using typeclasses when working in Scala, and that’s mostly what Scammander is about.

While a simple command in both the Sponge API and Scammander are laughably simple, the amount of complexity needed increases much quicker as you add more parameters with Sponge. If you want to group your stuff in a class in Sponge, you have to write the class for it yourself, while in Scammander it’s derived for you.

Too be honest, there might be stuff that the Sponge API can do that Scammander can’t do, at least not as easily. That wasn’t the goal of Scammander however. The goal was to create something which felt simple to use, both for small and large commands, and in that regard, I think I’ve at least come a good way.

The other reason? I wanted an awesome command API like the one in Sponge for other platforms.

2 Likes

You realize that the name of this plugin says more “scam salamander” than “Scala commander”?

Otherwise, neat project.

1 Like