diff --git a/README.md b/README.md index 664713d..af07c90 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,36 @@ counting for `reacao-validade-minutos` (default 15) even after the boss bar is gone, and the last 8 reaction sets stay in memory for that reason. Silently dropping a late click looks like a bug to the player. -### 3. Names are translated by the client, not by us +### 3. Bedrock cannot click, and cannot show emoji + +Geyser cannot deliver a chat `clickEvent` to a Bedrock client, and most emoji +render as tofu boxes there. So every clickable interaction has a typed +equivalent, and every reaction carries two labels: + +```yaml +reacoes: + uau: + java: "[😮]" # Java clients + texto: "[UAU]" # Bedrock clients — ASCII only + comando: "wow" # what a Bedrock player types +``` + +Messages with buttons are therefore built twice and sent per player +(`broadcastPerPlatform`), not via `Bukkit.broadcast`. Typed fallbacks: +`/reagir `, `/legal`, `/wow`, `/top`, `/f`, `/palpite `, +`/votar ` — all of which act on the most recent message, so the player +never needs a message id. + +Changing a `comando` to a new name also requires adding that command to +`plugin.yml` and restarting; Bukkit commands are static. + +**Detecting Bedrock**: Floodgate mints UUIDs whose high 64 bits are zero +(`00000000-0000-0000-0009-…`), so `Platform.isBedrock` checks +`getUniqueId().getMostSignificantBits() == 0`. That keeps Floodgate an optional +runtime dependency rather than a compile-time one. `/canalhandia plataformas` +lists who is online and on which platform. + +### 4. Names are translated by the client, not by us Block, item and mob names are emitted as **translatable components** (`Component.translatable(material.translationKey())`), so a pt-BR client renders diff --git a/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java b/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java index 4d178cc..9ba2665 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java +++ b/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java @@ -62,11 +62,10 @@ public final class Canalhandia extends JavaPlugin implements Listener { optOutKey = new NamespacedKey(this, "opt_out"); CanalhandiaCommand root = new CanalhandiaCommand(this); - register("canalhandia", root); - register("curiosidade", root); - register("adivinha", root); - register("enquete", root); - register("ranking", root); + for (String name : List.of("canalhandia", "curiosidade", "adivinha", "enquete", "ranking", + "reagir", "palpite", "votar", "legal", "wow", "top", "f")) { + register(name, root); + } getServer().getPluginManager().registerEvents(this, this); rescheduleTimer(); @@ -224,6 +223,21 @@ public final class Canalhandia extends JavaPlugin implements Listener { // --- reactions ---------------------------------------------------------- + /** + * Sends a message that renders differently per platform. + * + *

