ServerAPIv1.1.0

第三方站點串接Third-party stations

別的插件可以把自己的資料,接到 ServerAPI 上一起對外,網址是 /api/v1/custom/<名稱>。但要不要開、要不要金鑰,都是你(伺服器管理員)說了算。 插件只能「申請」,沒辦法自己把資料公開出去。

裝好預設是關的。插件第一次申請時,ServerAPI 只會在 plugins/ServerAPI/custom/ 底下放一個關著的設定檔,並在後台提醒你。 你看過、把 enabled 改成 true 再重載,資料才會真的出現在 API 上。

給寫插件的人

ServerAPI 一啟用,就會把註冊入口掛到 Bukkit 的 ServicesManager。 在你插件的 onEnable() 裡拿到它,最少只要兩樣東西:站點名稱,和一個回傳資料的函式。

var rsp = getServer().getServicesManager().getRegistration(ServerApiRegistry.class);
if (rsp == null) return;                 // 沒裝 ServerAPI,就當作沒這回事
ServerApiRegistry api = rsp.getProvider();

api.register(CustomStation.builder("myplugin_stats", this)
        .supplier(() -> Map.of("kills", killCount, "wins", winCount))
        .build());

這樣就好。等伺服器管理員開啟後,資料就會出現在 /api/v1/custom/myplugin_stats, 你回傳的 Map 會自動變成 JSON。

要放玩家或世界的資料?

上面那種寫法,你的函式是在處理 HTTP 請求的執行緒上跑的,這條執行緒不能碰 Bukkit 的東西, 因為玩家、世界、實體這些只有主執行緒讀才安全。如果你要放的正是這類資料,加一行 .cached(秒數):ServerAPI 會改成在主執行緒上、每隔幾秒幫你抓一次存起來,之後有人來讀就給快取。

api.register(CustomStation.builder("arena", this)
        .cached(5)                       // 每 5 秒在主執行緒抓一次
        .supplier(() -> {
            World w = Bukkit.getWorld("arena");
            return Map.of("players", w.getPlayers().size(), "time", w.getTime());
        })
        .build());

簡單說,資料本來就在記憶體、算得快,什麼都不用加,預設就是即時; 要讀玩家、世界、實體,才加 .cached(秒)。那個秒數只是建議值,伺服器管理員能自己改。

完整範例

一支只做這件事的小插件,大概長這樣:

public class MyPlugin extends JavaPlugin {

    private int kills = 0;
    private final UUID matchId = UUID.randomUUID();

    @Override
    public void onEnable() {
        var rsp = getServer().getServicesManager().getRegistration(ServerApiRegistry.class);
        if (rsp == null) {
            getLogger().info("沒裝 ServerAPI,略過資料串接。");
            return;
        }
        rsp.getProvider().register(CustomStation.builder("myplugin_stats", this)
                .description("我的插件統計")     // 選填,會顯示在 /api/v1 索引上
                .supplier(this::snapshot)
                .build());
    }

    // 各種型別都能直接回傳,ServerAPI 會遞迴轉成 JSON。
    // 要放 null 得用 LinkedHashMap,因為 Map.of 不接受 null。
    private Map<String, Object> snapshot() {
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("kills", kills);                             // 數字(整數)
        data.put("kdr", 2.4);                                 // 數字(小數)
        data.put("ranked", true);                             // 布林
        data.put("topPlayer", "Alice");                       // 字串
        data.put("recentPlayers", List.of("Alice", "Bob"));   // 陣列(List)
        data.put("lastScores", new int[]{10, 8, 5});          // 陣列(int[] 也行)
        data.put("arena", Map.of("name", "sky", "round", 3)); // 巢狀物件(Map)
        data.put("matchId", matchId);                         // 其他型別(UUID)自動轉字串
        data.put("nextEvent", null);                          // null
        return data;
    }

    @Override
    public void onDisable() {
        var rsp = getServer().getServicesManager().getRegistration(ServerApiRegistry.class);
        if (rsp != null) rsp.getProvider().unregister("myplugin_stats");
    }
}

如上,回傳值可以是 MapList、陣列、數字、小數、布林、字串、 null,其他型別會自動轉成字串,全都是純 Java。 unregister 會停止對外,但伺服器管理員的設定檔會留著,下次開插件不必重設。

怎麼把 ServerAPI 加進你的專案、以及完整的 API 參考,見開發者 API。 更長的教學與常見問題見 串接文件

給伺服器管理員:你會看到什麼

插件一申請,後台就會提示,並產生一個設定檔(一個站點一個檔,檔名就是站點名):

# plugins/ServerAPI/custom/myplugin_stats.yml
# 有個插件想透過 ServerAPI 對外提供資料,所以自動建立了這個檔案。
# 預設是關的:看過內容覺得沒問題,就把 enabled 改成 true、再執行 /serverapi reload。
plugin: CoolPlugin
path: /api/v1/custom/myplugin_stats
mode: async
registered-at: 2026-07-23T10:00:00

enabled: false           # false=這個網址打不開(回 404);改成 true 才會真的對外
require-key: true        # true=要帶 API 金鑰才讀得到;false=誰都能讀
protected-fields: []     # 沒帶金鑰的人看不到這些欄位,例如 [balance];留空就是全公開

要開啟:enabled 改成 true,跑一次 /serverapi reload。 開了以後,這個站點會出現在 /api/v1 索引,並套用你原本的 三層存取控管(金鑰、隱藏欄位、未授權回 404),跟內建端點一模一樣。

