Přeskočit na obsah

Vývoj vlastního pluginu

Nechceš oficiální plugin? Celé API je veřejné. Tenhle návod tě provede vlastní implementací pro aktivaci VIP — od smyčky synchronizace přes vláknovou bezpečnost až po nástrahy, které tě jinak čekají. Příklady jsou v Javě (Paper/Spigot), ale principy platí pro jakýkoli jazyk.

Klíčová myšlenka: tvůj plugin je jen spotřebitel fronty. Vyzvedne nákup, spustí příkazy, potvrdí. Veškerá logika peněz, kreditu a idempotence je na straně serveru — ty se staráš jen o spolehlivé spuštění a potvrzení.

1. Smyčka synchronizace

Základ je periodický cyklus: vyzvedni → zpracuj → potvrď. Musí běžet asynchronně, protože obsahuje síťové volání. Blokovat hlavní vlákno serverovým dotazem znamená zamrznout celý server pro všechny hráče.

java
// Spuštění async úlohy každých 30 sekund (600 ticků)
getServer().getScheduler().runTaskTimerAsynchronously(
    this, this::sync, 100L /* první běh za 5s */, 600L);

private final AtomicBoolean running = new AtomicBoolean(false);

private void sync() {
    // Když předchozí běh ještě neskončil (pomalá síť), přeskoč tento tik.
    // Jinak by se claim volání kupily na sebe.
    if (!running.compareAndSet(false, true)) return;
    try {
        List<Order> orders = api.claim(25);
        for (Order o : orders) deliver(o);
    } catch (InvalidTokenException e) {
        // Opakovat s vadným tokenem jen pálí požadavky. Zastav smyčku.
        getLogger().severe("Token odmítnut, zastavuji synchronizaci.");
        syncTask.cancel();
    } catch (Exception e) {
        // Síťový výpadek je normální. Nákupy zůstávají ve frontě na serveru.
        getLogger().warning("Sync selhal: " + e.getMessage());
    } finally {
        running.set(false);
    }
}

2. Vláknová bezpečnost (nejdůležitější část)

Tohle je místo, kde padá většina vlastních pluginů. Platí dvě neporušitelná pravidla Bukkitu:

text
• Síťová volání → ASYNC vlákno (jinak zamrzne server)
• Bukkit.dispatchCommand → HLAVNÍ vlákno (jinak poškodíš stav světa)

Takže tok je: async vyzvednutí → skok na hlavní vlákno kvůli příkazům → skok zpět na async kvůli potvrzení.

java
private void deliver(Order order) {
    // 1. BEZPEČNOST: ověř jméno hráče DŘÍV, než ho dosadíš do příkazu.
    if (!order.playerName().matches("^[A-Za-z0-9_]{3,16}$")) {
        api.ackAsync(order, false, "Neplatné jméno hráče");
        return;
    }

    CompletableFuture<Void> done = new CompletableFuture<>();

    // 2. Skok na HLAVNÍ vlákno kvůli příkazům
    getServer().getScheduler().runTask(this, () -> {
        try {
            for (String template : order.commands()) {
                String cmd = template
                    .replace("{player}", order.playerName())
                    .replace("{uuid}", order.playerUuid());
                Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd);
            }
            done.complete(null);
        } catch (Throwable t) {
            done.completeExceptionally(t);
        }
    });

    // 3. Potvrď AŽ PO spuštění, zpět na async vlákně
    try {
        done.get(60, TimeUnit.SECONDS);
        api.ackAsync(order, true, null);   // úspěch
    } catch (Exception e) {
        api.ackAsync(order, false, e.getMessage());  // selhání → retry
    }
}
Pořadí je zásadní: potvrzuj AŽ PO spuštění příkazů. Kdybys potvrdil první a server pak spadl před spuštěním, hráč zaplatil a nedostal nic — a systém o tom neví. Proto potvrzujeme až potom, i za cenu občasného dvojího spuštění.

3. Bezpečnost: injektáž do konzole

