improve dev API docs a bit

Luck 2020-11-13 17:14:19 +00:00
parent eef2344ba6
commit f993361cd6
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
2 changed files with 105 additions and 75 deletions

@ -128,17 +128,17 @@ Now you've added the API classes to your project, and obtained an instance of th
#### Thread safety #### Thread safety
* All LuckPerms internals are thread-safe. You can safely interact with the API from scheduler tasks (or just generally from other threads) * All LuckPerms internals are thread-safe. You can safely interact with the API from async scheduler tasks (or just generally from other threads)
* This also extends to the permission querying methods in Bukkit/Bungee/Sponge. These can be safely called async when LuckPerms is being used as the permissions plugin. * This also extends to the permission querying methods in Bukkit/Bungee/Sponge. These can be safely called async when LuckPerms is being used as the permissions plugin.
#### Immutability #### Immutability
* In cases where methods return classes from the Java collections framework, assume that the returned methods are always immutable, unless indicated otherwise. (in the JavaDocs) * In cases where methods return classes from the Java collections framework, assume that the returned methods are always immutable, unless indicated otherwise. (in the JavaDocs)
* This means that you cannot make changes to any returned collections. * This means that you cannot make changes to any returned collections, and that the collections are only an accurate representation of the underlying data at the time of the method call.
#### Blocking operations #### Blocking operations
* Some methods are not "main thread friendly", meaning if they are called from the main Minecraft Server thread, the server will lag. * Some methods are not "main thread friendly", meaning that if they are called from the main Minecraft Server thread, the server will lag.
* This is because many methods conduct I/O with either the file system or the network. * This is because many methods conduct I/O with either the file system or the network.
* In most cases, these methods return [CompletableFutures](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html). * In most cases, these methods return [CompletableFutures](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html).
* Futures can be an initially complex paradigm for some users - however, it is crucial that you have at least a basic understanding of how they work before attempting to use them. * Futures can be an initially complex paradigm for some users - however, it is crucial that you have at least a basic understanding of how they work before attempting to use them.
@ -155,13 +155,13 @@ For the purposes of explaining, take the following method in the `ActionLogger`
CompletableFuture<ActionLog> getLog(); CompletableFuture<ActionLog> getLog();
``` ```
After calling the method, we get a `CompletableFuture<Log>` - the object we actually want is the `ActionLog`. The `CompletableFuture` represents the result of some computation (in this case the computation to obtain the ActionLog), and provides us with methods to obtain the `ActionLog` object. After calling the method, we get a `CompletableFuture<ActionLog>` - the object we actually want is the `ActionLog`. The `CompletableFuture` represents the result of some computation (in this case the computation to obtain the ActionLog), and provides us with methods to obtain the `ActionLog` object.
If the context of our method call is already asynchronous (if we're calling the method from an async scheduler task), then we can do-away with the future entirely. If the context of our method call is already asynchronous (if we're calling the method from an async scheduler task), then we can do-away with the future entirely.
```java ```java
/* /*
Calling this method effectively "requests" an ActionLog from the API. Calling this method "requests" an ActionLog from the API.
However, it's unlikely that the log will be available immediately... However, it's unlikely that the log will be available immediately...
We need to wait for it to be supplied. We need to wait for it to be supplied.
@ -181,15 +181,15 @@ CompletableFuture<ActionLog> logFuture = actionLogger.getLog();
ActionLog log = logFuture.join(); ActionLog log = logFuture.join();
``` ```
An alternative to using `#join` is to register a callback with the Future, to be executed once the `Log` is supplied. An alternative to using `#join` is to register a callback with the CompletableFuture, to be executed once the `Log` is supplied.
If we need to use the instance on the main server thread, then a special executor can be passed to the callback is executed on the server thread. If we need to use the instance on the main server thread, then a special executor can be passed to the callback is executed on the server thread.
```java ```java
// The executor to run the callback on // Create an executor that will run our callback on the server thread.
Executor executor = runnable -> Bukkit.getScheduler().runTask(this, runnable); Executor executor = runnable -> Bukkit.getScheduler().runTask(plugin, runnable);
// To be called once the Log is obtained. // Register a callback with the future.
logFuture.whenCompleteAsync(new BiConsumer<ActionLog, Throwable>() { // can be reduced to a lambda, I've left it as an anonymous class for clarity logFuture.whenCompleteAsync(new BiConsumer<ActionLog, Throwable>() { // can be reduced to a lambda, I've left it as an anonymous class for clarity
@Override @Override
public void accept(ActionLog log, Throwable exception) { public void accept(ActionLog log, Throwable exception) {

@ -1,6 +1,7 @@
This page shows some sample usages of the LuckPerms API, which is introduced [here](https://github.com/lucko/LuckPerms/wiki/Developer-API). This page shows some sample usages of the LuckPerms API, which is introduced [here](https://github.com/lucko/LuckPerms/wiki/Developer-API).
More samples can be found on the [api-cookbook](https://github.com/LuckPerms/api-cookbook). As well as this documentation, we also have the [api-cookbook](https://github.com/LuckPerms/api-cookbook). This is an example Bukkit plugin which uses the API to perform certain common functions.
___ ___
### Index ### Index
@ -74,8 +75,8 @@ In order to conserve memory usage, LuckPerms will only load User data when it ab
Meaning: Meaning:
* Online players are guaranteed to have an associated User object loaded already. * **Online** players are guaranteed to have an associated User object loaded already.
* Offline players *may* have an associated User object loaded, but they most likely will not. * **Offline** players *may* have an associated User object loaded, but they most likely will not.
This makes getting a User instance a little complicated, depending on if the Player is online or not. This makes getting a User instance a little complicated, depending on if the Player is online or not.
@ -88,28 +89,22 @@ If we know the player is connected, LuckPerms will already have data in memory f
It's as simple as... It's as simple as...
```java ```java
public User loadUser(Player player) { Player player = ...;
// assert that the player is online User user = luckPerms.getPlayerAdapter(Player.class).getUser(player);
if (!player.isOnline()) {
throw new IllegalStateException("Player is offline");
}
return luckPerms.getUserManager().getUser(player.getUniqueId());
}
``` ```
However, remember that this instance *may* not represent the user's most up-to-date state. If you want to make changes, it's a good idea to request for the user's data to be loaded again. Or if you only have a `UUID`...
```java
User user = luckPerms.getUserManager().getUser(uuid);
```
However, remember that this instance *may* not represent the user's most up-to-date state. If you want to make changes, it's a good idea to request for the user's data to be loaded again (read on...).
##### If the player isn't (or might not be) online ##### If the player isn't (or might not be) online
Let's assume we want to load some data about a user - but we only have their unique id. Let's assume we want to load some data about a user - but we only have their unique id.
For the purposes of explaining, assume we want to write an implementation for this method.
```java
public void giveAdminPermissions(UUID uniqueId) {...}
```
The first thing we need to do is obtain the `UserManager`. This object is responsible for handling all operations relating to `User`s. The user manager provides a method which lets us load a `User` instance, appropriately named `loadUser`. The first thing we need to do is obtain the `UserManager`. This object is responsible for handling all operations relating to `User`s. The user manager provides a method which lets us load a `User` instance, appropriately named `loadUser`.
The method returns a `CompletableFuture` (explained [here](https://github.com/lucko/LuckPerms/wiki/Developer-API#using-completablefutures)). The method returns a `CompletableFuture` (explained [here](https://github.com/lucko/LuckPerms/wiki/Developer-API#using-completablefutures)).
@ -117,18 +112,16 @@ The method returns a `CompletableFuture` (explained [here](https://github.com/lu
We can simply attach a callback onto the future to apply the action. We can simply attach a callback onto the future to apply the action.
```java ```java
public void giveAdminPermissions(UUID uniqueId) { UserManager userManager = luckPerms.getUserManager();
UserManager userManager = luckPerms.getUserManager(); CompletableFuture<User> userFuture = userManager.loadUser(uniqueId);
CompletableFuture<User> userFuture = userManager.loadUser(uniqueId);
userFuture.thenAcceptAsync(user -> { userFuture.thenAcceptAsync(user -> {
// TODO: apply the action to the User instance // Now we have a user which we can query.
user.someMethod(...); // ...
}); });
}
``` ```
##### How to query information for a (potentially) offline player ##### If the player isn't (or might not be) online & we want to return something
The callback approach works well if you don't need to actually "return" anything. It performs all of the nasty i/o away from the main server thread, and handles everything in the background. The callback approach works well if you don't need to actually "return" anything. It performs all of the nasty i/o away from the main server thread, and handles everything in the background.
@ -157,7 +150,9 @@ In an ideal world, we'd be able to do something like this, without any consequen
```java ```java
public boolean isAdmin(UUID who) { public boolean isAdmin(UUID who) {
User user = luckPerms.getUserManager().loadUser(who); User user = luckPerms.getUserManager().loadUser(who);
return user.inheritsGroup("admin"); // not a real method, just used here as an example :p
Collection<Group> inheritedGroups = user.getInheritedGroups(user.getQueryOptions());
return inheritedGroups.stream().anyMatch(g -> g.getName().equals("admin"));
} }
public void informIfAdmin(CommandSender sender, UUID who) { public void informIfAdmin(CommandSender sender, UUID who) {
@ -169,14 +164,17 @@ public void informIfAdmin(CommandSender sender, UUID who) {
} }
``` ```
However, we can't, because `#loadUser` returns a Future - as it performs lots of expensive database queries to produce a result. However, we can't, because `#loadUser` returns a CompletableFuture - as it performs lots of expensive database queries to produce a result.
The solution? More futures! The solution? More futures!
```java ```java
public CompletableFuture<Boolean> isAdmin(UUID who) { public CompletableFuture<Boolean> isAdmin(UUID who) {
return luckPerms.getUserManager().loadUser(who) return luckPerms.getUserManager().loadUser(who)
.thenApplyAsync(user -> user.inheritsGroup("admin")); // again, inheritsGroup is not a real method, just used as an example .thenApplyAsync(user -> {
Collection<Group> inheritedGroups = user.getInheritedGroups(user.getQueryOptions());
return inheritedGroups.stream().anyMatch(g -> g.getName().equals("admin"));
});
} }
public void informIfAdmin(CommandSender sender, UUID who) { public void informIfAdmin(CommandSender sender, UUID who) {
@ -192,7 +190,7 @@ public void informIfAdmin(CommandSender sender, UUID who) {
To summarise, there are two ways to obtain a user. To summarise, there are two ways to obtain a user.
* Using `UserManager#getUser` * Using `UserManager#getUser` or `PlayerAdapter#getUser`
* Always returns a result for online players * Always returns a result for online players
* Is "main thread friendly" (can be called sync) * Is "main thread friendly" (can be called sync)
* Will sometimes (but usually not) return a result of offline players * Will sometimes (but usually not) return a result of offline players
@ -232,13 +230,26 @@ After making changes to a user/group/track, you have to save the changes back to
```java ```java
public void addPermission(User user, String permission) { public void addPermission(User user, String permission) {
// TODO add the permission // Add the permission
user.data().add(Node.builder(permission).build());
// Now we need to save changes. // Now we need to save changes.
luckPerms.getUserManager().saveUser(user); luckPerms.getUserManager().saveUser(user);
} }
``` ```
There is also a handy `modify*` method which handles loading and saving for you.
```java
public void addPermission(UUID userUuid, String permission) {
// Load, modify, then save
luckPerms.getUserManager().modifyUser(userUuid, user -> {
// Add the permission
user.data().add(Node.builder(permission).build());
});
}
```
The same methods also exist for groups and tracks. The same methods also exist for groups and tracks.
___ ___
@ -357,24 +368,30 @@ You can use the Stream API to easily filter the returned data to find what you n
```java ```java
Set<String> groups = user.getNodes().stream() Set<String> groups = user.getNodes().stream()
.filter(NodeType.INHERITANCE::matches) .filter(NodeType.INHERITANCE::matches)
.map(NodeType.INHERITANCE::cast) .map(NodeType.INHERITANCE::cast)
.map(InheritanceNode::getGroupName) .map(InheritanceNode::getGroupName)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
```
You can make this a bit simpler by passing the node type as a parameter!
```java
Set<String> groups = user.getNodes(NodeType.INHERITANCE).stream()
.map(InheritanceNode::getGroupName)
.collect(Collectors.toSet());
``` ```
Or even more complicated queries, like finding the max priority of a temporary prefix held on a specific server. Or even more complicated queries, like finding the max priority of a temporary prefix held on a specific server.
```java ```java
int maxWeight = user.getNodes().stream() int maxWeight = user.getNodes(NodeType.PREFIX).stream()
.filter(Node::hasExpiry) .filter(Node::hasExpiry)
.filter(NodeType.PREFIX::matches) .filter(n -> n.getContexts().getAnyValue(DefaultContextKeys.SERVER_KEY)
.map(NodeType.PREFIX::cast) .map(v -> v.equals("factions")).orElse(false))
.filter(n -> n.getContexts().getAnyValue(DefaultContextKeys.SERVER_KEY) .mapToInt(ChatMetaNode::getPriority)
.map(v -> v.equals("factions")).orElse(false)) .max()
.mapToInt(ChatMetaNode::getPriority) .orElse(0);
.max()
.orElse(0);
``` ```
If you need to do a more specific lookup or check, prefer using one of the other methods (described later) to avoid iterating over the whole collection of nodes. If you need to do a more specific lookup or check, prefer using one of the other methods (described later) to avoid iterating over the whole collection of nodes.
@ -461,10 +478,10 @@ ImmutableContextSet set3 = ImmutableContextSet.builder()
Map<String, String> map = new HashMap<>(); Map<String, String> map = new HashMap<>();
map.put("region", "something"); map.put("region", "something");
ImmutableContextSet.Builder builder2 = ImmutableContextSet.builder(); ImmutableContextSet.Builder builder = ImmutableContextSet.builder();
map.forEach(builder2::add); map.forEach(builder::add);
ImmutableContextSet set4 = builder2.build(); ImmutableContextSet set4 = builder.build();
``` ```
You can of course also create an `ImmutableContextSet` by first creating (or obtaining) a `MutableContextSet` and converting it. You can of course also create an `ImmutableContextSet` by first creating (or obtaining) a `MutableContextSet` and converting it.
@ -518,6 +535,7 @@ The subject type varies between platforms.
| BungeeCord | `net.md_5.bungee.api.connection.ProxiedPlayer` | | BungeeCord | `net.md_5.bungee.api.connection.ProxiedPlayer` |
| Sponge | `org.spongepowered.api.service.permission.Subject` | | Sponge | `org.spongepowered.api.service.permission.Subject` |
| Nukkit | `cn.nukkit.Player` | | Nukkit | `cn.nukkit.Player` |
| Velocity | `com.velocitypowered.api.proxy.Player` |
In order to provide your own context, you need to create and register a `ContextCalculator`. In order to provide your own context, you need to create and register a `ContextCalculator`.
@ -528,8 +546,8 @@ The `estimatePotentialContexts` method can be added, but is not necessary, to sh
public class CustomCalculator implements ContextCalculator<Player> { public class CustomCalculator implements ContextCalculator<Player> {
@Override @Override
public void calculate(Player t, ContextConsumer contextConsumer) { public void calculate(Player target, ContextConsumer contextConsumer) {
contextConsumer.accept("gamemode", t.getGameMode().name()); contextConsumer.accept("gamemode", target.getGameMode().name());
} }
@Override @Override
@ -547,7 +565,7 @@ public class CustomCalculator implements ContextCalculator<Player> {
Then register it using Then register it using
```java ```java
api.getContextManager().registerCalculator(new CustomCalculator()); luckPerms.getContextManager().registerCalculator(new CustomCalculator());
``` ```
#### Querying active contexts/query options #### Querying active contexts/query options
@ -559,22 +577,28 @@ If you already have an instance of the subject type, you can query directly usin
```java ```java
Player player = ...; Player player = ...;
ImmutableContextSet contextSet = api.getContextManager().getContext(player); ImmutableContextSet contextSet = luckPerms.getContextManager().getContext(player);
QueryOptions queryOptions = api.getContextManager().getQueryOptions(player); QueryOptions queryOptions = luckPerms.getContextManager().getQueryOptions(player);
``` ```
If you only have a `User`, you can still perform a lookup, however, a result will only be returned if the corresponding subject (player) is online. If you only have a `User`, you can still perform a lookup, however, a result will only be returned if the corresponding subject (player) is online.
```java ```java
Optional<ImmutableContextSet> contextSet = api.getContextManager().getContext(user); Optional<ImmutableContextSet> contextSet = luckPerms.getContextManager().getContext(user);
Optional<QueryOptions> queryOptions = api.getContextManager().getQueryOptions(user); Optional<QueryOptions> queryOptions = luckPerms.getContextManager().getQueryOptions(user);
``` ```
If you absolutely need to obtain an instance, you can fallback to the server's "static" context/query option. (these are formed using calculators which provide contexts/query options regardless of the passed subject.) If you absolutely need to obtain an instance, you can fallback to the server's "static" context/query option. (these are formed using calculators which provide contexts/query options regardless of the passed subject.)
```java ```java
ContextManager cm = api.getContextManager(); User user = ...;
// This is the easy way...
ImmutableContextSet contextSet = user.getQueryOptions().context();
QueryOptions queryOptions = user.getQueryOptions();
// But is equivalent to this...
ContextManager cm = luckPerms.getContextManager();
ImmutableContextSet contextSet = cm.getContext(user).orElse(cm.getStaticContext()); ImmutableContextSet contextSet = cm.getContext(user).orElse(cm.getStaticContext());
QueryOptions queryOptions = cm.getQueryOptions(user).orElse(cm.getStaticQueryOptions()); QueryOptions queryOptions = cm.getQueryOptions(user).orElse(cm.getStaticQueryOptions());
``` ```
@ -606,6 +630,12 @@ You need:
```java ```java
CachedPermissionData permissionData = user.getCachedData().getPermissionData(queryOptions); CachedPermissionData permissionData = user.getCachedData().getPermissionData(queryOptions);
CachedMetaData metaData = user.getCachedData().getMetaData(queryOptions); CachedMetaData metaData = user.getCachedData().getMetaData(queryOptions);
// If you want to just use the most appropriate current query options for the User..
// i.e. 'cm.getQueryOptions(user).orElse(cm.getStaticQueryOptions())'
// .. then you can skip the queryOpptions parameter.
CachedPermissionData permissionData = user.getCachedData().getPermissionData();
CachedMetaData metaData = user.getCachedData().getMetaData();
``` ```
#### Performing permission checks #### Performing permission checks
@ -622,27 +652,25 @@ We can put all of this together to create a method that can run a "normal" permi
```java ```java
public boolean hasPermission(User user, String permission) { public boolean hasPermission(User user, String permission) {
ContextManager contextManager = api.getContextManager(); return user.getCachedData().getPermissionData().checkPermission(permission).asBoolean();
ImmutableContextSet contextSet = contextManager.getContext(user).orElseGet(contextManager::getStaticContext);
CachedPermissionData permissionData = user.getCachedData().getPermissionData(QueryOptions.contextual(contextSet));
return permissionData.checkPermission(permission).asBoolean();
} }
``` ```
#### Retrieving prefixes/suffixes #### Retrieving prefixes/suffixes
```java ```java
String prefix = metaData.getPrefix(); String prefix = user.getCachedData().getMetaData().getPrefix();
String suffix = metaData.getSuffix(); String suffix = user.getCachedData().getMetaData().getSuffix();
``` ```
#### Retrieving meta data #### Retrieving meta data
```java ```java
String metaValue = metaData.getMetaValue("some-key"); String metaValue = user.getCachedData().getMetaData().getMetaValue("some-key");
``` ```
Of course these methods work with `Group`s too!
___ ___
### Listening to LuckPerms events ### Listening to LuckPerms events
@ -668,9 +696,10 @@ public class TestListener {
// get the LuckPerms event bus // get the LuckPerms event bus
EventBus eventBus = api.getEventBus(); EventBus eventBus = api.getEventBus();
// subscribe to an event using a lambda // subscribe to an event using an expression lambda
eventBus.subscribe(LogPublishEvent.class, e -> e.setCancelled(true)); eventBus.subscribe(LogPublishEvent.class, e -> e.setCancelled(true));
// subscribe to an event using a lambda
eventBus.subscribe(UserLoadEvent.class, e -> { eventBus.subscribe(UserLoadEvent.class, e -> {
System.out.println("User " + e.getUser().getUsername() + " was loaded!"); System.out.println("User " + e.getUser().getUsername() + " was loaded!");
// TODO: do something else... // TODO: do something else...
@ -696,3 +725,4 @@ public class TestListener {
``` ```
`EventBus#subscribe` returns an [`EventSubscription`](https://github.com/lucko/LuckPerms/blob/master/api/src/main/java/net/luckperms/api/event/EventSubscription.java) instance, which can be used to unregister the listener when your plugin disables. `EventBus#subscribe` returns an [`EventSubscription`](https://github.com/lucko/LuckPerms/blob/master/api/src/main/java/net/luckperms/api/event/EventSubscription.java) instance, which can be used to unregister the listener when your plugin disables.