跳到正文

开发者快速开始:完整 Paper 集成项目

第三方只能依赖公开 API,不能编译依赖 sealattributes-coresealattributes-paper 或反射内部类。坐标:

text
cn.seal:sealattributes-api:1.0.9

API baseline 是 1.3。API JAR 是 compileOnly,不能放进服务器 plugins/,也不要 shade。

项目结构

text
class-boost/
├─ settings.gradle.kts
├─ build.gradle.kts
└─ src/main/
   ├─ java/example/classboost/ClassBoostPlugin.java
   └─ resources/plugin.yml

settings.gradle.kts

kotlin
rootProject.name = "class-boost"

build.gradle.kts

kotlin
plugins { java }

group = "example"
version = "1.0.0"

repositories {
    mavenCentral()
    maven("https://repo.papermc.io/repository/maven-public/")
    mavenLocal() // 或团队存放 SealAttributes API 的私有 Maven 仓库
}

dependencies {
    compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
    compileOnly("cn.seal:sealattributes-api:1.0.9")
}

java {
    toolchain.languageVersion.set(JavaLanguageVersion.of(21))
}

plugin.yml

yaml
name: ClassBoost
version: 1.0.0
main: example.classboost.ClassBoostPlugin
api-version: '1.21'
depend: [SealAttributes]
commands:
  classboost:
    description: 切换示例生命加成
    usage: /classboost

ClassBoostPlugin.java

java
package example.classboost;

import cn.sealplugins.attributes.api.ProviderBindingResult;
import cn.sealplugins.attributes.api.SealAttributesApi;
import cn.sealplugins.attributes.api.SealAttributesApiProvider;
import cn.sealplugins.attributes.api.definition.AttributeId;
import cn.sealplugins.attributes.api.source.ContributionOperation;
import cn.sealplugins.attributes.api.source.PublishResult;
import cn.sealplugins.attributes.api.source.SourceContribution;
import cn.sealplugins.attributes.api.source.SourceKey;
import cn.sealplugins.attributes.api.source.SourcePersistence;
import cn.sealplugins.attributes.api.source.SourceSnapshot;
import cn.sealplugins.attributes.api.subject.QueryResult;
import cn.sealplugins.attributes.api.subject.SubjectKey;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin;

public final class ClassBoostPlugin extends JavaPlugin {
    private static final SourceKey SOURCE =
        new SourceKey("classboost", "class/warrior");
    private final Map<UUID, Long> revisions = new ConcurrentHashMap<>();
    private SealAttributesApi api;

    @Override public void onEnable() {
        RegisteredServiceProvider<SealAttributesApiProvider> service =
            Bukkit.getServicesManager().getRegistration(SealAttributesApiProvider.class);
        if (service == null) throw new IllegalStateException("SealAttributes service unavailable");

        ProviderBindingResult binding =
            service.getProvider().bind("classboost", ClassBoostPlugin.class);
        if (!binding.bound()) {
            throw new IllegalStateException("SealAttributes bind failed: " + binding.getStatus());
        }
        api = binding.getApi().orElseThrow();
        getLogger().info("Bound SealAttributes API " + api.apiVersion()
            + ", ownerEpoch=" + api.ownerEpoch());
    }

    @Override public boolean onCommand(
        CommandSender sender, Command command, String label, String[] args
    ) {
        if (!(sender instanceof Player player)) return false;
        QueryResult<SubjectKey> current = api.queries().currentSubject(player.getUniqueId());
        if (!current.found()) {
            player.sendMessage("属性尚未 READY,请稍后重试。");
            return true;
        }

        SubjectKey subject = current.getValue().orElseThrow();
        long revision = revisions.merge(player.getUniqueId(), 1L, Long::sum);
        SourceSnapshot snapshot = new SourceSnapshot(
            SOURCE,
            revision,
            List.of(new SourceContribution(
                AttributeId.of("sealattributes", "max_health"),
                ContributionOperation.ADD,
                50.0
            )),
            SourcePersistence.RUNTIME,
            Optional.empty()
        );
        PublishResult result = api.sources().publish(subject, snapshot);
        player.sendMessage("发布结果:" + result.getStatus());
        return true;
    }
}

构建与安装:

powershell
.\gradlew.bat clean build

class-boost-1.0.0.jar 放入测试服 plugins/,API JAR 不放。登录执行 /classboost,预期返回 ACCEPTED 类成功状态,/sa sources <玩家> 出现 classboost:class/warrior,最大生命增加 50。

绑定状态与生命周期

没有全局 singleton。每个插件用自己的主类绑定唯一小写 provider。状态包括:BOUNDINVALID_NAMESPACEOWNER_NOT_FOUNDNAMESPACE_CONFLICTPLATFORM_NOT_READYCLOSED

owner epoch 是租约:调用插件停用后,旧 facade、publisher、订阅都会失效并返回 typed stale/closed 状态,绝不会自动附着到新实例。不要把 facade 放在静态跨重载单例中。

必需联动用 depend: [SealAttributes]。可选联动才用 softdepend,并在每次启用检查服务。平台可能在第三方 onEnable 后才完成运行态激活,因此来源发布前要处理 NOT_READY;定义、公式、计划、Gate 和 extension 则必须在启动注册窗口内注册并检查 RegistrationStatus

Kotlin 调用提示

Java Optional 和 getter 在 Kotlin 可直接使用:

kotlin
val binding = provider.bind("classboost", ClassBoostPlugin::class.java)
check(binding.bound()) { "bind failed: ${binding.status}" }
val api = binding.api.orElseThrow()

不要在服务端线程对 CompletionStage 调用 join();持久来源完成后用 thenAccept,需要回 Bukkit 时再由插件调度器切回合法上下文。

Minecraft 服务端插件使用与开发文档