Jméno hráče se dosazuje do konzolových příkazů. Kdyby mělo mezeru nebo nový řádek, hráč by mohl přepsat nebo přidat příkaz. Proto nezávisle ověř jméno proti allowlistu ^[A-Za-z0-9_]{3,16}$ — i když ho validuje API, tvůj plugin je poslední obranná linie (operátor s přístupem do DB by API obešel).

java
private static final Pattern SAFE = Pattern.compile("^[A-Za-z0-9_]{3,16}$");

// V množině [A-Za-z0-9_] není znak, který by ukončil token, uvozoval
// řetězec nebo začal nový příkaz. Dosazení je tak prokazatelně bezpečné.
if (!SAFE.matcher(playerName).matches()) {
    // ODMÍTNI — nikdy nedosazuj nevalidované jméno do příkazu
}

4. Aktivace a expirace VIP

VIP „aktivuješ“ tím, že spustíš příkazy z nákupu — typicky přidání permission skupiny. Systém sám o sobě neposílá „deaktivační“ příkazy; expiraci si řeší tvůj plugin proti endpointu se seznamem aktivních VIP.

Doporučený postup aktivace

text
1. Nákup přijde přes claim → spusť "add" příkazy (lp user X parent add vip)
2. Ulož si do cache, do kdy VIP platí (order.expiresAt / GET /vips)
3. Periodicky (např. každých 5 min) načti GET /api/plugin/v1/vips
4. Porovnej s lokálním stavem:
     • hráč je v seznamu, ale nemá skupinu  → přidej ji (dohnání výpadku)
     • hráč NENÍ v seznamu, ale skupinu má  → odeber ji (expirace)
„Aktivní“ je vždy jen expiresAt > teď. Neukládej si žádný vlastní příznak „je VIP“ natrvalo — jediná pravda je datum expirace z API. Když ho respektuješ, plugin se sám opraví i po libovolně dlouhém výpadku.
java
// Deaktivace expirovaných: porovnej server (pravda) s realitou na serveru
private void reconcileVips() {
    Set<String> activeOnServer = api.fetchActiveVips().stream()
        .map(v -> v.player().toLowerCase()).collect(toSet());

    for (Player p : Bukkit.getOnlinePlayers()) {
        boolean hasGroup = perms.playerInGroup(p, "vip");
        boolean shouldHave = activeOnServer.contains(p.getName().toLowerCase());

        if (shouldHave && !hasGroup) {
            runOnMain(() -> dispatch("lp user " + p.getName() + " parent add vip"));
        } else if (!shouldHave && hasGroup) {
            runOnMain(() -> dispatch("lp user " + p.getName() + " parent remove vip"));
        }
    }
}

5. Ošetření chyb

text
401 INVALID_TOKEN   → zastav smyčku, upozorni operátora, neopakuj
429 RATE_LIMITED    → počkej podle hlavičky Retry-After
409 STALE_CLAIM     → lease vypršel, jinou instancí; zahoď nákup
5xx / timeout       → dočasné, zkus příště; nákup zůstává ve frontě
duplicitní příkaz   → očekávané (at-least-once); piš idempotentně

6. Kontrolní seznam

text
☐ Síťová volání běží async, příkazy na hlavním vlákně
☐ ACK až PO spuštění příkazů, s claimToken
☐ Jméno hráče validováno proti ^[A-Za-z0-9_]{3,16}$ i v pluginu
☐ Zástupné symboly {player} a {uuid} nahrazuješ až lokálně
☐ Duplicitní běh smyčky ošetřen (AtomicBoolean)
☐ 401 zastaví smyčku, 429 respektuje Retry-After
☐ Expirace řešena porovnáním s GET /vips, ne vlastním příznakem
☐ config.yml: api.url (https), api.token, interval
☐ Token se nikdy nedostane do logu ani commitu

Kompletní referenční implementaci najdeš v repozitáři ve složce plugin/ — soubory BlazeVipPlugin.java a ApiClient.java dělají přesně tohle a můžeš z nich vyjít.