Bedrock cannot show emoji or run a chat clickEvent, so anything with + * buttons has to be built twice. The console gets the Java rendering. + */ + void broadcastPerPlatform(java.util.function.Function builder) { + Component javaVersion = builder.apply(false); + Component bedrockVersion = builder.apply(true); + Bukkit.getConsoleSender().sendMessage(javaVersion); + for (Player player : Bukkit.getOnlinePlayers()) { + player.sendMessage(Platform.isBedrock(player) ? bedrockVersion : javaVersion); + } + } + /** Attaches a fresh reaction row to the message just broadcast. */ private void openReactions() { if (!settings.reactionsEnabled()) { @@ -233,7 +247,7 @@ public final class Canalhandia extends JavaPlugin implements Listener { liveReactions = reactions; remember(reactions); - Bukkit.broadcast(reactions.buttons("/canalhandia reagir")); + broadcastPerPlatform(reactions::buttons); reactions.show(); getServer().getScheduler().runTaskLater(this, () -> { @@ -243,11 +257,27 @@ public final class Canalhandia extends JavaPlugin implements Listener { } // Chat cannot be edited, so post the final count as its own line. if (reactions.hasAnyVote()) { - Bukkit.broadcast(Component.text(" ").append(reactions.tally())); + broadcastPerPlatform(bedrock -> + Component.text(" ").append(reactions.tally(bedrock))); } }, settings.reactionWindowSeconds() * 20L); } + /** + * The newest reaction set still accepting clicks, for typed shortcuts like + * {@code /wow} where the player never sees an id. + */ + Reactions latestReactions() { + long limit = settings.reactionValidityMinutes() * 60_000L; + Reactions best = null; + for (Reactions reactions : reactionHistory) { + if (reactions.ageMillis() <= limit) { + best = reactions; + } + } + return best; + } + private void remember(Reactions reactions) { reactionHistory.addLast(reactions); while (reactionHistory.size() > 8) { @@ -302,7 +332,7 @@ public final class Canalhandia extends JavaPlugin implements Listener { GuessRound round = new GuessRound(nextId++, chosen.subject(), chosen.fact().text(), candidates); guessRound = round; - Bukkit.broadcast(round.question("/canalhandia palpite")); + broadcastPerPlatform(bedrock -> round.question("/canalhandia palpite", bedrock)); getServer().getScheduler().runTaskLater(this, () -> { if (guessRound == round && !round.finished()) { @@ -330,7 +360,7 @@ public final class Canalhandia extends JavaPlugin implements Listener { } Poll started = new Poll(nextId++, question, options, author); poll = started; - Bukkit.broadcast(started.announcement("/canalhandia votar")); + broadcastPerPlatform(bedrock -> started.announcement("/canalhandia votar", bedrock)); started.show(); getServer().getScheduler().runTaskLater(this, () -> { @@ -370,18 +400,25 @@ public final class Canalhandia extends JavaPlugin implements Listener { if (!settings.moduleEnabled(Module.LUTO)) { return; } - Reactions mourning = new Reactions(nextId++, Map.of("f", "[F]")); + Reactions mourning = new Reactions(nextId++, + List.of(new ReactionDef("f", "[F]", "[F]", "f"))); liveReactions = mourning; remember(mourning); String name = event.getEntity().getName(); // One tick later so it prints under the vanilla death message. - getServer().getScheduler().runTaskLater(this, () -> Bukkit.broadcast( - Component.text(" ") - .append(Component.text("[F] ", NamedTextColor.YELLOW) - .clickEvent(ClickEvent.runCommand( - "/canalhandia reagir " + mourning.id() + " f"))) - .append(Component.text("prestar luto por " + name, NamedTextColor.GRAY))), 2L); + getServer().getScheduler().runTaskLater(this, () -> broadcastPerPlatform(bedrock -> { + Component button = Component.text("[F] ", NamedTextColor.YELLOW); + if (!bedrock) { + button = button.clickEvent(ClickEvent.runCommand( + "/canalhandia reagir " + mourning.id() + " f")); + } + return Component.text(" ").append(button) + .append(Component.text(bedrock + ? "digite /f para prestar luto por " + name + : "prestar luto por " + name, + NamedTextColor.GRAY)); + }), 2L); getServer().getScheduler().runTaskLater(this, () -> { if (mourning.hasAnyVote()) { diff --git a/src/main/java/dev/marcospaulo/canalhandia/CanalhandiaCommand.java b/src/main/java/dev/marcospaulo/canalhandia/CanalhandiaCommand.java index 783d58d..1a1433f 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/CanalhandiaCommand.java +++ b/src/main/java/dev/marcospaulo/canalhandia/CanalhandiaCommand.java @@ -40,7 +40,18 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter { case "adivinha" -> adivinha(sender); case "enquete" -> enquete(sender, args); case "ranking" -> ranking(sender, args); - default -> root(sender, args); + // Typed fallbacks. Bedrock cannot run a chat clickEvent, so every + // clickable interaction needs a command a player can type. + case "reagir" -> reactLatest(sender, args.length > 0 ? args[0] : null); + case "palpite" -> guessLatest(sender, args); + case "votar" -> voteLatest(sender, args); + default -> { + String reaction = plugin.settings().reactionForCommand(command.getName()); + if (reaction != null) { + yield reactLatest(sender, reaction); + } + yield root(sender, args); + } }; } @@ -60,6 +71,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter { // Admin. case "modulo" -> moduleToggle(sender, rest); case "modulos" -> modules(sender); + case "plataformas", "quem" -> platforms(sender); case "status" -> status(sender); case "marcos" -> { if (admin(sender)) { @@ -346,6 +358,93 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter { // --- reactions ---------------------------------------------------------- + /** + * Reacts to the most recent still-valid message, for players typing a + * shortcut like {@code /wow} who never see a message id. + */ + private boolean reactLatest(CommandSender sender, String key) { + if (!(sender instanceof Player player)) { + Msg.error(sender, "Só jogadores podem reagir."); + return true; + } + if (!player.hasPermission("canalhandia.reagir")) { + return denied(sender); + } + Reactions reactions = plugin.latestReactions(); + if (reactions == null) { + Msg.error(sender, "Nada para reagir agora."); + return true; + } + if (key == null) { + Msg.error(sender, "Uso: /reagir <" + String.join("|", reactionKeys()) + ">"); + return true; + } + if (!reactions.react(player, key)) { + Msg.error(sender, "Essa reação não vale para a última mensagem."); + } + return true; + } + + private boolean guessLatest(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + return true; + } + GuessRound round = plugin.guessRound(); + if (round == null || round.finished()) { + Msg.error(sender, "Nenhuma adivinha aberta."); + return true; + } + if (args.length == 0) { + Msg.error(sender, "Uso: /palpite "); + return true; + } + player.sendMessage(round.guess(player, args[0])); + return true; + } + + private boolean voteLatest(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + return true; + } + Poll poll = plugin.poll(); + if (poll == null || poll.closed()) { + Msg.error(sender, "Nenhuma enquete aberta."); + return true; + } + if (args.length == 0) { + Msg.error(sender, "Uso: /votar "); + return true; + } + Component result = poll.vote(player, parse(args[0], -1)); + player.sendMessage(result == null + ? Component.text("Essa opção não existe.", NamedTextColor.RED) + : result); + return true; + } + + private List reactionKeys() { + List keys = new ArrayList<>(); + for (ReactionDef def : plugin.settings().reactions()) { + keys.add(def.key()); + } + return keys; + } + + /** Who is online and on which platform. */ + private void platforms(CommandSender sender) { + Msg.header(sender, "Jogadores online"); + if (Bukkit.getOnlinePlayers().isEmpty()) { + sender.sendMessage(Component.text(" (ninguém online)", NamedTextColor.GRAY)); + return; + } + for (Player player : Bukkit.getOnlinePlayers()) { + boolean bedrock = Platform.isBedrock(player); + sender.sendMessage(Component.text(" " + player.getName() + " ", NamedTextColor.GREEN) + .append(Component.text(bedrock ? "[Bedrock]" : "[Java]", + bedrock ? NamedTextColor.AQUA : NamedTextColor.GOLD))); + } + } + private void react(CommandSender sender, String[] args) { if (!(sender instanceof Player player) || args.length < 2) { return; @@ -466,15 +565,19 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter { if (!admin(sender)) { return; } - if (args.length >= 3 && args[0].equalsIgnoreCase("add")) { - String label = String.join(" ", Arrays.copyOfRange(args, 2, args.length)); - plugin.settings().reaction(args[1].toLowerCase(), label); - Msg.ok(sender, "Reação '" + args[1] + "' definida como " + label + "."); + if (args.length >= 4 && args[0].equalsIgnoreCase("add")) { + // add [comando] + String key = args[1].toLowerCase(); + String command = args.length >= 5 ? args[4].toLowerCase() : key; + plugin.settings().reaction(key, args[2], args[3], command); + Msg.ok(sender, "Reação '" + key + "': " + args[2] + " / " + args[3] + + " (/" + command + "). Reinicie para registrar o comando novo."); } else if (args.length >= 2 && args[0].equalsIgnoreCase("remover")) { plugin.settings().removeReaction(args[1].toLowerCase()); Msg.ok(sender, "Reação '" + args[1] + "' removida."); } else { - Msg.error(sender, "Uso: /curiosidade reacao add | remover "); + Msg.error(sender, "Uso: /curiosidade reacao add [comando]" + + " | remover "); } } @@ -506,7 +609,12 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter { Msg.line(sender, "reações", settings.reactionsEnabled() ? "ativas" : "desativadas"); Msg.line(sender, "barra de reação", settings.reactionWindowSeconds() + "s"); Msg.line(sender, "cliques válidos por", settings.reactionValidityMinutes() + " min"); - Msg.line(sender, "reações definidas", String.join(" ", settings.reactions().values())); + StringBuilder labels = new StringBuilder(); + for (ReactionDef def : settings.reactions()) { + labels.append(def.java()).append(' ').append(def.texto()) + .append(" (/").append(def.comando()).append(") "); + } + Msg.line(sender, "reações definidas", labels.toString().trim()); Msg.line(sender, "adivinha", settings.guessSeconds() + "s por rodada"); Msg.line(sender, "enquete", settings.pollMinutes() + " min por enquete"); Msg.line(sender, "ranking", "top " + settings.rankingSize()); @@ -543,6 +651,10 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter { commands.put("/ranking [categoria]", "mostra os placares do servidor"); commands.put("/canalhandia status", "mostra toda a configuração"); commands.put("/canalhandia modulos", "lista os módulos e seu estado"); + commands.put("/canalhandia plataformas", "quem está online e se é Java ou Bedrock"); + commands.put("/reagir ", "reage à última mensagem (funciona no Bedrock)"); + commands.put("/palpite ", "palpita na adivinha sem clicar"); + commands.put("/votar ", "vota na enquete sem clicar"); if (sender.hasPermission(ADMIN)) { commands.put("/canalhandia modulo ", "liga/desliga um módulo"); commands.put("/canalhandia marcos", "força uma verificação de marcos"); @@ -623,7 +735,7 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter { } if (name.equals("canalhandia")) { if (args.length == 1) { - List options = new ArrayList<>(List.of("status", "modulos")); + List options = new ArrayList<>(List.of("status", "modulos", "plataformas")); if (sender.hasPermission(ADMIN)) { options.addAll(List.of("modulo", "marcos", "limpar", "reload")); } diff --git a/src/main/java/dev/marcospaulo/canalhandia/GuessRound.java b/src/main/java/dev/marcospaulo/canalhandia/GuessRound.java index 4e97d5d..2c21c3d 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/GuessRound.java +++ b/src/main/java/dev/marcospaulo/canalhandia/GuessRound.java @@ -70,8 +70,13 @@ final class GuessRound { return Component.text("Palpite registrado: " + name, NamedTextColor.GREEN); } - /** The question, with one clickable button per candidate. */ - Component question(String commandBase) { + /** + * The question with the candidates listed. + * + *

Java clients get clickable names; Bedrock cannot run a chat clickEvent, + * so it gets the same names plus the command to type instead. + */ + Component question(String commandBase, boolean bedrock) { Component message = Msg.tag("Adivinha", NamedTextColor.LIGHT_PURPLE) .append(Component.text("Alguém aqui ", NamedTextColor.WHITE) .decoration(TextDecoration.BOLD, false)) @@ -81,8 +86,16 @@ final class GuessRound { Component row = Component.text(" "); for (String candidate : candidates) { - row = row.append(Component.text("[" + candidate + "] ", NamedTextColor.AQUA) - .clickEvent(ClickEvent.runCommand(commandBase + " " + id + " " + candidate))); + Component piece = Component.text("[" + candidate + "] ", NamedTextColor.AQUA); + if (!bedrock) { + piece = piece.clickEvent( + ClickEvent.runCommand(commandBase + " " + id + " " + candidate)); + } + row = row.append(piece); + } + if (bedrock) { + row = row.append(Component.newline()) + .append(Component.text(" digite /palpite ", NamedTextColor.GRAY)); } return message.append(Component.newline()).append(row); } diff --git a/src/main/java/dev/marcospaulo/canalhandia/Platform.java b/src/main/java/dev/marcospaulo/canalhandia/Platform.java new file mode 100644 index 0000000..2bd2308 --- /dev/null +++ b/src/main/java/dev/marcospaulo/canalhandia/Platform.java @@ -0,0 +1,33 @@ +package dev.marcospaulo.canalhandia; + +import org.bukkit.entity.Player; + +/** + * Tells Bedrock players apart from Java players. + * + *

Floodgate mints UUIDs whose high 64 bits are all zero — every Bedrock + * player on this server looks like {@code 00000000-0000-0000-0009-01f584c722c4}. + * Checking that costs nothing and keeps Floodgate an optional runtime + * dependency rather than a compile-time one, so the plugin still loads on a + * server without it. + * + *

This matters because Bedrock cannot do two things Java can: + *

    + *
  • render most emoji — they come out as tofu boxes
  • + *
  • run a chat {@code clickEvent} — the text simply is not clickable
  • + *
+ * So Bedrock players get plain-text labels and typed commands instead. + */ +final class Platform { + + private Platform() { + } + + static boolean isBedrock(Player player) { + return player.getUniqueId().getMostSignificantBits() == 0L; + } + + static String label(Player player) { + return isBedrock(player) ? "Bedrock" : "Java"; + } +} diff --git a/src/main/java/dev/marcospaulo/canalhandia/Poll.java b/src/main/java/dev/marcospaulo/canalhandia/Poll.java index e405a54..edfa639 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/Poll.java +++ b/src/main/java/dev/marcospaulo/canalhandia/Poll.java @@ -69,7 +69,13 @@ final class Poll { return total; } - Component announcement(String commandBase) { + /** + * The poll with its options. + * + *

Java clients get clickable options; Bedrock cannot run a chat + * clickEvent, so it gets the command to type instead. + */ + Component announcement(String commandBase, boolean bedrock) { Component message = Msg.tag("Enquete", NamedTextColor.BLUE) .append(Component.text(question, NamedTextColor.WHITE) .decoration(TextDecoration.BOLD, false)) @@ -78,9 +84,17 @@ final class Poll { Component row = Component.text(" "); for (int i = 0; i < options.size(); i++) { - row = row.append(Component.text("[" + (i + 1) + ". " + options.get(i) + "] ", - NamedTextColor.AQUA) - .clickEvent(ClickEvent.runCommand(commandBase + " " + id + " " + (i + 1)))); + Component piece = Component.text("[" + (i + 1) + ". " + options.get(i) + "] ", + NamedTextColor.AQUA); + if (!bedrock) { + piece = piece.clickEvent( + ClickEvent.runCommand(commandBase + " " + id + " " + (i + 1))); + } + row = row.append(piece); + } + if (bedrock) { + row = row.append(Component.newline()) + .append(Component.text(" digite /votar ", NamedTextColor.GRAY)); } return message.append(Component.newline()).append(row); } diff --git a/src/main/java/dev/marcospaulo/canalhandia/ReactionDef.java b/src/main/java/dev/marcospaulo/canalhandia/ReactionDef.java new file mode 100644 index 0000000..74878e0 --- /dev/null +++ b/src/main/java/dev/marcospaulo/canalhandia/ReactionDef.java @@ -0,0 +1,17 @@ +package dev.marcospaulo.canalhandia; + +/** + * One configured reaction, with a label per platform and a typed shortcut. + * + * @param key internal name, used in {@code /reagir } + * @param java label for Java clients — emoji are fine here + * @param texto label for Bedrock clients — ASCII only, emoji render as tofu + * @param comando typed shortcut such as {@code wow}, for players who cannot click + */ +record ReactionDef(String key, String java, String texto, String comando) { + + /** The label to show to a given platform. */ + String label(boolean bedrock) { + return bedrock ? texto : java; + } +} diff --git a/src/main/java/dev/marcospaulo/canalhandia/Reactions.java b/src/main/java/dev/marcospaulo/canalhandia/Reactions.java index a84dc59..8cbdf0b 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/Reactions.java +++ b/src/main/java/dev/marcospaulo/canalhandia/Reactions.java @@ -10,6 +10,7 @@ import org.bukkit.entity.Player; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -17,27 +18,30 @@ import java.util.UUID; /** * Reaction state for one announced message. * - *

Chat cannot be edited after sending, so the counts inside the clickable - * buttons are frozen at send time. Three other surfaces carry the live numbers: - * a boss bar while the window is open, an action bar shown to whoever just - * reacted, and a final tally line broadcast when the window closes. Reactions - * keep being accepted after the boss bar disappears, because people scroll back - * and click late. + *

Chat cannot be edited after sending, so the counts inside the buttons are + * frozen at send time. Three other surfaces carry the live numbers: a boss bar + * while the window is open, an action bar shown to whoever just reacted, and a + * final tally line when the window closes. Reactions keep being accepted after + * the boss bar disappears, because people scroll back and click late. + * + *

Bedrock clients get a different rendering: ASCII labels instead of emoji, + * and a typed-command hint instead of clickable text, because Geyser cannot + * deliver a chat {@code clickEvent} to Bedrock. */ final class Reactions { private final int id; + private final List defs; private final Map> votes = new LinkedHashMap<>(); - private final Map labels; private final BossBar bar; private final long createdAt = System.currentTimeMillis(); private boolean barVisible; - Reactions(int id, Map labels) { + Reactions(int id, List defs) { this.id = id; - this.labels = labels; - labels.keySet().forEach(key -> votes.put(key, new LinkedHashSet<>())); - this.bar = BossBar.bossBar(tally(), 1.0f, BossBar.Color.PURPLE, BossBar.Overlay.PROGRESS); + this.defs = defs; + defs.forEach(def -> votes.put(def.key(), new LinkedHashSet<>())); + this.bar = BossBar.bossBar(tally(false), 1.0f, BossBar.Color.PURPLE, BossBar.Overlay.PROGRESS); } int id() { @@ -52,6 +56,10 @@ final class Reactions { return votes.values().stream().anyMatch(set -> !set.isEmpty()); } + boolean knows(String key) { + return votes.containsKey(key); + } + /** * Records a reaction. One per player per message; reacting again with a * different key moves the vote rather than double-counting it. @@ -65,9 +73,9 @@ final class Reactions { votes.values().forEach(set -> set.remove(player.getUniqueId())); votes.get(key).add(player.getUniqueId()); if (barVisible) { - bar.name(tally()); + bar.name(tally(false)); } - player.sendActionBar(tally()); + player.sendActionBar(tally(Platform.isBedrock(player))); return true; } @@ -76,25 +84,48 @@ final class Reactions { return set == null ? 0 : set.size(); } - /** The clickable row. Counts are the values at send time and never change. */ - Component buttons(String commandBase) { + /** + * The reaction row. On Java the labels are clickable; on Bedrock they are + * plain text followed by the commands to type, since clicking does nothing + * there. + */ + Component buttons(boolean bedrock) { Component row = Component.text(" "); - for (Map.Entry entry : labels.entrySet()) { - String text = entry.getValue() + " " + count(entry.getKey()) + " "; - row = row.append(Component.text(text, NamedTextColor.YELLOW) - .clickEvent(ClickEvent.runCommand(commandBase + " " + id + " " + entry.getKey())) - .hoverEvent(HoverEvent.showText(Component.text( - "Clique para reagir com " + entry.getValue(), NamedTextColor.GRAY)))); + for (ReactionDef def : defs) { + String text = def.label(bedrock) + " " + count(def.key()) + " "; + Component piece = Component.text(text, NamedTextColor.YELLOW); + if (!bedrock) { + piece = piece + .clickEvent(ClickEvent.runCommand("/canalhandia reagir " + id + " " + def.key())) + .hoverEvent(HoverEvent.showText(Component.text( + "Clique para reagir com " + def.java(), NamedTextColor.GRAY))); + } + row = row.append(piece); + } + if (bedrock) { + row = row.append(Component.text("— digite " + shortcuts(), NamedTextColor.GRAY)); } return row; } + /** Comma-joined typed shortcuts, e.g. "/legal, /wow, /top". */ + String shortcuts() { + StringBuilder text = new StringBuilder(); + for (ReactionDef def : defs) { + if (text.length() > 0) { + text.append(", "); + } + text.append('/').append(def.comando()); + } + return text.toString(); + } + /** Live counts, used for the boss bar, action bar and closing line. */ - Component tally() { + Component tally(boolean bedrock) { Component text = Component.text("Reações: ", NamedTextColor.WHITE); - for (Map.Entry entry : labels.entrySet()) { - text = text.append(Component.text(entry.getValue() + " ", NamedTextColor.YELLOW)) - .append(Component.text(count(entry.getKey()) + " ", NamedTextColor.AQUA)); + for (ReactionDef def : defs) { + text = text.append(Component.text(def.label(bedrock) + " ", NamedTextColor.YELLOW)) + .append(Component.text(count(def.key()) + " ", NamedTextColor.AQUA)); } return text; } diff --git a/src/main/java/dev/marcospaulo/canalhandia/Settings.java b/src/main/java/dev/marcospaulo/canalhandia/Settings.java index 02b9011..691bf05 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/Settings.java +++ b/src/main/java/dev/marcospaulo/canalhandia/Settings.java @@ -2,8 +2,8 @@ package dev.marcospaulo.canalhandia; import org.bukkit.configuration.ConfigurationSection; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; /** * Typed view over config.yml. @@ -106,22 +106,53 @@ final class Settings { set("reacao-validade-minutos", Math.max(1, minutes)); } - Map reactions() { - Map reactions = new LinkedHashMap<>(); + /** + * Configured reactions. + * + *

Accepts both the rich form (a section with {@code java}, {@code texto} + * and {@code comando}) and the old flat form where the value was just a + * label string, so an existing config keeps working — the flat form gets an + * ASCII fallback derived from the key. + */ + List reactions() { + List reactions = new ArrayList<>(); ConfigurationSection section = plugin.getConfig().getConfigurationSection("reacoes"); if (section != null) { for (String key : section.getKeys(false)) { - reactions.put(key, section.getString(key, key)); + ConfigurationSection entry = section.getConfigurationSection(key); + if (entry != null) { + reactions.add(new ReactionDef(key, + entry.getString("java", "[" + key + "]"), + entry.getString("texto", "[" + key.toUpperCase() + "]"), + entry.getString("comando", key))); + } else { + String label = section.getString(key, key); + reactions.add(new ReactionDef(key, label, + "[" + key.toUpperCase() + "]", key)); + } } } if (reactions.isEmpty()) { - reactions.put("joia", "[+1]"); + reactions.add(new ReactionDef("joia", "[+1]", "[+1]", "legal")); } return reactions; } - void reaction(String key, String label) { - set("reacoes." + key, label); + /** Maps a typed shortcut such as "wow" to its reaction key, or null. */ + String reactionForCommand(String command) { + for (ReactionDef def : reactions()) { + if (def.comando().equalsIgnoreCase(command)) { + return def.key(); + } + } + return null; + } + + void reaction(String key, String javaLabel, String textLabel, String command) { + plugin.getConfig().set("reacoes." + key + ".java", javaLabel); + plugin.getConfig().set("reacoes." + key + ".texto", textLabel); + plugin.getConfig().set("reacoes." + key + ".comando", command); + plugin.saveConfig(); } void removeReaction(String key) { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index c80947a..a4e9a40 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -59,13 +59,29 @@ janela-reacao-segundos: 90 # parece defeito. reacao-validade-minutos: 15 -# As reações disponíveis. A chave é usada no comando; o valor aparece no chat. -# Emoji funcionam no Java; no Bedrock (Geyser) alguns não renderizam, então -# rótulos em texto puro são a opção segura se você tem muitos jogadores Bedrock. +# As reações disponíveis. +# +# java - rótulo para jogadores Java (emoji funcionam) +# texto - rótulo para jogadores Bedrock (SÓ ASCII: emoji viram quadradinhos) +# comando - atalho digitável, para quem não consegue clicar +# +# O Bedrock não consegue executar clique no chat, então todo jogador Bedrock vê +# os rótulos "texto" e a dica de digitar o comando. Os comandos abaixo precisam +# existir no plugin.yml para serem registrados — mudar "comando" para um nome +# novo exige adicionar o comando lá e reiniciar. reacoes: - joia: "[👍]" - uau: "[😮]" - fogo: "[🔥]" + joia: + java: "[👍]" + texto: "[+1]" + comando: "legal" + uau: + java: "[😮]" + texto: "[UAU]" + comando: "wow" + fogo: + java: "[🔥]" + texto: "[TOP]" + comando: "top" # --- Jogos ------------------------------------------------------------------- diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index f45eab5..497071e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -26,6 +26,32 @@ commands: description: Placares do servidor. usage: /ranking [categoria] aliases: [rankings, placar] + # Typed fallbacks. Bedrock cannot run a chat clickEvent, so every clickable + # interaction needs a command that can be typed instead. The shortcut names + # below must exist here to be registered; config.yml maps each one to a + # reaction key. + reagir: + description: Reage à última mensagem. + usage: /reagir + palpite: + description: Palpita na adivinha aberta. + usage: /palpite + votar: + description: Vota na enquete aberta. + usage: /votar + legal: + description: Reage com "legal" à última mensagem. + usage: /legal + wow: + description: Reage com "uau" à última mensagem. + usage: /wow + aliases: [uau] + top: + description: Reage com "top" à última mensagem. + usage: /top + f: + description: Presta luto pela última morte. + usage: /f permissions: # Declared explicitly: an undeclared Bukkit permission falls back to op-only,