-
Notifications
You must be signed in to change notification settings - Fork 12
Multiplatform design
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.
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.
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 aQuery
-
addKillForUser
is used when you know theUUID
of the user. -
asQuery
is used when you want to turn aUUID
into aQuery
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.