Skip to content

Multiplatform design

alexstaeding edited this page Feb 8, 2020 · 2 revisions

At the highest level of abstraction, we are going to separate our plugin into three sections:

  • API (api)
  • Platform-independent (common)
  • Platform-dependent (sponge, velocity, etc...)

In the context of Anvil, "platform" refers to plugin platforms such as Sponge or Velocity.

API

This section defines the set of "abilities" our plugin has. How these "abilities" are achieved (implemented) is not defined in this module (that happens in the other ones). All we want to do in this module is to define what our plugin should be capable of, but not how it is implemented.

For example, we may have the method in api:

CompleteableFuture<Boolean> addKillForUser(UUID userUUID);

We do not actually define how this method should increment kills for a user. Simply put, all we are defining at this point is that this method will increment kills but we do not care how. Later, when we use this method, we assume that it will fulfill its contract. That is to say, it will increment kills for a user and let us know if it was successful or not.

In essence, the API will contain a set of these methods which we then implement in either the platform-independent or platform-dependent module.

Platform-independent

This module contains all the code that does not directly use a platform's features. Our entire datastore implementation will be in this module because it does not "care" whether it runs on any specific platform (like sponge or velocity). A typical MongoDB implementation for the above method would look like

    @Override
    public CompletableFuture<Boolean> addKill(Query<Member<ObjectId>> query) {
        return update(query, inc("kills"));
    }

    @Override
    public CompletableFuture<Boolean> addKillForUser(UUID userUUID) {
        return addKill(asQuery(userUUID));
    }

    @Override
    public Query<Member<ObjectId>> asQuery(UUID userUUID) {
        return asQuery().field("userUUID").equal(userUUID);
    }

Let's break down those three methods. While only the middle one is shown in the code block in API above, the other two are also important and so are included in this example. You can assume that all three are defined in the API.

Firstly, when another part of our project wants to increment the kills for a user, the only information they have is the UUID of the user. So they must use addKillForUser. This method then constructs a Query based on the provided UUID. This Query is then passed on to addKill which then actually increments the field kills by one.

  • addKill is used when you have a Query
  • addKillForUser is used when you know the UUID of the user.
  • asQuery is used when you want to turn a UUID into a Query

Platform-dependent

Some things can only be done directly in Platform-dependent code. One such thing is kicking players off the server. Let's say we have an interface KickService that looks like this:

public interface KickService {

    void kick(UUID userUUID, Object reason);

    void kick(String userName, Object reason);
}

If we were to deploy on Sponge, the implementation would look like this:

public class SpongeKickService implements KickService {

    @Inject
    private UserService<User, Player> userService;

    @Override
    public void kick(UUID userUUID, Object reason) {
        userService.getPlayer(userUUID).ifPresent(player -> player.kick(Text.of(reason)));
    }

    @Override
    public void kick(String userName, Object reason) {
        userService.getPlayer(userName).ifPresent(player -> player.kick(Text.of(reason)));
    }
}

Whereas the Velocity implementation would look like this:

public class VelocityKickService implements KickService {

    @Inject
    private ProxyServer proxyServer;

    @Override
    public void kick(UUID userUUID, Object reason) {
        proxyServer.getPlayer(userUUID).ifPresent(p -> p.disconnect(getReason(reason)));
    }

    @Override
    public void kick(String userName, Object reason) {
        proxyServer.getPlayer(userName).ifPresent(p -> p.disconnect(getReason(reason)));
    }
}

As you can see, Sponge and Velocity have different ways of kicking players. And as stated earlier, we don't actually care how the player is kicked, just that the player is kicked. This abstraction lets call the kick method in KickService in our platform-independent code and let the KickService worry about how to actually do it.

Separating code into these three modules is paramount to writing healthy code that can be easily maintained. Remember, when in doubt don't repeat yourself.

Please head on over to Hello world! to continue.