package dev.marcospaulo.curiosidades; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.Bukkit; import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.scheduler.BukkitTask; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.UUID; public final class Curiosidades extends JavaPlugin implements Listener { private final Random random = new Random(); private final Map lastFeatured = new HashMap<>(); private final Deque recentFacts = new ArrayDeque<>(); private Settings settings; private NamespacedKey optOutKey; private BukkitTask timerTask; private int nextSessionId = 1; private ReactionSession active; @Override public void onEnable() { saveDefaultConfig(); settings = new Settings(this); optOutKey = new NamespacedKey(this, "opt_out"); CuriosidadeCommand command = new CuriosidadeCommand(this); if (getCommand("curiosidade") != null) { getCommand("curiosidade").setExecutor(command); getCommand("curiosidade").setTabCompleter(command); } getServer().getPluginManager().registerEvents(this, this); rescheduleTimer(); getLogger().info("Curiosidades ativo — modo " + settings.mode() + (settings.mode().firesOnTimer() ? " (intervalo de " + settings.intervalMinutes() + " min)" : "")); } @Override public void onDisable() { if (active != null) { active.hide(); } } Settings settings() { return settings; } /** Starts, stops or restarts the repeating announcement task to match the mode. */ void rescheduleTimer() { if (timerTask != null) { timerTask.cancel(); timerTask = null; } if (!settings.mode().firesOnTimer()) { return; } long ticks = settings.intervalMinutes() * 60L * 20L; timerTask = getServer().getScheduler().runTaskTimer(this, () -> announceRandom(null), ticks, ticks); } // --- announcing --------------------------------------------------------- /** * Announces one curiosity. * * @param subject who to talk about, or null to pick a random eligible player * @return false if there was nobody eligible or nothing notable to say */ boolean announceRandom(Player subject) { if (subject == null) { List candidates = new ArrayList<>(); for (Player player : Bukkit.getOnlinePlayers()) { if (isEligible(player)) { candidates.add(player); } } if (candidates.isEmpty()) { return false; } subject = candidates.get(random.nextInt(candidates.size())); } List facts = CuriosityFactory.facts(subject, settings); if (facts.isEmpty()) { return false; } // Prefer a fact that has not been announced recently; fall back to any. List fresh = new ArrayList<>(facts); fresh.removeIf(fact -> recentFacts.contains(plain(fact.text()))); Fact chosen = (fresh.isEmpty() ? facts : fresh).get(random.nextInt( (fresh.isEmpty() ? facts : fresh).size())); remember(chosen); lastFeatured.put(subject.getUniqueId(), System.currentTimeMillis()); broadcast(subject, chosen.text()); return true; } private void remember(Fact fact) { recentFacts.addLast(plain(fact.text())); while (recentFacts.size() > settings.noRepeat()) { recentFacts.removeFirst(); } } private static String plain(Component component) { return PlainTextComponentSerializer.plainText().serialize(component); } /** True if the player can currently be the subject of an announcement. */ boolean isEligible(Player player) { if (isOptedOut(player) || player.hasPermission("curiosidades.isento")) { return false; } Long last = lastFeatured.get(player.getUniqueId()); if (last == null) { return true; } long cooldownMillis = settings.cooldownMinutes() * 60_000L; return System.currentTimeMillis() - last >= cooldownMillis; } /** Builds the message for {@code subject} without broadcasting it. */ Component render(Player subject, Component fact) { return Component.text("[Curiosidade] ", NamedTextColor.GOLD, TextDecoration.BOLD) .append(Component.text("Sabia que o ", NamedTextColor.WHITE) .decoration(TextDecoration.BOLD, false)) .append(Component.text(subject.getName(), NamedTextColor.GREEN) .decoration(TextDecoration.BOLD, false)) .append(Component.text(" ", NamedTextColor.WHITE).decoration(TextDecoration.BOLD, false)) .append(fact.decoration(TextDecoration.BOLD, false)) .append(Component.text("?", NamedTextColor.WHITE).decoration(TextDecoration.BOLD, false)); } private void broadcast(Player subject, Component fact) { if (active != null) { active.hide(); active = null; } Bukkit.broadcast(render(subject, fact)); if (!settings.reactionsEnabled()) { return; } ReactionSession session = new ReactionSession(nextSessionId++, settings.reactions()); active = session; Bukkit.broadcast(buttons(session)); session.show(); getServer().getScheduler().runTaskLater(this, () -> { session.hide(); if (active == session) { active = null; } }, settings.reactionWindowSeconds() * 20L); } /** The clickable reaction row. Counts are frozen at send time; the boss bar is live. */ private Component buttons(ReactionSession session) { Component row = Component.text(" "); for (Map.Entry entry : settings.reactions().entrySet()) { row = row.append(Component.text(entry.getValue() + " ", NamedTextColor.YELLOW) .clickEvent(ClickEvent.runCommand( "/curiosidade reagir " + session.id() + " " + entry.getKey())) .hoverEvent(HoverEvent.showText( Component.text("Clique para reagir com " + entry.getValue(), NamedTextColor.GRAY)))); } return row; } ReactionSession activeSession() { return active; } // --- events ------------------------------------------------------------- @EventHandler public void onJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); if (active != null) { active.showTo(player); } if (!settings.mode().firesOnJoin() || !isEligible(player)) { return; } // Delayed so the curiosity lands after the join message rather than racing it. getServer().getScheduler().runTaskLater(this, () -> { if (player.isOnline()) { announceRandom(player); } }, settings.joinDelaySeconds() * 20L); } // --- per-player opt out ------------------------------------------------- boolean isOptedOut(Player player) { Byte value = player.getPersistentDataContainer().get(optOutKey, PersistentDataType.BYTE); return value != null && value == 1; } void setOptedOut(Player player, boolean optedOut) { player.getPersistentDataContainer() .set(optOutKey, PersistentDataType.BYTE, (byte) (optedOut ? 1 : 0)); } void clearCooldowns() { lastFeatured.clear(); } void clearHistory() { recentFacts.clear(); } }