Minecraft 服务端权限上下文系统及其应用

权限上下文系统(英文名称 Permission Context),是一个在服务端插件开发的部分领域,有着不可或缺的重要作用的系统,但一直以来未得到广大开发者甚至服务端维护者的重视。不过,权限上下文系统已经在很多地方得到了支持。

  • Sponge API 的权限系统自带权限上下文系统的支持。
  • LuckPerms API 自带权限上下文系统的支持,并支持你听说过的几乎所有插件服务端。

一方面,考虑到本人只会写 Sponge 插件,因此后续内容将基于 Sponge API。另一方面,考虑到今年已经是 9102 年的第二年,不太可能还有人不在自己的服务端中添加并使用 LuckPerms,因此,本文内容主要使用 LuckPerms 5.0 API 完成。读者可参照相关的 Wiki 页面完成项目对 LuckPerms 5.0 API 的依赖设置。

引言

很多开发者甚至服务器维护者可能都有为玩家在特定场合赋予权限的需求,比如说:

  • 当玩家在某个特定的领地时拥有 pvp 权限,而在其他地方没有。
  • 玩家在特定时间段有执行某些特定命令的权限,而在其他时间段没有。

我们把上面提到的“特定领地”、“特定时间段”,统称为玩家所处的上下文。那我们如何构造这样的上下文呢?

一个常见的思路是在玩家进入领地时为玩家赋予权限,并在玩家离开领地时取消权限。这样做也不是不可行,但是一个值得考虑的问题就是:我们需要确保玩家在进出领地时权限能够正常切换,甚至 1 tick 的差错都不能有。

然而,由于以下几个问题,这一目标实际上很难做到:

  1. 我们要求我们需要在监听相关事件方面滴水不漏,不能有差错。
  2. 目前权限设置大多需要后台文件操作或数据库操作,因而不可能没有延迟。
  3. 我们需要编辑插件配置才能让插件知道哪些权限需要这样,而这理应是权限插件的事。

我们不妨换个角度思考问题。我们既然不能准确把握切换上下文的时间点,那我们在检查权限的时候去动态获知上下文不就好了?众所周知,插件通常情况下使用 hasPermission 方法来检查权限,而该方法将会直接关联到权限插件执行进一步操作。因此,只要有权限插件的适当配合,我们就一定能够“稳、准、狠”地完成目标。

以上便是权限上下文系统的理论基础。

上下文对象

LuckPerms 规定,上下文对象由键值对构成,比如说:

  • world=world-nether 代表当前玩家正处于下界。
  • server=global 代表当前玩家正处于 global 服务器内(名称可由 LuckPerms 配置文件修改)。

而当我们输入以下命令时:

/lp group default permission set minecraft.command.tp world=world-nether

我们便指定了使用 /tp 命令的权限只会在 world=world-nether 存在时生效。由于该权限的设置是自带上下文的,因此我们便可以轻易地确保玩家只有在下界的时候才能使用 /tp 指令。

如果使用 LuckPerms API 添加权限节点,以下是我们的代码:

import net.luckperms.api.LuckPermsProvider;  
import net.luckperms.api.node.Node;

import java.util.Objects;

Objects.requireNonNull(LuckPermsProvider.get().getGroupManager().getGroup("default")).data()  
        .add(Node.builder("minecraft.command.tp").withContext("world", "world-nether").build());

自定义上下文

LuckPerms 提供了 serverworld 两个上下文对象,但作为插件开发者,我们更需要的是我们自己的上下文,比如说刚才提到的“特定领地”、“特定时间段”、等等。

LuckPerms 为我们提供了注册 ContextCalculator 的方式。ContextCalculator 接口声明了 calculate 方法,而该方法会在 hasPermission 调用时调用。因此,我们可以在该方法内部动态地收集上下文。

ContextCalculator 接口需要提供一个泛型参数:

  • 对于 Bukkit 它是 org.bukkit.entity.Player
  • 对于 Sponge 它是 org.spongepowered.api.service.permission.Subject
  • 对于 BungeeCord 它是 net.md_5.bungee.api.connection.ProxiedPlayer

下面的代码实现了玩家在距离出生点 40 格以内时自动添加 player-near-spawn=true 上下文(Sponge 的 Subject 需要一些操作才能转换到 Player,对于 Bukkit 和 BungeeCord 不需要):

import com.flowpowered.math.vector.Vector3d;  
import net.luckperms.api.LuckPermsProvider;  
import net.luckperms.api.context.ContextCalculator;  
import net.luckperms.api.context.ContextConsumer;  
import org.spongepowered.api.entity.living.player.Player;  
import org.spongepowered.api.service.permission.Subject;  
import org.spongepowered.api.util.annotation.NonnullByDefault;

import java.util.Optional;

LuckPermsProvider.get().getContextManager().registerCalculator(new SubjectContextCalculator());

public static class SubjectContextCalculator implements ContextCalculator<Subject>  
{
    @Override
    public void calculate(Subject subject, ContextConsumer accumulator)
    {
        Optional<?> sourceOptional = subject.getCommandSource();
        if (sourceOptional.isPresent() && sourceOptional.get() instanceof Player)
        {
            this.calculatePlayer(accumulator, (Player) sourceOptional.get());
        }
    }

    private void calculatePlayer(ContextConsumer accumulator, Player player)
    {
        Vector3d playerPosition = player.getPosition();
        Vector3d spawnPosition = player.getWorld().getProperties().getSpawnPosition().toDouble();
        if (playerPosition.distance(spawnPosition) <= 40)
        {
            accumulator.accept("player-near-spawn", "true");
        }
    }
}

LuckPerms 要求所有 ContextCalculator 的实现满足两点:

  • 执行效率:calculate 方法会被频繁调用,因此相应实现应尽可能快速。
  • 线程安全:由于 hasPermission 会被不同的线程调用,因此实现必须保证线程安全。