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.
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.
// 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:
• 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í.
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
}
}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).
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
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)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.// 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
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
☐ 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 commituKompletní 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.