package ru.clouddelight.cdvote; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.event.Event; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.plugin.EventExecutor; import org.bukkit.plugin.java.JavaPlugin; import java.io.File; import java.io.IOException; import java.util.Locale; /** * CDVote — награды за голосование в мониторинге Cloud Delight (и любом другом, что шлёт голоса через Votifier). * Голос принимает NuVotifier / azuvotifier, CDVote выдаёт награду: команды из config.yml, офлайн-игрокам — при входе, * /vote — ссылка на страницу голосования, напоминание в чат. Зависимость от Votifier — во время работы (через * рефлексию), поэтому один jar подходит и к NuVotifier, и к форкам, и к старому Votifier 1.x. */ public final class CDVote extends JavaPlugin implements Listener { private File pendingFile; private YamlConfiguration pending; private int reminderTask = -1; @Override public void onEnable() { saveDefaultConfig(); pendingFile = new File(getDataFolder(), "pending.yml"); pending = YamlConfiguration.loadConfiguration(pendingFile); getServer().getPluginManager().registerEvents(this, this); if (!hookVotifier()) { getLogger().warning("Не найден Votifier! Поставьте NuVotifier или azuvotifier — без него голоса не дойдут до сервера."); } startReminder(); } @Override public void onDisable() { savePending(); } /** Подписка на com.vexsoftware.votifier.model.VotifierEvent без зависимости при сборке. */ @SuppressWarnings("unchecked") private boolean hookVotifier() { try { Class cls = (Class) Class.forName("com.vexsoftware.votifier.model.VotifierEvent"); EventExecutor ex = (listener, event) -> { if (!cls.isInstance(event)) return; try { Object vote = event.getClass().getMethod("getVote").invoke(event); String user = String.valueOf(vote.getClass().getMethod("getUsername").invoke(vote)).trim(); String service = String.valueOf(vote.getClass().getMethod("getServiceName").invoke(vote)); Bukkit.getScheduler().runTask(this, () -> onVote(user, service)); } catch (ReflectiveOperationException e) { getLogger().warning("Не удалось прочитать голос: " + e); } }; getServer().getPluginManager().registerEvent(cls, this, EventPriority.NORMAL, ex, this); getLogger().info("Votifier найден — ждём голоса."); return true; } catch (ClassNotFoundException e) { return false; } } private void onVote(String user, String service) { if (user.isEmpty() || !user.matches("[A-Za-z0-9_]{2,16}")) { getLogger().warning("Голос с непонятным ником «" + user + "» от " + service + " — пропущен"); return; } if (user.equalsIgnoreCase(getConfig().getString("test-nick", "CloudDelightTest"))) { getLogger().info("Тестовый голос от " + service + " получен — связь работает, награда не выдаётся."); return; } getLogger().info("Голос: " + user + " (" + service + ")"); pending.set("voted." + user.toLowerCase(Locale.ROOT), System.currentTimeMillis()); // напоминание его сутки не трогает Player p = findOnline(user); if (p != null) { reward(p.getName(), service); } else if (getConfig().getBoolean("offline-rewards", true)) { String key = "queue." + user.toLowerCase(Locale.ROOT); pending.set(key, pending.getInt(key, 0) + 1); } savePending(); String bc = getConfig().getString("messages.broadcast", ""); if (!bc.isEmpty()) { Bukkit.broadcastMessage(color(bc.replace("%player%", user).replace("%service%", service))); } } private void reward(String name, String service) { for (String cmd : getConfig().getStringList("rewards")) { String c = cmd.replace("%player%", name).replace("%service%", service); if (c.startsWith("/")) c = c.substring(1); try { Bukkit.dispatchCommand(Bukkit.getConsoleSender(), c); } catch (Exception e) { getLogger().warning("Команда награды не выполнилась: " + c + " — " + e.getMessage()); } } Player p = Bukkit.getPlayerExact(name); String msg = getConfig().getString("messages.thanks", ""); if (p != null && !msg.isEmpty()) p.sendMessage(color(msg.replace("%player%", name).replace("%service%", service))); } @EventHandler public void onJoin(PlayerJoinEvent e) { String key = "queue." + e.getPlayer().getName().toLowerCase(Locale.ROOT); int n = pending.getInt(key, 0); if (n <= 0) return; pending.set(key, null); savePending(); String name = e.getPlayer().getName(); Bukkit.getScheduler().runTaskLater(this, () -> { for (int i = 0; i < n; i++) reward(name, "офлайн"); Player p = Bukkit.getPlayerExact(name); String msg = getConfig().getString("messages.offline", ""); if (p != null && !msg.isEmpty()) p.sendMessage(color(msg.replace("%count%", String.valueOf(n)))); }, 40L); } private Player findOnline(String user) { Player p = Bukkit.getPlayerExact(user); if (p != null) return p; for (Player o : Bukkit.getOnlinePlayers()) if (o.getName().equalsIgnoreCase(user)) return o; return null; } private void startReminder() { if (reminderTask != -1) Bukkit.getScheduler().cancelTask(reminderTask); reminderTask = -1; int min = getConfig().getInt("reminder.minutes", 60); String text = getConfig().getString("reminder.text", ""); if (!getConfig().getBoolean("reminder.enabled", true) || min <= 0 || text.isEmpty()) return; long t = min * 60L * 20L; reminderTask = Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> { long day = System.currentTimeMillis() - 24L * 3600L * 1000L; for (Player p : Bukkit.getOnlinePlayers()) { if (pending.getLong("voted." + p.getName().toLowerCase(Locale.ROOT), 0L) < day) sendLink(p, text); } }, t, t); } /** Кликабельная ссылка через tellraw — работает на 1.8–1.21 без библиотек чата. */ private void sendLink(Player p, String text) { String url = getConfig().getString("vote-url", ""); String t = ChatColor.translateAlternateColorCodes('&', text); if (url.isEmpty() || url.contains("ВАШ-СЕРВЕР")) { p.sendMessage(t); return; } String json = "{\"text\":\"" + esc(t) + "\",\"clickEvent\":{\"action\":\"open_url\",\"value\":\"" + esc(url) + "\"},\"hoverEvent\":{\"action\":\"show_text\",\"value\":\"" + esc(ChatColor.YELLOW + url) + "\"}}"; Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "tellraw " + p.getName() + " " + json); } @Override public boolean onCommand(CommandSender s, Command cmd, String label, String[] a) { if (cmd.getName().equalsIgnoreCase("vote")) { if (s instanceof Player) { sendLink((Player) s, getConfig().getString("messages.vote", "&eНажми, чтобы проголосовать")); } else { s.sendMessage("Ссылка на голосование: " + getConfig().getString("vote-url", "")); } return true; } if (!s.hasPermission("cdvote.admin")) { s.sendMessage(ChatColor.RED + "Недостаточно прав"); return true; } if (a.length >= 1 && a[0].equalsIgnoreCase("reload")) { reloadConfig(); startReminder(); s.sendMessage(ChatColor.GREEN + "CDVote: конфиг перезагружен"); return true; } if (a.length >= 2 && a[0].equalsIgnoreCase("test")) { onVote(a[1], "тест " + s.getName()); s.sendMessage(ChatColor.GREEN + "CDVote: тестовый голос за " + a[1] + " — награда выдана (или ждёт входа игрока)"); return true; } s.sendMessage(ChatColor.YELLOW + "/cdvote reload" + ChatColor.GRAY + " — перечитать конфиг"); s.sendMessage(ChatColor.YELLOW + "/cdvote test <ник>" + ChatColor.GRAY + " — выдать награду как за голос (проверка наград)"); return true; } private void savePending() { try { pending.save(pendingFile); } catch (IOException e) { getLogger().warning("Не сохранился pending.yml: " + e.getMessage()); } } private static String color(String s) { return ChatColor.translateAlternateColorCodes('&', s); } private static String esc(String s) { return s.replace("\\", "\\\\").replace("\"", "\\\""); } }