如果這個站點是 cached 模式,設定檔會多一行 snapshot-seconds, 用來調它多久更新一次;async 模式即時讀取,沒有這一行。

最上面 pluginpathmoderegistered-at 只是資訊, 插件每次註冊都會蓋回,改了沒用;你真正要動的只有下面四行。

Other plugins can plug their own data into ServerAPI and have it served alongside the built-in endpoints, at /api/v1/custom/<name>. But whether it opens, and whether it needs a key, is entirely your call — a plugin can only ask; it cannot make its data public by itself.

It starts off. The first time a plugin asks, ServerAPI just drops a closed config file into plugins/ServerAPI/custom/ and tells you in the console. Only after you look it over, set enabled to true and reload does the data actually appear on the API.

For plugin developers

ServerAPI puts the registration entry point into Bukkit's ServicesManager when it enables. Grab it in your plugin's onEnable(). The minimum is two things: a station name and a function that returns the data.

var rsp = getServer().getServicesManager().getRegistration(ServerApiRegistry.class);
if (rsp == null) return;                 // No ServerAPI installed — just skip it
ServerApiRegistry api = rsp.getProvider();

api.register(CustomStation.builder("myplugin_stats", this)
        .supplier(() -> Map.of("kills", killCount, "wins", winCount))
        .build());

That is all. Once the owner enables it, the data shows up at /api/v1/custom/myplugin_stats, and the Map you return becomes JSON automatically.

Exposing player or world data?

With the code above, your function runs on the HTTP request thread, and that thread must not touch Bukkit — players, worlds and entities are only safe to read on the main thread. If that is exactly the kind of data you want to expose, add one line, .cached(seconds): ServerAPI then calls your function on the main thread every few seconds, stores the result, and serves that snapshot to readers.

api.register(CustomStation.builder("arena", this)
        .cached(5)                       // grab it on the main thread every 5s
        .supplier(() -> {
            World w = Bukkit.getWorld("arena");
            return Map.of("players", w.getPlayers().size(), "time", w.getTime());
        })
        .build());

In one line: if the data is already in memory and cheap to build, add nothing (live is the default); add .cached(seconds) only when you need player / world / entity data. That number is just a suggestion — the owner can change it.

A full example

A small plugin that does nothing but this looks about like:

public class MyPlugin extends JavaPlugin {

    private int kills = 0;
    private final UUID matchId = UUID.randomUUID();

    @Override
    public void onEnable() {
        var rsp = getServer().getServicesManager().getRegistration(ServerApiRegistry.class);
        if (rsp == null) {
            getLogger().info("No ServerAPI, skipping data integration.");
            return;
        }
        rsp.getProvider().register(CustomStation.builder("myplugin_stats", this)
                .description("My plugin stats")   // optional, shown in the /api/v1 index
                .supplier(this::snapshot)
                .build());
    }

    // Any of these types can be returned directly; ServerAPI converts to JSON recursively.
    // Use a LinkedHashMap if you need a null value, since Map.of rejects null.
    private Map<String, Object> snapshot() {
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("kills", kills);                             // number (int)
        data.put("kdr", 2.4);                                 // number (double)
        data.put("ranked", true);                             // boolean
        data.put("topPlayer", "Alice");                       // string
        data.put("recentPlayers", List.of("Alice", "Bob"));   // array (List)
        data.put("lastScores", new int[]{10, 8, 5});          // array (int[] works too)
        data.put("arena", Map.of("name", "sky", "round", 3)); // nested object (Map)
        data.put("matchId", matchId);                         // other types (UUID) become a string
        data.put("nextEvent", null);                          // null
        return data;
    }

    @Override
    public void onDisable() {
        var rsp = getServer().getServicesManager().getRegistration(ServerApiRegistry.class);
        if (rsp != null) rsp.getProvider().unregister("myplugin_stats");
    }
}

As above, the return value can be a Map, List, array, number, boolean, string or null, and any other type becomes a string. All plain Java. unregister stops serving but keeps the owner's config file, so nothing has to be reconfigured next time.

For how to add ServerAPI to your project and the full API reference, see Developer API. For a longer walkthrough and FAQ, see the integration guide.

For the owner: what you see

The moment a plugin asks, the console tells you and a config file appears — one file per station, named after the station:

# plugins/ServerAPI/custom/myplugin_stats.yml
# A plugin asked to expose its data through ServerAPI, so this file was created for it.
# It starts off: once you are happy with it, set enabled to true and run /serverapi reload.
plugin: CoolPlugin
path: /api/v1/custom/myplugin_stats
mode: async
registered-at: 2026-07-23T10:00:00

enabled: false           # false = the URL is closed (404); set true to actually serve it
require-key: true        # true = an API key is required; false = anyone can read it
protected-fields: []     # fields hidden from callers without a key, e.g. [balance]; [] shows all

To turn it on: set enabled to true and run /serverapi reload. Once enabled the station shows up in the /api/v1 index and obeys your existing three layers of access control (key, hidden fields, 404 for unauthorised requests), exactly like the built-in endpoints.

A cached station's file has one extra line, snapshot-seconds, controlling how often it refreshes; an async station reads live and has no such line.

The top plugin / path / mode / registered-at lines are just information and get overwritten on every registration; the only lines you change are the four below them.
ServerAPI · 僅供非商業用途,商業伺服器需另行取得授權。
ServerAPI · Free for non-commercial use; commercial servers need a separate licence.
Copyright © 2021-2026 CloudXact Studio. All Rights Reserved.