Files
canalhandia/src/main/java/dev/marcospaulo/canalhandia/CanalhandiaCommand.java
T

2416 lines
113 KiB
Java

package dev.marcospaulo.canalhandia;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
/**
* Every command the plugin owns.
*
* <p>{@code /curiosidade}, {@code /adivinha}, {@code /enquete} and
* {@code /ranking} are player-facing shortcuts; {@code /canalhandia} is the
* admin root and also carries the click callbacks that the chat buttons invoke.
*/
final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
private static final String ADMIN = "canalhandia.admin";
private final Canalhandia plugin;
CanalhandiaCommand(Canalhandia plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
return switch (command.getName().toLowerCase()) {
case "curiosidade" -> curiosidade(sender, args);
case "adivinha" -> adivinha(sender);
case "enquete" -> enquete(sender, args);
case "ranking" -> ranking(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);
case "reacoes" -> whoReacted(sender);
case "ia" -> ia(sender, args, false);
case "iap" -> ia(sender, args, true);
case "nota" -> nota(sender, args);
// Quick shortcut: "/save" pins where you are, "/save <texto>" pins
// it with a note. Private by default — the safe default for a
// one-word command nobody reads the help for first.
case "save" -> saveShortcut(sender, args);
case "recado" -> recado(sender, args);
case "recados" -> recados(sender);
case "mortes" -> mortes(sender);
case "conquistas" -> conquistas(sender, args);
case "perfil" -> perfil(sender, args);
case "titulo" -> titulo(sender, args);
case "chunkloader", "ancora", "ancoras", "chunkloaders" -> chunkloader(sender, args);
// Typed twin of the [F] mourning button — "f" is not a configured
// reaction (the mourning set is hardcoded), so it can't be found via
// reactionForCommand; route it directly. Acts on the latest message,
// so it pays respects only if a mourning window is the most recent.
case "f" -> reactLatest(sender, "f");
default -> {
String reaction = plugin.settings().reactionForCommand(command.getName());
if (reaction != null) {
yield reactLatest(sender, reaction);
}
yield root(sender, args);
}
};
}
// --- /canalhandia -------------------------------------------------------
private boolean root(CommandSender sender, String[] args) {
if (args.length == 0) {
help(sender);
return true;
}
String[] rest = Arrays.copyOfRange(args, 1, args.length);
switch (args[0].toLowerCase()) {
// Click callbacks.
case "reagir" -> react(sender, rest);
case "palpite" -> guess(sender, rest);
case "votar" -> vote(sender, rest);
// Admin.
case "modulo" -> moduleToggle(sender, rest);
case "modulos" -> modules(sender);
case "plataformas", "quem" -> platforms(sender);
case "status" -> status(sender);
case "marcos" -> {
if (admin(sender)) {
plugin.runMilestoneCheck();
Msg.ok(sender, "Verificação de marcos executada.");
}
}
case "limpar" -> clear(sender, rest);
case "zoacao" -> zoacaoEdit(sender, rest);
case "reload" -> {
if (admin(sender)) {
plugin.reloadConfig();
plugin.reloadCatalogo();
plugin.reloadI18n();
plugin.rescheduleTimer();
plugin.rescheduleMilestones();
Msg.ok(sender, "Recarregado: " + Achievement.values().length
+ " conquistas e " + plugin.milestones().trackCount() + " marcos.");
}
}
default -> help(sender);
}
return true;
}
// --- /curiosidade -------------------------------------------------------
private boolean curiosidade(CommandSender sender, String[] args) {
if (args.length == 0) {
if (!sender.hasPermission("canalhandia.forcar")) {
return denied(sender);
}
if (!plugin.announceCuriosity(null)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.curiosidade.nenhum-elegivel"));
}
return true;
}
String[] rest = Arrays.copyOfRange(args, 1, args.length);
switch (args[0].toLowerCase()) {
case "ver" -> preview(sender, rest);
case "listar" -> list(sender, rest);
case "toggle" -> toggle(sender);
case "status" -> status(sender);
case "categorias" -> categories(sender);
case "categoria" -> categoryToggle(sender, rest);
case "modo" -> mode(sender, rest);
case "intervalo" -> setNumber(sender, rest, "intervalo <minutos>",
v -> {
plugin.settings().intervalMinutes(v);
plugin.rescheduleTimer();
return "Intervalo: " + plugin.settings().intervalMinutes() + " min.";
});
case "atraso" -> setNumber(sender, rest, "atraso <segundos>",
v -> {
plugin.settings().joinDelaySeconds(v);
return "Atraso após entrar: " + plugin.settings().joinDelaySeconds() + "s.";
});
case "cooldown" -> setNumber(sender, rest, "cooldown <minutos>",
v -> {
plugin.settings().cooldownMinutes(v);
return "Cooldown: " + plugin.settings().cooldownMinutes() + " min.";
});
case "repetir" -> setNumber(sender, rest, "repetir <quantidade>",
v -> {
plugin.settings().noRepeat(v);
return "Evitando repetir as últimas " + plugin.settings().noRepeat() + ".";
});
case "janela" -> setNumber(sender, rest, "janela <segundos>",
v -> {
plugin.settings().reactionWindowSeconds(v);
return "Janela de reação: " + plugin.settings().reactionWindowSeconds() + "s.";
});
case "resumo" -> setNumber(sender, rest, "resumo <quantidade>",
v -> {
plugin.settings().summaryNames(v);
return "Mostrando até " + plugin.settings().summaryNames()
+ " nomes no resumo.";
});
case "intervalo-minimo", "minimo" -> setNumber(sender, rest, "minimo <segundos>",
v -> {
plugin.settings().minGapSeconds(v);
return "Intervalo mínimo entre curiosidades automáticas: "
+ plugin.settings().minGapSeconds() + "s.";
});
case "validade" -> setNumber(sender, rest, "validade <minutos>",
v -> {
plugin.settings().reactionValidityMinutes(v);
return "Cliques válidos por "
+ plugin.settings().reactionValidityMinutes() + " min.";
});
case "reacoes" -> reactionsToggle(sender, rest);
case "reacao" -> reactionEdit(sender, rest);
case "ajuda" -> help(sender);
default -> {
Player target = Bukkit.getPlayerExact(args[0]);
if (target == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.curiosidade.subdesconhecido"));
} else if (!sender.hasPermission("canalhandia.forcar")) {
denied(sender);
} else if (!plugin.announceCuriosity(target)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.curiosidade.sem-stats",
Component.text(target.getName())));
}
}
}
return true;
}
private void preview(CommandSender sender, String[] args) {
Player target = resolveTarget(sender, args);
if (target == null) {
return;
}
List<Fact> facts = CuriosityFactory.facts(target, plugin.settings());
if (facts.isEmpty()) {
Msg.error(sender, Lang.tr("canalhandia.cmd.curiosidade.sem-curiosidade",
Component.text(target.getName())));
return;
}
Fact fact = facts.get((int) (Math.random() * facts.size()));
sender.sendMessage(Msg.tag("Curiosidade", NamedTextColor.GOLD)
.append(Component.text(target.getName() + " ", NamedTextColor.GREEN))
.append(fact.text()));
}
/** Dumps every available fact for a player, privately. Handy for tuning thresholds. */
private void list(CommandSender sender, String[] args) {
Player target = resolveTarget(sender, args);
if (target == null) {
return;
}
List<Fact> facts = CuriosityFactory.facts(target, plugin.settings());
Msg.header(sender, "Curiosidades de " + target.getName() + " (" + facts.size() + ")");
for (Fact fact : facts) {
sender.sendMessage(Component.text(" [" + fact.category().label() + "] ",
NamedTextColor.DARK_AQUA).append(fact.text()));
}
if (facts.isEmpty()) {
sender.sendMessage(Component.text(" (nada ainda)", NamedTextColor.GRAY));
}
}
private Player resolveTarget(CommandSender sender, String[] args) {
if (!sender.hasPermission("canalhandia.ver")) {
denied(sender);
return null;
}
if (args.length > 0) {
Player target = Bukkit.getPlayerExact(args[0]);
if (target == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.jogador.offline", Component.text(args[0])));
}
return target;
}
if (sender instanceof Player player) {
return player;
}
Msg.error(sender, Lang.tr("canalhandia.cmd.jogador.informe"));
return null;
}
private void toggle(CommandSender sender) {
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.usar"));
return;
}
boolean optedOut = !plugin.isOptedOut(player);
plugin.setOptedOut(player, optedOut);
sender.sendMessage(optedOut
? Lang.tr("canalhandia.cmd.curiosidade.toggle-off").color(NamedTextColor.YELLOW)
: Lang.tr("canalhandia.cmd.curiosidade.toggle-on").color(NamedTextColor.GREEN));
}
// --- /adivinha ----------------------------------------------------------
private boolean adivinha(CommandSender sender) {
if (!sender.hasPermission("canalhandia.forcar")) {
return denied(sender);
}
if (!plugin.settings().moduleEnabled(Module.ADIVINHA)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desativado", Component.text("adivinha")));
} else if (!plugin.startGuess()) {
Msg.error(sender, "Precisa de pelo menos 2 jogadores online com estatísticas.");
}
return true;
}
private void guess(CommandSender sender, String[] args) {
if (!(sender instanceof Player player) || args.length < 2) {
return;
}
GuessRound round = plugin.guessRound();
if (round == null || round.id() != parse(args[0], -1)) {
player.sendActionBar(Lang.tr("canalhandia.cmd.adivinha.rodada-acabou").color(NamedTextColor.RED));
return;
}
player.sendActionBar(round.guess(player, args[1]));
}
// --- /enquete -----------------------------------------------------------
private boolean enquete(CommandSender sender, String[] args) {
if (!sender.hasPermission("canalhandia.enquete")) {
return denied(sender);
}
if (!plugin.settings().moduleEnabled(Module.ENQUETE)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desativado", Component.text("enquete")));
return true;
}
if (args.length == 1 && args[0].equalsIgnoreCase("encerrar")) {
Poll poll = plugin.poll();
if (poll == null || poll.closed()) {
Msg.error(sender, Lang.tr("canalhandia.cmd.enquete.nenhuma"));
} else {
Bukkit.broadcast(poll.close());
}
return true;
}
// Everything is one string split on "|": question | option | option ...
String[] parts = String.join(" ", args).split("\\|");
if (parts.length < 3) {
Msg.error(sender, "Uso: /enquete Pergunta | Opção 1 | Opção 2 [| Opção 3 ...]");
return true;
}
List<String> options = new ArrayList<>();
for (int i = 1; i < parts.length && options.size() < 5; i++) {
String option = parts[i].trim();
if (!option.isEmpty()) {
options.add(option);
}
}
if (options.size() < 2) {
Msg.error(sender, "Informe pelo menos duas opções.");
return true;
}
plugin.startPoll(parts[0].trim(), options, sender.getName());
return true;
}
private void vote(CommandSender sender, String[] args) {
if (!(sender instanceof Player player) || args.length < 2) {
return;
}
Poll poll = plugin.poll();
if (poll == null || poll.id() != parse(args[0], -1)) {
player.sendActionBar(Lang.tr("canalhandia.cmd.votar.encerrada").color(NamedTextColor.RED));
return;
}
Component result = poll.vote(player, parse(args[1], -1));
if (result != null) {
player.sendActionBar(result);
}
}
// --- /ranking -----------------------------------------------------------
private boolean ranking(CommandSender sender, String[] args) {
if (!sender.hasPermission("canalhandia.ver")) {
return denied(sender);
}
if (!plugin.settings().moduleEnabled(Module.RANKING)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desativado", Component.text("ranking")));
return true;
}
if (args.length == 0) {
Msg.header(sender, "Rankings disponíveis");
for (RankingMetric metric : RankingMetric.values()) {
sender.sendMessage(Component.text(" " + metric.commandKey(), NamedTextColor.AQUA)
.clickEvent(ClickEvent.runCommand("/ranking " + metric.commandKey()))
.append(Component.text("" + metric.label(), NamedTextColor.GRAY)));
}
sender.sendMessage(Component.text(" semanal [métrica]", NamedTextColor.GOLD)
.clickEvent(ClickEvent.runCommand("/ranking semanal"))
.append(Component.text(" — só o que foi ganho nesta semana",
NamedTextColor.GRAY)));
return true;
}
// "/ranking semanal [metrica]" — the same boards, but showing only what
// was gained since the weekly baseline. An all-time board on a small
// server is decided by who started first; this makes it a contest again.
boolean weekly = args[0].equalsIgnoreCase("semanal") || args[0].equalsIgnoreCase("semana");
String[] rest = weekly ? Arrays.copyOfRange(args, 1, args.length) : args;
if (weekly && rest.length == 0) {
// Default to the metric people actually race on.
rest = new String[]{RankingMetric.MINERACAO.commandKey()};
}
RankingMetric metric = RankingMetric.byKey(rest[0]);
if (metric == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.ranking.desconhecido"));
return true;
}
int size = plugin.settings().rankingSize();
List<OfflineStats.Row> rows;
if (weekly) {
rows = WeeklyStats.delta(plugin.offlineStats().ranking(metric, Integer.MAX_VALUE),
plugin.weeklyStats().baseline(metric), size);
} else {
rows = plugin.offlineStats().ranking(metric, size);
}
Msg.header(sender, (weekly ? "Ranking da semana: " : "Ranking: ") + metric.label());
if (weekly) {
long taken = plugin.weeklyStats().takenAt();
Msg.line(sender, "desde", taken == 0 ? "o começo" : Msg.ago(taken));
}
if (rows.isEmpty()) {
sender.sendMessage(Component.text(" ", NamedTextColor.GRAY)
.append(Lang.tr("canalhandia.cmd.ranking.sem-dados")));
return true;
}
for (int i = 0; i < rows.size(); i++) {
OfflineStats.Row row = rows.get(i);
NamedTextColor color = switch (i) {
case 0 -> NamedTextColor.GOLD;
case 1 -> NamedTextColor.GRAY;
case 2 -> NamedTextColor.DARK_RED;
default -> NamedTextColor.WHITE;
};
sender.sendMessage(Component.text(" " + (i + 1) + ". ", color)
.append(Component.text(row.name() + " ", NamedTextColor.GREEN))
.append(Component.text(metric.format(row.value()), NamedTextColor.AQUA)));
}
return true;
}
// --- 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, Lang.tr("canalhandia.cmd.sojogador.reagir"));
return true;
}
if (!player.hasPermission("canalhandia.reagir")) {
return denied(sender);
}
Reactions reactions = plugin.latestReactions();
if (reactions == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.reagir.nada"));
return true;
}
if (key == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.reagir.uso",
Component.text(String.join("|", reactionKeys()))));
return true;
}
if (!reactions.react(player, key)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.reagir.invalida"));
} else {
plugin.afterReact(player, reactions.id(), key);
}
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, Lang.tr("canalhandia.cmd.palpite.nenhuma"));
return true;
}
if (args.length == 0) {
Msg.error(sender, Lang.tr("canalhandia.cmd.palpite.uso"));
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, Lang.tr("canalhandia.cmd.enquete.nenhuma"));
return true;
}
if (args.length == 0) {
Msg.error(sender, Lang.tr("canalhandia.cmd.votar.uso"));
return true;
}
Component result = poll.vote(player, parse(args[0], -1));
player.sendMessage(result == null
? Lang.tr("canalhandia.cmd.votar.opcao-inexistente").color(NamedTextColor.RED)
: result);
return true;
}
/** Who reacted to the last message — sent privately so chat stays clean. */
private boolean whoReacted(CommandSender sender) {
Reactions reactions = plugin.latestReactions();
if (reactions == null || !reactions.hasAnyVote()) {
Msg.error(sender, Lang.tr("canalhandia.cmd.reacoes.nenhuma"));
return true;
}
boolean bedrock = sender instanceof Player player && Platform.isBedrock(player);
Msg.header(sender, Lang.tr("canalhandia.cmd.reacoes.cabecalho", Component.text(reactions.total())));
reactions.breakdown(bedrock).forEach(sender::sendMessage);
return true;
}
private List<String> reactionKeys() {
List<String> 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;
}
if (!player.hasPermission("canalhandia.reagir")) {
denied(sender);
return;
}
Reactions reactions = plugin.findReactions(parse(args[0], -1));
if (reactions == null) {
player.sendActionBar(Lang.tr("canalhandia.cmd.reagir.expirou").color(NamedTextColor.RED));
return;
}
if (!reactions.react(player, args[1])) {
player.sendActionBar(Lang.tr("canalhandia.cmd.reagir.desconhecida").color(NamedTextColor.RED));
} else {
plugin.afterReact(player, reactions.id(), args[1]);
}
}
// --- admin --------------------------------------------------------------
private void moduleToggle(CommandSender sender, String[] args) {
if (!admin(sender)) {
return;
}
if (args.length < 2) {
Msg.error(sender, "Uso: /canalhandia modulo <nome> <on|off>");
return;
}
Module module = Module.byKey(args[0]);
if (module == null) {
Msg.error(sender, "Módulo desconhecido. Veja /canalhandia modulos");
return;
}
boolean on = args[1].equalsIgnoreCase("on");
plugin.settings().moduleEnabled(module, on);
if (module == Module.CHUNKLOADER) {
if (on) {
plugin.chunkLoaders().loadAllTickets();
plugin.chunkLoaders().startSimulation();
ChunkAnchorItem.registerRecipe(plugin);
} else {
plugin.chunkLoaders().stopSimulation();
plugin.chunkLoaders().unloadAllTickets();
ChunkAnchorItem.unregisterRecipe(plugin);
}
plugin.blueMap().syncChunkLoaders();
}
plugin.rescheduleTimer();
plugin.rescheduleMilestones();
Msg.ok(sender, "Módulo " + module.label() + " " + (on ? "ativado" : "desativado") + ".");
}
private void modules(CommandSender sender) {
Msg.header(sender, "Módulos");
for (Module module : Module.values()) {
boolean on = plugin.settings().moduleEnabled(module);
sender.sendMessage(Component.text(" " + module.key() + " ", NamedTextColor.WHITE)
.append(Component.text(on ? "ativado" : "desativado",
on ? NamedTextColor.GREEN : NamedTextColor.RED))
.append(Component.text(" [alternar]", NamedTextColor.DARK_AQUA)
.clickEvent(ClickEvent.runCommand("/canalhandia modulo "
+ module.key() + (on ? " off" : " on"))))
.append(Component.text(" " + module.label(), NamedTextColor.DARK_GRAY)));
}
}
private void mode(CommandSender sender, String[] args) {
if (!admin(sender)) {
return;
}
if (args.length == 0) {
Msg.error(sender, "Uso: /curiosidade modo <entrada|intervalo|ambos|manual>");
return;
}
Mode mode = Mode.byKey(args[0]);
if (mode == null) {
Msg.error(sender, "Modo inválido. Use entrada, intervalo, ambos ou manual.");
return;
}
plugin.settings().mode(mode);
plugin.rescheduleTimer();
Msg.ok(sender, "Modo alterado para " + mode + ".");
}
private void categoryToggle(CommandSender sender, String[] args) {
if (!admin(sender)) {
return;
}
if (args.length < 2) {
Msg.error(sender, "Uso: /curiosidade categoria <nome> <on|off>");
return;
}
Category category = Category.byKey(args[0]);
if (category == null) {
Msg.error(sender, "Categoria desconhecida. Veja /curiosidade categorias");
return;
}
boolean on = args[1].equalsIgnoreCase("on");
plugin.settings().categoryEnabled(category, on);
Msg.ok(sender, "Categoria " + category.label() + " " + (on ? "ativada" : "desativada") + ".");
}
private void categories(CommandSender sender) {
Msg.header(sender, "Categorias");
for (Category category : Category.values()) {
boolean on = plugin.settings().categoryEnabled(category);
sender.sendMessage(Component.text(" " + category.key() + " ", NamedTextColor.WHITE)
.append(Component.text(on ? "ativada" : "desativada",
on ? NamedTextColor.GREEN : NamedTextColor.RED))
.append(Component.text(" [alternar]", NamedTextColor.DARK_AQUA)
.clickEvent(ClickEvent.runCommand("/curiosidade categoria "
+ category.key() + (on ? " off" : " on")))));
}
}
private void reactionsToggle(CommandSender sender, String[] args) {
if (!admin(sender)) {
return;
}
if (args.length == 0) {
Msg.error(sender, "Uso: /curiosidade reacoes <on|off>");
return;
}
boolean on = args[0].equalsIgnoreCase("on");
plugin.settings().reactionsEnabled(on);
Msg.ok(sender, "Reações " + (on ? "ativadas" : "desativadas") + ".");
}
private void reactionEdit(CommandSender sender, String[] args) {
if (!admin(sender)) {
return;
}
if (args.length >= 4 && args[0].equalsIgnoreCase("add")) {
// add <chave> <rótulo-java> <rótulo-bedrock> [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 <chave> <java> <bedrock> [comando]"
+ " | remover <chave>");
}
}
/**
* In-game editing of the {@code zoacao} chat gag: the match mode, the
* trigger pattern, and the replacement message list. Admin-only; every
* change writes through to config.yml immediately.
*/
private void zoacaoEdit(CommandSender sender, String[] args) {
if (!admin(sender)) {
return;
}
Settings settings = plugin.settings();
String sub = args.length > 0 ? args[0].toLowerCase(Locale.ROOT) : "listar";
switch (sub) {
case "listar", "status" -> {
Msg.header(sender, "Zoação (gag de chat)");
Msg.line(sender, "correspondência", settings.zoacaoMode().key()
+ " (igual | contem | comeca | termina | regex)");
Msg.line(sender, "padrão", settings.zoacaoPattern());
List<String> messages = settings.zoacaoMessages();
Msg.line(sender, "mensagens", "(" + messages.size() + ")");
for (int i = 0; i < messages.size(); i++) {
sender.sendMessage(Component.text(" " + (i + 1) + ". ", NamedTextColor.DARK_AQUA)
.append(Component.text(messages.get(i), NamedTextColor.WHITE)));
}
}
case "modo" -> {
if (args.length < 2) {
Msg.error(sender, "Uso: /canalhandia zoacao modo <igual|contem|comeca|termina|regex>");
return;
}
Zoacao.Mode mode = Zoacao.Mode.byKey(args[1]);
if (mode == null) {
Msg.error(sender, "Modo inválido. Use: igual, contem, comeca, termina, regex.");
return;
}
settings.zoacaoMode(mode);
Msg.ok(sender, "Correspondência da zoação: " + mode.key() + ".");
}
case "padrao" -> {
if (args.length < 2) {
Msg.error(sender, "Uso: /canalhandia zoacao padrao <texto> (atual: " + settings.zoacaoPattern() + ")");
return;
}
String pattern = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
settings.zoacaoPattern(pattern);
Msg.ok(sender, "Padrão da zoação: " + pattern + ".");
}
case "adicionar", "add" -> {
if (args.length < 2) {
Msg.error(sender, "Uso: /canalhandia zoacao adicionar <frase>");
return;
}
String line = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
List<String> messages = new ArrayList<>(settings.zoacaoMessages());
messages.add(line);
settings.zoacaoMessages(messages);
Msg.ok(sender, "Adicionado: " + line + " (agora " + messages.size() + " mensagens).");
}
case "remover", "remove" -> {
List<String> messages = new ArrayList<>(settings.zoacaoMessages());
if (args.length < 2) {
Msg.error(sender, "Uso: /canalhandia zoacao remover <número|texto>");
return;
}
String target = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
int index = parse(target, Integer.MIN_VALUE);
boolean removed;
if (index != Integer.MIN_VALUE) {
int idx = index - 1;
removed = idx >= 0 && idx < messages.size();
if (removed) {
messages.remove(idx);
}
} else {
removed = messages.removeIf(m -> m.equalsIgnoreCase(target));
}
if (!removed) {
Msg.error(sender, "Não encontrei '" + target + "' na lista.");
return;
}
settings.zoacaoMessages(messages);
Msg.ok(sender, "Removido. Restam " + messages.size() + " mensagens.");
}
case "limpar" -> {
// Clear the configured list so the built-in defaults come back.
settings.zoacaoMessages(List.of());
Msg.ok(sender, "Lista limpa — voltou para as mensagens padrão.");
}
default -> Msg.error(sender, "Uso: /canalhandia zoacao <listar|modo|padrao|adicionar|remover|limpar>");
}
}
private void clear(CommandSender sender, String[] args) {
if (!admin(sender)) {
return;
}
String what = args.length > 0 ? args[0].toLowerCase() : "tudo";
if (what.equals("cooldown") || what.equals("tudo")) {
plugin.clearCooldowns();
}
if (what.equals("historico") || what.equals("tudo")) {
plugin.clearHistory();
}
Msg.ok(sender, "Limpo: " + what + ".");
}
private void status(CommandSender sender) {
Settings settings = plugin.settings();
Msg.header(sender, "Canalhandia — status");
Msg.line(sender, "módulos ativos", enabledModules());
Msg.line(sender, "modo", settings.mode().name().toLowerCase());
Msg.line(sender, "intervalo", settings.intervalMinutes() + " min"
+ (settings.mode().firesOnTimer() ? "" : " (inativo neste modo)"));
Msg.line(sender, "atraso após entrar", settings.joinDelaySeconds() + "s"
+ (settings.mode().firesOnJoin() ? "" : " (inativo neste modo)"));
Msg.line(sender, "cooldown por jogador", settings.cooldownMinutes() + " min");
Msg.line(sender, "evitar repetir", settings.noRepeat() + " últimas");
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, "nomes no resumo", settings.summaryNames() + " (resto vira +N)");
Msg.line(sender, "intervalo mínimo", settings.minGapSeconds()
+ "s entre curiosidades automáticas");
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());
List<String> enabled = new ArrayList<>();
for (Category category : Category.values()) {
if (settings.categoryEnabled(category)) {
enabled.add(category.key());
}
}
Msg.line(sender, "categorias ativas", enabled.isEmpty() ? "nenhuma" : String.join(", ", enabled));
Msg.line(sender, "zoação", settings.zoacaoMode().key() + " '" + settings.zoacaoPattern()
+ "' (" + settings.zoacaoMessages().size() + " mensagens)");
Msg.line(sender, "ia", plugin.ai().configured()
? settings.aiModel() + " · perfil " + settings.aiProfile().name().toLowerCase(Locale.ROOT)
+ " · " + plugin.ai().askedToday() + "/" + settings.aiDailyLimit() + " hoje"
+ " · cooldown " + settings.aiCooldownSeconds() + "s · "
+ (settings.aiPublic() ? "resposta pública" : "resposta privada")
+ " · " + plugin.ai().corrections().all().size() + " correções"
+ " · " + plugin.ai().feedbackWrong() + " feedback ruim"
: "sem chave configurada");
Msg.line(sender, "ia contexto", "personalidade " + settings.aiPersona().key()
+ " · chat " + settings.aiChatContextLines() + " linhas ("
+ plugin.chatLog().size() + " na memória)"
+ " · estado do servidor " + (settings.aiServerState() ? "on" : "off")
+ " · estatísticas " + (settings.aiPlayerStats() ? "on" : "off")
+ " · estilo " + (settings.aiFancy() ? "rico" : "simples"));
Msg.line(sender, "ia espontânea",
(settings.aiEvents() ? "eventos on" : "eventos off")
+ " · " + (settings.aiWelcome() ? "saudação on" : "saudação off")
+ " · " + plugin.aiBudget().usedToday(System.currentTimeMillis())
+ "/" + settings.aiSpontaneousPerDay() + " hoje"
+ " · intervalo " + settings.aiSpontaneousGapMinutes() + "min");
Msg.line(sender, "notas", plugin.notes().size() + " no total"
+ (sender instanceof Player player
? " · " + plugin.notes().countBy(player.getUniqueId().toString())
+ " suas (máx. " + Notes.MAX_PER_PLAYER + ")"
: "")
+ " · " + settings.aiNotes() + " públicas vão para a IA");
}
private String enabledModules() {
List<String> on = new ArrayList<>();
for (Module module : Module.values()) {
if (plugin.settings().moduleEnabled(module)) {
on.add(module.key());
}
}
return on.isEmpty() ? "nenhum" : String.join(", ", on);
}
private void help(CommandSender sender) {
Msg.header(sender, "Canalhandia — comandos");
Map<String, String> commands = new LinkedHashMap<>();
commands.put("/curiosidade", "anuncia uma curiosidade agora");
commands.put("/curiosidade <jogador>", "anuncia sobre alguém específico");
commands.put("/curiosidade ver [jogador]", "mostra só para você");
commands.put("/curiosidade listar [jogador]", "lista todas as curiosidades disponíveis");
commands.put("/curiosidade toggle", "entra/sai do sorteio");
commands.put("/adivinha", "inicia uma rodada de 'adivinhe de quem é'");
commands.put("/enquete P | A | B", "abre uma enquete com opções clicáveis");
commands.put("/enquete encerrar", "encerra a enquete aberta");
commands.put("/ranking [categoria]", "mostra os placares do servidor");
commands.put("/ia <pergunta>", "pergunta para a IA (precisa de permissão)");
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 <chave>", "reage à última mensagem (funciona no Bedrock)");
commands.put("/reacoes", "mostra quem reagiu à última mensagem");
commands.put("/palpite <nome>", "palpita na adivinha sem clicar");
commands.put("/votar <número>", "vota na enquete sem clicar");
if (sender.hasPermission(ADMIN)) {
commands.put("/canalhandia modulo <nome> <on|off>", "liga/desliga um módulo");
commands.put("/canalhandia marcos", "força uma verificação de marcos");
commands.put("/canalhandia limpar [cooldown|historico|tudo]", "zera estado temporário");
commands.put("/canalhandia reload",
"recarrega config.yml, conquistas-catalogo.yml e marcos-catalogo.yml");
commands.put("/curiosidade modo <m>", "entrada | intervalo | ambos | manual");
commands.put("/curiosidade intervalo <min>", "intervalo do modo temporizado");
commands.put("/curiosidade atraso <seg>", "espera após o jogador entrar");
commands.put("/curiosidade cooldown <min>", "mínimo entre citar o mesmo jogador");
commands.put("/curiosidade repetir <n>", "quantas recentes evitar repetir");
commands.put("/curiosidade janela <seg>", "duração da barra de reações");
commands.put("/curiosidade validade <min>", "por quanto tempo cliques ainda contam");
commands.put("/curiosidade resumo <n>", "quantos nomes mostrar no resumo de reações");
commands.put("/curiosidade minimo <seg>", "intervalo mínimo entre curiosidades automáticas");
commands.put("/curiosidade reacoes <on|off>", "liga/desliga reações");
commands.put("/curiosidade reacao add <chave> <rótulo>", "cria ou muda uma reação");
commands.put("/curiosidade reacao remover <chave>", "remove uma reação");
commands.put("/curiosidade categoria <nome> <on|off>", "liga/desliga uma categoria");
commands.put("/canalhandia zoacao listar", "mostra a regra e as frases da zoação");
commands.put("/canalhandia zoacao modo <m>", "igual | contem | comeca | termina | regex");
commands.put("/canalhandia zoacao padrao <texto>", "texto/regex que dispara a zoação");
commands.put("/canalhandia zoacao adicionar <frase>", "adiciona uma frase de zoação");
commands.put("/canalhandia zoacao remover <n|texto>", "remove uma frase de zoação");
commands.put("/canalhandia zoacao limpar", "volta para as frases padrão");
commands.put("/ia personalidade", "lista as personalidades da IA");
commands.put("/ia personalidade <nome>", "muda o tom da IA (zoeiro, amigao, seco…)");
commands.put("/ia eventos <on|off>", "IA comenta mortes seguidas sozinha");
commands.put("/ia saudacao <on|off>", "IA dá as boas-vindas de quem entra");
}
commands.forEach((cmd, description) -> sender.sendMessage(
Component.text(" " + cmd, NamedTextColor.AQUA)
.append(Component.text("" + description, NamedTextColor.GRAY))));
}
// --- helpers ------------------------------------------------------------
/** Applies a validated positive-integer setting and reports the result. */
private void setNumber(CommandSender sender, String[] args, String usage,
java.util.function.IntFunction<String> apply) {
if (!admin(sender)) {
return;
}
if (args.length == 0) {
Msg.error(sender, "Uso: /curiosidade " + usage);
return;
}
int value = parse(args[0], Integer.MIN_VALUE);
if (value == Integer.MIN_VALUE) {
Msg.error(sender, "'" + args[0] + "' não é um número.");
return;
}
Msg.ok(sender, apply.apply(value));
}
private static int parse(String text, int fallback) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
return fallback;
}
}
// --- /ia ----------------------------------------------------------------
/**
* Asks the model a question.
*
* <p>Gated by {@code canalhandia.ia}, which defaults to op — so the operator
* has it out of the box and LuckPerms can hand it to anyone else with
* {@code lp user <nome> permission set canalhandia.ia true}.
*/
private boolean ia(CommandSender sender, String[] args, boolean isPrivate) {
// /ia and /iap are separate gates so LuckPerms can grant them
// independently — an operator can let someone ask privately without
// letting them spam public chat, or vice-versa. Both default to op,
// matching the "operator + LuckPerms-permitted only" rule.
String perm = isPrivate ? "canalhandia.ia.privado" : "canalhandia.ia";
if (!sender.hasPermission(perm)) {
return denied(sender);
}
if (!plugin.settings().moduleEnabled(Module.IA)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("IA")));
return true;
}
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.ia"));
return true;
}
// Subcommands come before the question. They only hijack when the
// second token is one they would actually act on, so a real question
// like "/ia perfil do servidor" or "/ia feedback do mapa?" falls
// through and is asked. "corrigir" stays greedy — a correction always
// reads the rest of the line, and "/ia corrigir ..." is never a natural
// question.
if (args.length > 0) {
String sub = args[0].toLowerCase(Locale.ROOT);
if (sub.equals("perfil") && (args.length == 1 || AiProfile.isValid(args[1]))) {
iaProfile(sender, args);
return true;
}
if (sub.equals("corrigir")) {
iaCorrect(sender, args);
return true;
}
if (sub.equals("status")) {
iaStatus(sender);
return true;
}
if (sub.equals("esquecer") || sub.equals("limpar")) {
iaForget(sender);
return true;
}
if (sub.equals("lembrar") && args.length >= 2) {
String fact = String.join(" ", Arrays.copyOfRange(args, 1, args.length)).trim();
if (!fact.isBlank() && plugin.playerMemory() != null) {
plugin.playerMemory().addFact(player.getUniqueId(), player.getName(), fact);
Msg.ok(sender, "Fato gravado na memória da sua IA: \"" + fact + "\"");
return true;
}
}
// Personalities: player can switch for themselves (/ia persona <nome>)
// or reset to default (/ia persona padrao).
if ((sub.equals("personalidade") || sub.equals("persona"))
&& (args.length == 1 || args[1].equalsIgnoreCase("padrao")
|| args[1].equalsIgnoreCase("reset") || args[1].equalsIgnoreCase("global")
|| Persona.isValid(args[1]))) {
iaPersona(sender, args);
return true;
}
// Toggles for the spontaneous lines. Same guard as above: only
// hijack on an explicit on/off, so "/ia eventos do servidor?"
// stays a question.
if ((sub.equals("eventos") || sub.equals("saudacao")) && args.length == 2
&& (args[1].equalsIgnoreCase("on") || args[1].equalsIgnoreCase("off"))) {
if (!sender.hasPermission("canalhandia.ia.perfil")) {
denied(sender);
return true;
}
boolean on = args[1].equalsIgnoreCase("on");
if (sub.equals("eventos")) {
plugin.settings().aiEvents(on);
Msg.ok(sender, "IA comentando eventos: " + (on ? "ligada" : "desligada") + ".");
} else {
plugin.settings().aiWelcome(on);
Msg.ok(sender, "Saudação da IA: " + (on ? "ligada" : "desligada") + ".");
}
return true;
}
if (sub.equals("feedback") && args.length >= 2 && args[1].equalsIgnoreCase("ruim")) {
iaFeedback(sender, args);
return true;
}
// default: treat all args as the question
}
if (args.length == 0) {
Msg.error(sender, Lang.tr("canalhandia.cmd.ia.uso",
Component.text(isPrivate ? "iap" : "ia")));
return true;
}
plugin.ai().ask(player, String.join(" ", args), isPrivate);
return true;
}
private void iaProfile(CommandSender sender, String[] args) {
if (!sender.hasPermission("canalhandia.ia.perfil")) {
denied(sender);
return;
}
if (args.length < 2) {
Msg.error(sender, "Uso: /ia perfil <economico|preciso> (atual: "
+ plugin.settings().aiProfile().name().toLowerCase(Locale.ROOT) + ")");
return;
}
AiProfile profile = AiProfile.byKey(args[1]);
plugin.settings().aiProfile(profile);
Msg.ok(sender, "Perfil da IA: " + profile.name().toLowerCase(Locale.ROOT)
+ (profile == AiProfile.PRECISO ? " (consulta a wiki)" : " (sem wiki, mais rápido)"));
}
/**
* {@code /ia persona [nome]} or {@code /ia personalidade [nome]}.
* Players can choose their personal AI persona or reset to server default.
* Operators can set global default with {@code /ia persona global <nome>}.
*/
private void iaPersona(CommandSender sender, String[] args) {
Player player = sender instanceof Player p ? p : null;
Persona globalDefault = plugin.settings().aiPersona();
Persona current = player != null && plugin.playerMemory() != null
? plugin.playerMemory().persona(player.getUniqueId(), globalDefault)
: globalDefault;
Persona rawPersonal = player != null && plugin.playerMemory() != null
? plugin.playerMemory().rawPersona(player.getUniqueId())
: null;
if (args.length < 2) {
Msg.header(sender, "Personalidades da IA");
for (Persona persona : Persona.values()) {
String marker = persona == current ? "» " : " ";
Msg.line(sender, marker + persona.displayName() + " (" + persona.key() + ")",
persona.description());
}
if (player != null) {
String status = rawPersonal == null
? "padrão do servidor (" + globalDefault.displayName() + ")"
: "personalizada (" + rawPersonal.displayName() + ")";
Msg.ok(sender, "Sua IA ativa: " + current.displayName() + "" + status);
Msg.ok(sender, "Para escolher: /ia persona <nome> | Para resetar: /ia persona padrao");
} else {
Msg.ok(sender, "Padrão global: " + globalDefault.displayName() + ". Uso: /ia persona <nome>");
}
return;
}
String target = args[1].toLowerCase(Locale.ROOT);
if (target.equals("padrao") || target.equals("reset") || target.equals("limpar")) {
if (player == null) {
Msg.error(sender, "Comando exclusivo para jogadores.");
return;
}
if (plugin.playerMemory() != null) {
plugin.playerMemory().resetPersona(player.getUniqueId());
}
Msg.ok(sender, "Sua IA voltou para a personalidade padrão do servidor ("
+ globalDefault.displayName() + ").");
return;
}
if (target.equals("global")) {
if (!sender.hasPermission("canalhandia.ia.perfil")) {
denied(sender);
return;
}
if (args.length < 3) {
Msg.error(sender, "Uso: /ia persona global <nome>");
return;
}
Persona p = Persona.byKey(args[2]);
if (p == null) {
Msg.error(sender, "Personalidade desconhecida. Use /ia persona para ver a lista.");
return;
}
plugin.settings().aiPersona(p);
Msg.ok(sender, "Personalidade global da IA alterada para: " + p.displayName()
+ "" + p.description());
return;
}
Persona chosen = Persona.byKey(target);
if (chosen == null) {
Msg.error(sender, "Personalidade desconhecida. Use /ia persona para ver a lista.");
return;
}
if (player != null && plugin.playerMemory() != null) {
plugin.playerMemory().setPersona(player.getUniqueId(), player.getName(), chosen);
Msg.ok(sender, "Sua personalidade da IA foi definida para: " + chosen.displayName()
+ "" + chosen.description());
} else {
if (sender.hasPermission("canalhandia.ia.perfil")) {
plugin.settings().aiPersona(chosen);
Msg.ok(sender, "Personalidade global da IA: " + chosen.displayName());
} else {
denied(sender);
}
}
}
private void iaStatus(CommandSender sender) {
if (!(sender instanceof Player player)) {
Msg.header(sender, "Status da IA (Servidor)");
Msg.line(sender, "Perguntas hoje", String.valueOf(plugin.ai().askedToday()));
Msg.line(sender, "Feedbacks negativos", String.valueOf(plugin.ai().feedbackWrong()));
Msg.line(sender, "Personalidade global", plugin.settings().aiPersona().displayName());
return;
}
Persona global = plugin.settings().aiPersona();
Persona active = plugin.playerMemory() != null
? plugin.playerMemory().persona(player.getUniqueId(), global)
: global;
Persona raw = plugin.playerMemory() != null
? plugin.playerMemory().rawPersona(player.getUniqueId())
: null;
Msg.header(sender, "Status da sua IA");
Msg.line(sender, "Personalidade", active.displayName()
+ (raw == null ? " (padrão do servidor)" : " (personalizada)"));
if (plugin.playerMemory() != null) {
String summary = plugin.playerMemory().summary(player.getUniqueId());
if (summary != null && !summary.isBlank()) {
Msg.line(sender, "Memória recente", summary);
}
List<String> facts = plugin.playerMemory().facts(player.getUniqueId());
if (!facts.isEmpty()) {
Msg.line(sender, "Fatos conhecidos", String.join("; ", facts));
}
}
Msg.ok(sender, "Para trocar de IA: /ia persona <nome> | Para limpar memória: /ia esquecer");
}
private void iaForget(CommandSender sender) {
if (!(sender instanceof Player player)) {
Msg.error(sender, "Comando exclusivo para jogadores.");
return;
}
if (plugin.playerMemory() != null) {
plugin.playerMemory().clearHistory(player.getUniqueId());
}
plugin.ai().conversations().forget(player.getUniqueId());
Msg.ok(sender, "A memória da IA sobre você foi limpa.");
}
// --- /nota and /save ----------------------------------------------------
/**
* {@code /nota <sub>} — the full note interface.
*
* <p>Two scopes. A <b>private</b> note is visible only to its author and
* everyone may write one ({@code canalhandia.nota}, default true). A
* <b>public</b> note is broadcast and readable by all, and writing one needs
* {@code canalhandia.nota.publica} (default op), so public notes stay a
* curated board rather than a graffiti wall.
*/
private boolean nota(CommandSender sender, String[] args) {
if (!plugin.settings().moduleEnabled(Module.NOTAS)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("anotações")));
return true;
}
if (!sender.hasPermission("canalhandia.nota")) {
return denied(sender);
}
if (args.length == 0) {
return notaHelp(sender);
}
String[] rest = Arrays.copyOfRange(args, 1, args.length);
switch (args[0].toLowerCase(Locale.ROOT)) {
case "add", "adicionar", "nova" -> notaAdd(sender, rest, Note.Scope.PRIVADA);
case "publica", "publico" -> notaAdd(sender, rest, Note.Scope.PUBLICA);
case "privada", "privado" -> notaAdd(sender, rest, Note.Scope.PRIVADA);
case "listar", "lista", "ls" -> notaList(sender, rest);
case "ver" -> notaShow(sender, rest);
case "buscar", "procurar" -> notaSearch(sender, rest);
case "remover", "apagar", "rm" -> notaRemove(sender, rest);
default -> notaHelp(sender);
}
return true;
}
/**
* {@code /save} — pin the spot you are standing on, privately.
*
* <p>{@code /save} and {@code /save coords} store the location with a
* generated label; {@code /save <texto>} stores it with that text. Private
* on purpose: this is the command someone types without reading help first,
* and the safe default for that is the one that cannot surprise anyone by
* broadcasting. {@code /nota publica <texto>} is the explicit way to share.
*/
private boolean saveShortcut(CommandSender sender, String[] args) {
if (!plugin.settings().moduleEnabled(Module.NOTAS)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("anotações")));
return true;
}
if (!sender.hasPermission("canalhandia.nota")) {
return denied(sender);
}
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.nota"));
return true;
}
// "/save" and "/save coords" mean the same thing: just the place. The
// word is accepted so the command is discoverable ("/save coords" is
// what people guess) without becoming a note whose text is "coords".
boolean placeOnly = args.length == 0
|| (args.length == 1 && args[0].equalsIgnoreCase("coords"));
String text = placeOnly
? "Local salvo em " + ServerState.worldLabel(player.getWorld())
: String.join(" ", args);
store(player, Note.Scope.PRIVADA, text);
return true;
}
// --- /recado ------------------------------------------------------------
/**
* {@code /recado <jogador> <texto>} — leave a line for someone who is not
* online, delivered on their next join.
*
* <p>If the recipient <em>is</em> online it is delivered immediately rather
* than queued, because queueing it would mean the person standing next to
* you reads it only after a relog.
*/
private boolean recado(CommandSender sender, String[] args) {
if (!plugin.settings().moduleEnabled(Module.RECADOS)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("recados")));
return true;
}
if (!sender.hasPermission("canalhandia.recado")) {
return denied(sender);
}
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.recado"));
return true;
}
if (args.length < 2) {
Msg.error(sender, Lang.tr("canalhandia.cmd.recado.uso"));
return true;
}
String text = Note.cleanText(String.join(" ", Arrays.copyOfRange(args, 1, args.length)));
if (text == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.recado.vazio"));
return true;
}
Player online = Bukkit.getPlayerExact(args[0]);
if (online != null) {
deliverNow(player, online, text);
return true;
}
// Offline: resolve through usercache, so a message can be left for
// anyone who has played here before.
OfflineStats.Known target = plugin.offlineStats().resolve(args[0]);
if (target == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.recado.desconhecido", Component.text(args[0])));
return true;
}
if (target.uuid().equals(player.getUniqueId().toString())) {
Msg.error(sender, Lang.tr("canalhandia.cmd.recado.mesmo"));
return true;
}
Mail.Message message = plugin.mail().send(player.getName(),
player.getUniqueId().toString(), target.uuid(), text);
if (message == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.recado.caixa-cheia",
Component.text(target.name()), Component.text(Mail.MAX_PER_RECIPIENT)));
return true;
}
Msg.ok(sender, Lang.tr("canalhandia.cmd.recado.guardado", Component.text(target.name())));
return true;
}
/** The recipient is online: say it now, to both sides. */
private void deliverNow(Player from, Player to, String text) {
to.sendMessage(Msg.tag("Recado", NamedTextColor.AQUA)
.append(Component.text(from.getName() + ": ", NamedTextColor.GRAY))
.append(Component.text(text, NamedTextColor.WHITE)));
Msg.ok(from, Lang.tr("canalhandia.cmd.recado.online", Component.text(to.getName())));
}
/** {@code /recados} — how many of your messages are still unread. */
private boolean recados(CommandSender sender) {
if (!plugin.settings().moduleEnabled(Module.RECADOS)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("recados")));
return true;
}
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.recados"));
return true;
}
int pending = plugin.mail().countFrom(player.getUniqueId().toString());
Msg.ok(sender, pending == 0
? Lang.tr("canalhandia.cmd.recados.tudo-entregue")
: Lang.tr(pending == 1 ? "canalhandia.cmd.recados.pendentes-singular"
: "canalhandia.cmd.recados.pendentes-plural",
Component.text(pending)));
return true;
}
/**
* {@code /mortes} — your recent deaths, newest first, with the comic cause
* and where it happened.
*
* <p>Your own only. Where someone died is where their stuff is; a public
* list of that is a looting guide, which is why the coords are private at
* death time too.
*/
private boolean mortes(CommandSender sender) {
if (!plugin.settings().moduleEnabled(Module.MORTES)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("mortes")));
return true;
}
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.mortes"));
return true;
}
List<DeathLog.Entry> deaths = plugin.deathLog().forPlayer(player.getUniqueId().toString());
if (deaths.isEmpty()) {
Msg.ok(sender, Lang.tr("canalhandia.cmd.mortes.nenhuma"));
return true;
}
Msg.header(sender, Lang.tr("canalhandia.cmd.mortes.cabecalho", Component.text(deaths.size())));
boolean bedrock = Platform.isBedrock(player);
for (DeathLog.Entry death : deaths) {
Component place = Component.text(death.place(), NamedTextColor.GRAY);
if (!bedrock) {
place = place.clickEvent(ClickEvent.copyToClipboard(death.coords()))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(
Lang.tr("canalhandia.cmd.mortes.copiar").color(NamedTextColor.DARK_GRAY)));
}
sender.sendMessage(Component.text(" " + death.flavor(), NamedTextColor.YELLOW)
.append(Component.text("", NamedTextColor.DARK_GRAY))
.append(place)
.append(Component.text(" " + Msg.ago(death.at()), NamedTextColor.DARK_GRAY)));
}
return true;
}
/**
* {@code /conquistas [jogador]} — the full catalogue, with the ones earned
* marked. Showing the locked ones too is the point: an achievement nobody
* can see is one nobody chases.
*
* <p>With no name it is your own list, from the recorded flags (what was
* actually announced). With a name it is that player's, computed from their
* current stats on disk, so it works for anyone the server has seen — online
* or not.
*/
private boolean conquistas(CommandSender sender, String[] args) {
if (!plugin.settings().moduleEnabled(Module.CONQUISTAS)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("conquistas")));
return true;
}
if (args.length == 0) {
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.conquistas"));
return true;
}
renderCatalogue(sender, plugin.achievements().earnedBy(player),
Platform.isBedrock(player), player.getName());
return true;
}
String wanted = String.join(" ", args);
OfflineStats.Known who = plugin.offlineStats().resolve(wanted);
if (who == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.jogador.desconhecido", Component.text(wanted)));
return true;
}
Map<String, Long> stats = plugin.offlineStats().achievementStats(UUID.fromString(who.uuid()));
if (stats == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.conquistas.sem-stats", Component.text(who.name())));
return true;
}
boolean bedrock = sender instanceof Player viewer && Platform.isBedrock(viewer);
renderCatalogue(sender, Achievement.earned(stats), bedrock, who.name());
return true;
}
/** The catalogue with earned entries ticked — shared by self and lookup. */
private void renderCatalogue(CommandSender sender, java.util.Collection<Achievement> earned,
boolean bedrock, String who) {
Msg.header(sender, Lang.tr("canalhandia.cmd.conquistas.cabecalho",
Component.text(who), Component.text(earned.size()),
Component.text(Achievement.values().length)));
// Bedrock renders "✔" as a tofu box, so it gets an ASCII marker — the
// same rule the reaction labels follow.
String tick = bedrock ? " [x] " : "";
String blank = bedrock ? " [ ] " : " · ";
for (Achievement achievement : Achievement.values()) {
boolean has = earned.contains(achievement);
sender.sendMessage(Component.text(has ? tick : blank,
has ? NamedTextColor.GREEN : NamedTextColor.DARK_GRAY)
.append(Component.text(achievement.title(),
has ? achievement.color() : NamedTextColor.GRAY))
.append(Component.text("" + achievement.description(),
NamedTextColor.DARK_GRAY)));
}
}
/**
* {@code /perfil [jogador]} — a status card: headline stats, how many titles
* earned and which are worn. Works for anyone the server has seen, online or
* not, reading the same numbers the rankings do.
*/
private boolean perfil(CommandSender sender, String[] args) {
String uuid;
String name;
if (args.length == 0) {
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.perfil.diga"));
return true;
}
uuid = player.getUniqueId().toString();
name = player.getName();
} else {
String wanted = String.join(" ", args);
OfflineStats.Known who = plugin.offlineStats().resolve(wanted);
if (who == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.jogador.desconhecido",
Component.text(wanted)));
return true;
}
uuid = who.uuid();
name = who.name();
}
UUID id = UUID.fromString(uuid);
Msg.header(sender, Lang.tr("canalhandia.cmd.perfil.cabecalho", Component.text(name)));
String summary = plugin.offlineStats().summary(id);
if (summary == null) {
Msg.line(sender, Lang.tr("canalhandia.cmd.perfil.rotulo.estatisticas"),
Lang.tr("canalhandia.cmd.perfil.sem-dados"));
} else {
// summary() prefixes the name; the header already has it, so drop it.
int colon = summary.indexOf(": ");
sender.sendMessage(Component.text(" "
+ (colon >= 0 ? summary.substring(colon + 2) : summary), NamedTextColor.GRAY));
}
Map<String, Long> stats = plugin.offlineStats().achievementStats(id);
List<Achievement> earned = stats == null ? List.of() : Achievement.earned(stats);
Msg.line(sender, Lang.tr("canalhandia.cmd.perfil.rotulo.conquistas"),
Component.text(earned.size() + "/" + Achievement.values().length
+ (earned.isEmpty() ? "" : " (" + titlesList(earned) + ")")));
Achievement worn = plugin.titles().chosenAchievement(id);
Msg.line(sender, Lang.tr("canalhandia.cmd.perfil.rotulo.titulo"),
worn == null ? Lang.tr("canalhandia.cmd.perfil.titulo.nenhum")
: Component.text(worn.title()));
return true;
}
/**
* {@code /titulo [nome|limpar]} — choose which earned title to wear in chat,
* or clear it. Only titles you have actually earned can be worn; the list
* with no argument shows exactly those, so nobody has to guess the spelling.
*/
private boolean titulo(CommandSender sender, String[] args) {
if (!plugin.settings().moduleEnabled(Module.CONQUISTAS)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.modulo.desligado", Component.text("conquistas")));
return true;
}
if (!(sender instanceof Player player)) {
Msg.error(sender, Lang.tr("canalhandia.cmd.sojogador.titulo"));
return true;
}
List<Achievement> earned = plugin.achievements().earnedBy(player);
if (args.length == 0) {
Achievement worn = plugin.titles().chosenAchievement(player.getUniqueId());
Msg.line(sender, Lang.tr("canalhandia.cmd.titulo.atual"),
worn == null ? Lang.tr("canalhandia.cmd.perfil.titulo.nenhum")
: Component.text(worn.title()));
if (earned.isEmpty()) {
Msg.error(sender, Lang.tr("canalhandia.cmd.titulo.nenhum-bloqueado"));
} else {
Msg.line(sender, Lang.tr("canalhandia.cmd.titulo.disponiveis"),
Component.text(titlesList(earned)));
sender.sendMessage(Lang.tr("canalhandia.cmd.titulo.uso").color(NamedTextColor.DARK_GRAY));
}
return true;
}
String arg = String.join(" ", args);
if (arg.equalsIgnoreCase("limpar") || arg.equalsIgnoreCase("nenhum")) {
plugin.titles().clear(player.getUniqueId());
Msg.ok(player, Lang.tr("canalhandia.cmd.titulo.removido"));
return true;
}
Achievement chosen = matchEarned(arg, earned);
if (chosen == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.titulo.nao-tem", Component.text(arg)));
return true;
}
plugin.titles().set(player.getUniqueId(), chosen);
Msg.ok(player, Lang.tr("canalhandia.cmd.titulo.definido", Component.text(chosen.title())));
return true;
}
/** Matches typed text to an earned achievement by key or (case-insensitive) title. */
static Achievement matchEarned(String text, List<Achievement> earned) {
Achievement byKey = Achievement.byKey(text);
if (byKey != null && earned.contains(byKey)) {
return byKey;
}
for (Achievement achievement : earned) {
if (achievement.title().equalsIgnoreCase(text.trim())) {
return achievement;
}
}
return null;
}
private static String titlesList(List<Achievement> earned) {
List<String> names = new ArrayList<>();
for (Achievement achievement : earned) {
names.add(achievement.title());
}
return String.join(", ", names);
}
private void notaAdd(CommandSender sender, String[] args, Note.Scope scope) {
if (!(sender instanceof Player player)) {
Msg.error(sender, "Só jogadores podem anotar (a anotação guarda onde você está).");
return;
}
if (scope == Note.Scope.PUBLICA && !sender.hasPermission("canalhandia.nota.publica")) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.publica-negado"));
return;
}
if (args.length == 0) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.uso-escopo", Component.text(scope.key())));
return;
}
store(player, scope, String.join(" ", args));
}
/** Shared tail of every create path: clean, store, confirm, broadcast. */
private void store(Player player, Note.Scope scope, String rawText) {
String text = Note.cleanText(rawText);
if (text == null) {
Msg.error(player, Lang.tr("canalhandia.cmd.nota.vazia"));
return;
}
Location at = player.getLocation();
Note note = plugin.notes().add(scope, player.getName(), player.getUniqueId().toString(),
text, ServerState.worldLabel(player.getWorld()),
at.getBlockX(), at.getBlockY(), at.getBlockZ());
if (note == null) {
Msg.error(player, Lang.tr("canalhandia.cmd.nota.cheia",
Component.text(Notes.MAX_PER_PLAYER)));
return;
}
Msg.ok(player, Lang.tr("canalhandia.cmd.nota.salva",
Component.text(note.id()), Component.text(scope.label()),
Component.text(note.place())));
if (scope == Note.Scope.PUBLICA) {
// A new public note changes what the web map should show.
plugin.blueMap().sync();
// Public notes are announced, because a board nobody is told about
// is a board nobody reads.
plugin.broadcastPerPlatform(bedrock -> Msg.tag("Nota", NamedTextColor.GREEN)
.append(Component.text(player.getName() + ": ", NamedTextColor.GRAY))
.append(noteBody(note, bedrock)));
} else {
player.sendMessage(Msg.tag("Nota", NamedTextColor.GREEN).append(noteBody(note, false)));
}
}
private void notaList(CommandSender sender, String[] args) {
Note.Scope scope = args.length > 0 ? Note.Scope.byKey(args[0]) : null;
if (args.length > 0 && scope == null) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.listar-uso"));
return;
}
Component title = scope == null
? Lang.tr("canalhandia.cmd.nota.lista.tudo")
: Lang.tr("canalhandia.cmd.nota.lista.escopo", Component.text(scope.key() + "s"));
show(sender, plugin.notes().visibleTo(viewerId(sender), scope, null), title);
}
private void notaSearch(CommandSender sender, String[] args) {
if (args.length == 0) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.buscar-uso"));
return;
}
String query = String.join(" ", args);
show(sender, plugin.notes().visibleTo(viewerId(sender), null, query),
Lang.tr("canalhandia.cmd.nota.lista.busca", Component.text(query)));
}
private void notaShow(CommandSender sender, String[] args) {
Note note = args.length == 0 ? null : plugin.notes().byId(parseLong(args[0]));
// A note the viewer cannot see is reported as missing rather than as
// forbidden: saying "that one is private" would confirm it exists, which
// is itself a leak about someone else's note.
if (note == null || !note.visibleTo(viewerId(sender))) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.nao-encontrada"));
return;
}
Msg.header(sender, Lang.tr("canalhandia.cmd.nota.cabecalho", Component.text(note.id())));
sender.sendMessage(noteBody(note, isBedrock(sender)));
Msg.line(sender, Lang.tr("canalhandia.cmd.nota.autor"), Component.text(note.author()));
Msg.line(sender, Lang.tr("canalhandia.cmd.nota.escopo"),
Component.text(note.scope().key() + " (" + note.scope().label() + ")"));
Msg.line(sender, Lang.tr("canalhandia.cmd.nota.lugar"), Component.text(note.place()));
}
private void notaRemove(CommandSender sender, String[] args) {
if (args.length == 0) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.remover-uso"));
return;
}
Note note = plugin.notes().byId(parseLong(args[0]));
if (note == null || !note.visibleTo(viewerId(sender))) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.nao-encontrada"));
return;
}
if (!note.deletableBy(viewerId(sender), sender.hasPermission(ADMIN))) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.de-outro", Component.text(note.author())));
return;
}
plugin.notes().remove(note.id());
if (note.scope() == Note.Scope.PUBLICA) {
plugin.blueMap().sync();
}
Msg.ok(sender, Lang.tr("canalhandia.cmd.nota.apagada", Component.text(note.id())));
}
private void show(CommandSender sender, List<Note> notes, Component title) {
if (notes.isEmpty()) {
Msg.error(sender, Lang.tr("canalhandia.cmd.nota.nenhuma"));
return;
}
Msg.header(sender, title.append(Component.text(" (" + notes.size() + ")")));
boolean bedrock = isBedrock(sender);
// Capped so a long list cannot push everything else out of the chat
// window; the rest are reachable with /nota buscar.
int shown = Math.min(notes.size(), 10);
for (int i = 0; i < shown; i++) {
sender.sendMessage(noteBody(notes.get(i), bedrock));
}
if (notes.size() > shown) {
Msg.line(sender, Component.text(""),
Lang.tr("canalhandia.cmd.nota.e-mais", Component.text(notes.size() - shown)));
}
}
/**
* One note as a chat line: id, scope colour, text, and the place.
*
* <p>On Java the coordinates are click-to-copy, the same affordance the
* death-coords message uses. Bedrock renders no click event, so it gets the
* same text plainly rather than a dead link.
*/
private Component noteBody(Note note, boolean bedrock) {
NamedTextColor colour = note.scope() == Note.Scope.PUBLICA
? NamedTextColor.GREEN : NamedTextColor.LIGHT_PURPLE;
Component place = Component.text(note.place(), NamedTextColor.GRAY);
if (!bedrock) {
place = place.clickEvent(ClickEvent.copyToClipboard(note.coords()))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(
Lang.tr("canalhandia.cmd.mortes.copiar").color(NamedTextColor.DARK_GRAY)));
}
return Component.text(" #" + note.id() + " ", colour)
.append(Component.text(note.text(), NamedTextColor.WHITE))
.append(Component.text("", NamedTextColor.DARK_GRAY))
.append(place);
}
private boolean notaHelp(CommandSender sender) {
Msg.header(sender, "Anotações");
Map<String, String> commands = new LinkedHashMap<>();
commands.put("/save", "salva onde você está (privado)");
commands.put("/save coords", "o mesmo, escrito por extenso");
commands.put("/save <texto>", "salva o lugar com um texto");
commands.put("/nota add <texto>", "anotação privada, só você vê");
if (sender.hasPermission("canalhandia.nota.publica")) {
commands.put("/nota publica <texto>", "anotação pública, todos veem");
}
commands.put("/nota listar [publicas|privadas]", "lista o que você pode ver");
commands.put("/nota buscar <texto>", "procura no texto das anotações");
commands.put("/nota ver <n>", "mostra uma anotação inteira");
commands.put("/nota remover <n>", "apaga uma anotação sua");
commands.forEach((cmd, description) -> sender.sendMessage(
Component.text(" " + cmd, NamedTextColor.AQUA)
.append(Component.text("" + description, NamedTextColor.GRAY))));
return true;
}
/** The viewer's UUID as stored on notes, or null for the console. */
private String viewerId(CommandSender sender) {
return sender instanceof Player player ? player.getUniqueId().toString() : null;
}
private boolean isBedrock(CommandSender sender) {
return sender instanceof Player player && Platform.isBedrock(player);
}
/** Parses a note id, returning -1 (never a valid id) on junk input. */
private long parseLong(String raw) {
try {
return Long.parseLong(raw.trim().replace("#", ""));
} catch (NumberFormatException e) {
return -1;
}
}
private void iaCorrect(CommandSender sender, String[] args) {
if (!sender.hasPermission("canalhandia.ia.corrigir")) {
denied(sender);
return;
}
Ai.Answered last = plugin.ai().lastAnswer();
if (last == null) {
Msg.error(sender, "Nenhuma resposta recente para corrigir.");
return;
}
if (args.length < 2) {
Msg.error(sender, "Uso: /ia corrigir <resposta correta>");
return;
}
String correct = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
plugin.ai().corrections().add(last.question(), correct);
Msg.ok(sender, "Correção registrada para perguntas parecidas com: " + last.question());
}
private void iaFeedback(CommandSender sender, String[] args) {
if (args.length < 2 || !args[1].equalsIgnoreCase("ruim")) {
Msg.error(sender, "Uso: /ia feedback ruim (marca a última resposta como errada)");
return;
}
if (plugin.ai().flagLastAnswerWrong()) {
Msg.ok(sender, "Obrigado pelo feedback. Um operador vai revisar.");
} else {
Msg.error(sender, "Nenhuma resposta recente para marcar.");
}
}
private boolean admin(CommandSender sender) {
if (sender.hasPermission(ADMIN)) {
return true;
}
denied(sender);
return false;
}
private boolean denied(CommandSender sender) {
Msg.error(sender, Lang.tr("canalhandia.cmd.negado"));
return true;
}
// --- /chunkloader -------------------------------------------------------
private boolean chunkloader(CommandSender sender, String[] args) {
if (!plugin.settings().moduleEnabled(Module.CHUNKLOADER)) {
Msg.error(sender, "O módulo de Âncoras de Chunk está desativado.");
return true;
}
if (!sender.hasPermission("canalhandia.chunkloader")) {
return denied(sender);
}
if (args.length == 0) {
return chunkloaderHelp(sender);
}
String sub = args[0].toLowerCase(Locale.ROOT);
String[] rest = Arrays.copyOfRange(args, 1, args.length);
switch (sub) {
case "listar", "list" -> chunkloaderList(sender, rest);
case "info", "chunk" -> chunkloaderInfo(sender);
case "remover", "remove", "del" -> chunkloaderRemove(sender, rest);
case "receita", "craft" -> chunkloaderRecipe(sender);
case "limite", "limit" -> chunkloaderLimit(sender, rest);
case "nome", "renomear", "rename" -> chunkloaderRename(sender, rest);
case "ativar", "enable" -> chunkloaderSetEnabled(sender, rest, true);
case "desativar", "pausar", "disable", "pause" -> chunkloaderSetEnabled(sender, rest, false);
case "admin" -> chunkloaderAdmin(sender, rest);
case "dar", "give" -> {
if (admin(sender)) {
chunkloaderGive(sender, rest);
}
}
default -> chunkloaderHelp(sender);
}
return true;
}
private void chunkloaderList(CommandSender sender, String[] args) {
if (!(sender instanceof Player player)) {
chunkloaderAdminList(sender, args.length > 0 ? args[0] : null);
return;
}
List<ChunkLoader> mine = plugin.chunkLoaders().byOwner(player.getUniqueId().toString());
int limit = ChunkLoaders.playerLimit(player, plugin.settings().chunkLoaderDefaultLimit());
String limitStr = limit == Integer.MAX_VALUE ? "ilimitado" : String.valueOf(limit);
Msg.header(sender, "Suas Âncoras de Chunk (" + mine.size() + "/" + limitStr + ")");
if (mine.isEmpty()) {
sender.sendMessage(Component.text(" Você não possui nenhuma Âncora de Chunk ativa.", NamedTextColor.GRAY));
sender.sendMessage(Component.text(" Use ", NamedTextColor.GRAY)
.append(Component.text("/chunkloader receita", NamedTextColor.AQUA))
.append(Component.text(" para ver como criar uma.", NamedTextColor.GRAY)));
return;
}
boolean bedrock = isBedrock(sender);
for (ChunkLoader loader : mine) {
String statusText = loader.enabled() ? "[Ativa]" : "[Pausada]";
NamedTextColor statusColor = loader.enabled() ? NamedTextColor.GREEN : NamedTextColor.RED;
Component line = Component.text(" " + loader.displayName() + " ", NamedTextColor.AQUA)
.append(Component.text(loader.place() + " " + loader.chunkCoords() + " ", NamedTextColor.WHITE))
.append(Component.text(statusText + " ", statusColor));
if (!bedrock) {
if (loader.enabled()) {
line = line.append(Component.text("[Pausar] ", NamedTextColor.YELLOW)
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(
Component.text("Clique para pausar " + loader.displayName(), NamedTextColor.GRAY)))
.clickEvent(net.kyori.adventure.text.event.ClickEvent.runCommand(
"/chunkloader desativar " + loader.id())));
} else {
line = line.append(Component.text("[Ativar] ", NamedTextColor.GREEN)
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(
Component.text("Clique para reativar " + loader.displayName(), NamedTextColor.GRAY)))
.clickEvent(net.kyori.adventure.text.event.ClickEvent.runCommand(
"/chunkloader ativar " + loader.id())));
}
line = line.append(Component.text("[Remover]", NamedTextColor.RED)
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(
Component.text("Clique para recolher " + loader.displayName(), NamedTextColor.GRAY)))
.clickEvent(net.kyori.adventure.text.event.ClickEvent.suggestCommand(
"/chunkloader remover " + loader.id())));
}
sender.sendMessage(line);
}
}
private void chunkloaderInfo(CommandSender sender) {
if (!(sender instanceof Player player)) {
Msg.error(sender, "Comando exclusivo para jogadores.");
return;
}
Location loc = player.getLocation();
String world = loc.getWorld().getName();
int cx = loc.getBlockX() >> 4;
int cz = loc.getBlockZ() >> 4;
ChunkLoader here = plugin.chunkLoaders().byChunk(world, cx, cz);
int limit = ChunkLoaders.playerLimit(player, plugin.settings().chunkLoaderDefaultLimit());
int used = plugin.chunkLoaders().byOwner(player.getUniqueId().toString()).size();
String limitStr = limit == Integer.MAX_VALUE ? "ilimitado" : String.valueOf(limit);
Msg.header(sender, "Informações da Chunk");
sender.sendMessage(Component.text(" Chunk atual: ", NamedTextColor.GRAY)
.append(Component.text("[" + cx + ", " + cz + "]", NamedTextColor.AQUA))
.append(Component.text(" em " + world, NamedTextColor.WHITE)));
if (here != null) {
String status = here.enabled() ? "Ativa" : "Pausada";
NamedTextColor statusCol = here.enabled() ? NamedTextColor.GREEN : NamedTextColor.RED;
sender.sendMessage(Component.text(" Status: ", NamedTextColor.GRAY)
.append(Component.text(here.displayName() + " (" + status + ")", statusCol))
.append(Component.text(" de " + here.ownerName(), NamedTextColor.WHITE)));
} else {
sender.sendMessage(Component.text(" Status: ", NamedTextColor.GRAY)
.append(Component.text("Carregamento normal (sem âncora)", NamedTextColor.DARK_GRAY)));
}
sender.sendMessage(Component.text(" Suas âncoras ativas: ", NamedTextColor.GRAY)
.append(Component.text(used + "/" + limitStr, NamedTextColor.YELLOW)));
}
private void chunkloaderRename(CommandSender sender, String[] args) {
if (args.length < 2) {
Msg.error(sender, "Uso: /chunkloader nome <id|nome_antigo> <novo_nome>");
return;
}
ChunkLoader loader = plugin.chunkLoaders().find(args[0]);
if (loader == null) {
Msg.error(sender, "Nenhuma âncora encontrada para '" + args[0] + "'.");
return;
}
boolean isOwner = sender instanceof Player player && player.getUniqueId().toString().equalsIgnoreCase(loader.ownerUuid());
boolean isAdmin = sender.hasPermission(ADMIN) || sender.hasPermission("canalhandia.chunkloader.admin");
if (!isOwner && !isAdmin) {
Msg.error(sender, "Esta âncora pertence a " + loader.ownerName() + ".");
return;
}
String newName = String.join(" ", Arrays.copyOfRange(args, 1, args.length)).trim();
if (newName.length() > 32) {
Msg.error(sender, "Nome muito longo (máximo 32 caracteres).");
return;
}
plugin.chunkLoaders().rename(loader.id(), newName);
plugin.blueMap().syncChunkLoaders();
Msg.ok(sender, "Âncora #" + loader.id() + " renomeada para '" + newName + "'.");
}
private void chunkloaderSetEnabled(CommandSender sender, String[] args, boolean enabled) {
if (args.length < 1) {
Msg.error(sender, "Uso: /chunkloader " + (enabled ? "ativar" : "desativar") + " <id|nome>");
return;
}
ChunkLoader loader = plugin.chunkLoaders().find(args[0]);
if (loader == null) {
Msg.error(sender, "Nenhuma âncora encontrada para '" + args[0] + "'.");
return;
}
boolean isOwner = sender instanceof Player player && player.getUniqueId().toString().equalsIgnoreCase(loader.ownerUuid());
boolean isAdmin = sender.hasPermission(ADMIN) || sender.hasPermission("canalhandia.chunkloader.admin");
if (!isOwner && !isAdmin) {
Msg.error(sender, "Esta âncora pertence a " + loader.ownerName() + ".");
return;
}
if (loader.enabled() == enabled) {
Msg.error(sender, "A âncora " + loader.displayName() + " já está " + (enabled ? "ativa" : "pausada") + ".");
return;
}
plugin.chunkLoaders().setEnabled(loader.id(), enabled);
plugin.blueMap().syncChunkLoaders();
Msg.ok(sender, "Âncora " + loader.displayName() + " " + (enabled ? "ATIVADA." : "PAUSADA/DESATIVADA."));
}
private void chunkloaderRemove(CommandSender sender, String[] args) {
if (args.length < 1) {
Msg.error(sender, "Uso: /chunkloader remover <id|nome>");
return;
}
ChunkLoader loader = plugin.chunkLoaders().find(args[0]);
if (loader == null) {
Msg.error(sender, "Nenhuma Âncora de Chunk encontrada para '" + args[0] + "'.");
return;
}
long id = loader.id();
boolean isOwner = sender instanceof Player player && player.getUniqueId().toString().equalsIgnoreCase(loader.ownerUuid());
boolean isAdmin = sender.hasPermission(ADMIN) || sender.hasPermission("canalhandia.chunkloader.admin");
if (!isOwner && !isAdmin) {
Msg.error(sender, "Esta âncora pertence a " + loader.ownerName() + ".");
return;
}
plugin.chunkLoaders().remove(id);
plugin.blueMap().syncChunkLoaders();
try {
org.bukkit.World w = Bukkit.getWorld(loader.world());
if (w != null && w.isChunkLoaded(loader.chunkX(), loader.chunkZ())) {
Block b = w.getBlockAt(loader.x(), loader.y(), loader.z());
if (b.getType() == Material.RESPAWN_ANCHOR) {
b.setType(Material.AIR);
}
}
} catch (Exception ignored) {
}
if (sender instanceof Player player) {
Map<Integer, ItemStack> overflow = player.getInventory().addItem(ChunkAnchorItem.create(plugin, 1));
for (ItemStack drop : overflow.values()) {
player.getWorld().dropItemNaturally(player.getLocation(), drop);
}
Msg.ok(sender, "Âncora de Chunk " + loader.displayName() + " desativada e devolvida ao inventário.");
} else {
try {
org.bukkit.World w = Bukkit.getWorld(loader.world());
if (w != null) {
w.dropItemNaturally(new Location(w, loader.x() + 0.5, loader.y() + 0.5, loader.z() + 0.5),
ChunkAnchorItem.create(plugin, 1));
}
} catch (Exception ignored) {
}
Msg.ok(sender, "Âncora de Chunk " + loader.displayName() + " desativada e dropada em " + loader.place() + ".");
}
}
private void chunkloaderRecipe(CommandSender sender) {
Msg.header(sender, "Receita da Âncora de Chunk");
sender.sendMessage(Component.text(" Na Bancada de Trabalho:", NamedTextColor.GRAY));
sender.sendMessage(Component.text(" [ D ] [ O ] [ D ]", NamedTextColor.AQUA));
sender.sendMessage(Component.text(" [ O ] [ E ] [ O ]", NamedTextColor.LIGHT_PURPLE));
sender.sendMessage(Component.text(" [ D ] [ O ] [ D ]", NamedTextColor.AQUA));
sender.sendMessage(Component.text(" D = Diamante | O = Obsidiana Chorona | E = Olho do Fim", NamedTextColor.YELLOW));
}
private void chunkloaderAdmin(CommandSender sender, String[] args) {
if (!admin(sender)) {
return;
}
if (args.length == 0) {
Msg.header(sender, "Admin Chunk Loaders");
sender.sendMessage(Component.text(" /chunkloader admin listar [jogador]", NamedTextColor.AQUA));
sender.sendMessage(Component.text(" /chunkloader admin remover <id>", NamedTextColor.AQUA));
sender.sendMessage(Component.text(" /chunkloader dar <jogador> [quantidade]", NamedTextColor.AQUA));
return;
}
switch (args[0].toLowerCase(Locale.ROOT)) {
case "listar", "list" -> chunkloaderAdminList(sender, args.length > 1 ? args[1] : null);
case "remover", "del" -> {
if (args.length < 2) {
Msg.error(sender, "Uso: /chunkloader admin remover <id>");
return;
}
chunkloaderRemove(sender, new String[]{args[1]});
}
case "dar", "give" -> chunkloaderGive(sender, Arrays.copyOfRange(args, 1, args.length));
default -> Msg.error(sender, "Subcomando de admin desconhecido.");
}
}
private void chunkloaderAdminList(CommandSender sender, String targetName) {
List<ChunkLoader> all = plugin.chunkLoaders().all();
if (targetName != null && !targetName.isBlank()) {
all = all.stream().filter(l -> l.ownerName().equalsIgnoreCase(targetName)).toList();
}
Msg.header(sender, "Todas as Âncoras do Servidor (" + all.size() + ")");
if (all.isEmpty()) {
sender.sendMessage(Component.text(" Nenhuma âncora ativa encontrada.", NamedTextColor.GRAY));
return;
}
for (ChunkLoader loader : all) {
sender.sendMessage(Component.text(" #" + loader.id() + " ", NamedTextColor.AQUA)
.append(Component.text(loader.ownerName(), NamedTextColor.YELLOW))
.append(Component.text("" + loader.place() + " " + loader.chunkCoords(), NamedTextColor.WHITE)));
}
}
private void chunkloaderGive(CommandSender sender, String[] args) {
if (args.length < 1) {
Msg.error(sender, "Uso: /chunkloader dar <jogador> [quantidade]");
return;
}
Player target = Bukkit.getPlayer(args[0]);
if (target == null) {
Msg.error(sender, "Jogador não encontrado: " + args[0]);
return;
}
int amount = 1;
if (args.length >= 2) {
try {
amount = Math.max(1, Integer.parseInt(args[1]));
} catch (NumberFormatException ignored) {
}
}
target.getInventory().addItem(ChunkAnchorItem.create(plugin, amount));
Msg.ok(sender, "Entregue " + amount + "x Âncora de Chunk para " + target.getName() + ".");
Msg.ok(target, "Você recebeu " + amount + "x Âncora de Chunk!");
}
private void chunkloaderLimit(CommandSender sender, String[] args) {
if (args.length == 0) {
int defaultLimit = plugin.settings().chunkLoaderDefaultLimit();
Msg.header(sender, "Limite de Âncoras de Chunk");
sender.sendMessage(Component.text(" Limite padrão global: ", NamedTextColor.GRAY)
.append(Component.text(defaultLimit, NamedTextColor.YELLOW)));
if (sender instanceof Player player) {
int myLimit = ChunkLoaders.playerLimit(player, defaultLimit);
String myLimitStr = myLimit == Integer.MAX_VALUE ? "ilimitado" : String.valueOf(myLimit);
int current = plugin.chunkLoaders().byOwner(player.getUniqueId().toString()).size();
sender.sendMessage(Component.text(" Seu limite atual: ", NamedTextColor.GRAY)
.append(Component.text(current + "/" + myLimitStr, NamedTextColor.AQUA)));
}
if (admin(sender)) {
sender.sendMessage(Component.text(" Alterar padrão global: ", NamedTextColor.GRAY)
.append(Component.text("/chunkloader limite padrao <quantidade>", NamedTextColor.YELLOW)));
sender.sendMessage(Component.text(" Definir por jogador (LuckPerms): ", NamedTextColor.GRAY)
.append(Component.text("/chunkloader limite <jogador> <quantidade>", NamedTextColor.YELLOW)));
}
return;
}
if (!admin(sender)) {
return;
}
if (args[0].equalsIgnoreCase("padrao") || args[0].equalsIgnoreCase("default")) {
if (args.length < 2) {
Msg.error(sender, "Uso: /chunkloader limite padrao <quantidade>");
return;
}
try {
int newLimit = Math.max(0, Integer.parseInt(args[1]));
plugin.settings().chunkLoaderDefaultLimit(newLimit);
Msg.ok(sender, "Limite padrão global de Âncoras alterado para " + newLimit + ".");
} catch (NumberFormatException e) {
Msg.error(sender, "Quantidade inválida: " + args[1]);
}
return;
}
Player target = Bukkit.getPlayerExact(args[0]);
if (target == null) {
Msg.error(sender, "Jogador '" + args[0] + "' não encontrado online.");
return;
}
if (args.length < 2) {
int targetLimit = ChunkLoaders.playerLimit(target, plugin.settings().chunkLoaderDefaultLimit());
String str = targetLimit == Integer.MAX_VALUE ? "ilimitado" : String.valueOf(targetLimit);
int current = plugin.chunkLoaders().byOwner(target.getUniqueId().toString()).size();
Msg.ok(sender, "Limite de " + target.getName() + ": " + current + "/" + str + " âncoras ativas.");
return;
}
try {
int newLimit = Math.max(0, Integer.parseInt(args[1]));
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "lp user " + target.getName() + " permission set canalhandia.chunkloader.limite." + newLimit);
Msg.ok(sender, "Limite de " + target.getName() + " definido para " + newLimit + " âncoras.");
} catch (NumberFormatException e) {
Msg.error(sender, "Quantidade inválida: " + args[1]);
}
}
private boolean chunkloaderHelp(CommandSender sender) {
Msg.header(sender, "Âncoras de Chunk");
sender.sendMessage(Component.text(" /chunkloader info", NamedTextColor.AQUA)
.append(Component.text(" — Informações sobre a chunk onde você está", NamedTextColor.GRAY)));
sender.sendMessage(Component.text(" /chunkloader listar", NamedTextColor.AQUA)
.append(Component.text(" — Lista suas âncoras, nomes, status e coordenadas", NamedTextColor.GRAY)));
sender.sendMessage(Component.text(" /chunkloader nome <id|nome> <novo_nome>", NamedTextColor.AQUA)
.append(Component.text(" — Define um nome/rótulo para a âncora", NamedTextColor.GRAY)));
sender.sendMessage(Component.text(" /chunkloader ativar <id|nome>", NamedTextColor.AQUA)
.append(Component.text(" — Reativa o carregamento contínuo da âncora", NamedTextColor.GRAY)));
sender.sendMessage(Component.text(" /chunkloader desativar <id|nome>", NamedTextColor.AQUA)
.append(Component.text(" — Pausa o carregamento sem quebrar o bloco", NamedTextColor.GRAY)));
sender.sendMessage(Component.text(" /chunkloader remover <id|nome>", NamedTextColor.AQUA)
.append(Component.text(" — Desativa e recolhe uma de suas âncoras", NamedTextColor.GRAY)));
sender.sendMessage(Component.text(" /chunkloader receita", NamedTextColor.AQUA)
.append(Component.text(" — Mostra a receita de criação do bloco", NamedTextColor.GRAY)));
sender.sendMessage(Component.text(" /chunkloader limite", NamedTextColor.AQUA)
.append(Component.text(" — Mostra ou configura os limites de âncoras", NamedTextColor.GRAY)));
if (sender.hasPermission(ADMIN) || sender.hasPermission("canalhandia.chunkloader.admin")) {
sender.sendMessage(Component.text(" /chunkloader admin", NamedTextColor.YELLOW)
.append(Component.text(" — Comandos de administração", NamedTextColor.GRAY)));
}
return true;
}
// --- tab completion -----------------------------------------------------
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String label, String[] args) {
String name = command.getName().toLowerCase();
if (name.equals("ranking") && args.length == 1) {
List<String> keys = new ArrayList<>();
for (RankingMetric metric : RankingMetric.values()) {
keys.add(metric.commandKey());
}
return filter(keys, args[0]);
}
if (name.equals("enquete") && args.length == 1) {
return filter(List.of("encerrar"), args[0]);
}
if (name.equals("nota")) {
if (args.length == 1) {
List<String> options = new ArrayList<>(
List.of("add", "listar", "buscar", "ver", "remover"));
if (sender.hasPermission("canalhandia.nota.publica")) {
options.add("publica");
}
return filter(options, args[0]);
}
if (args.length == 2 && args[0].equalsIgnoreCase("listar")) {
return filter(List.of("publicas", "privadas"), args[1]);
}
// Completing note ids for ver/remover: only the ones this player is
// allowed to see, so completion cannot enumerate someone else's.
if (args.length == 2
&& (args[0].equalsIgnoreCase("ver") || args[0].equalsIgnoreCase("remover"))) {
List<String> ids = new ArrayList<>();
for (Note note : plugin.notes().visibleTo(viewerId(sender), null, null)) {
ids.add(String.valueOf(note.id()));
}
return filter(ids, args[1]);
}
return List.of();
}
if (name.equals("recado") && args.length == 1) {
// Everyone the server has seen, not just who is online — the whole
// point is leaving a message for someone who is not here.
return filter(plugin.offlineStats().knownNames(), args[0]);
}
if (name.equals("save")) {
return args.length == 1 ? filter(List.of("coords"), args[0]) : List.of();
}
if ((name.equals("perfil") || name.equals("conquistas")) && args.length == 1) {
// Any player the server has seen — these commands read off disk.
return filter(plugin.offlineStats().knownNames(), args[0]);
}
if (name.equals("titulo") && args.length == 1) {
// Only the titles this player has actually earned, plus the clear
// word — completion should never suggest a title you cannot wear.
List<String> options = new ArrayList<>();
options.add("limpar");
if (sender instanceof Player player) {
for (Achievement achievement : plugin.achievements().earnedBy(player)) {
options.add(achievement.title());
}
}
return filter(options, args[0]);
}
if (name.equals("ia") || name.equals("iap")) {
if (args.length == 1) {
List<String> subs = new ArrayList<>(List.of("persona", "personalidade", "status", "lembrar", "esquecer"));
if (sender.hasPermission("canalhandia.ia.perfil")) {
subs.addAll(List.of("perfil", "eventos", "saudacao", "corrigir"));
}
return filter(subs, args[0]);
}
if (args.length == 2 && (args[0].equalsIgnoreCase("personalidade") || args[0].equalsIgnoreCase("persona"))) {
List<String> keys = new ArrayList<>();
keys.add("padrao");
for (Persona persona : Persona.values()) {
keys.add(persona.key());
}
if (sender.hasPermission("canalhandia.ia.perfil")) {
keys.add("global");
}
return filter(keys, args[1]);
}
if (args.length == 3 && (args[0].equalsIgnoreCase("personalidade") || args[0].equalsIgnoreCase("persona"))
&& args[1].equalsIgnoreCase("global") && sender.hasPermission("canalhandia.ia.perfil")) {
List<String> keys = new ArrayList<>();
for (Persona persona : Persona.values()) {
keys.add(persona.key());
}
return filter(keys, args[2]);
}
if (args.length == 2 && args[0].equalsIgnoreCase("perfil") && sender.hasPermission("canalhandia.ia.perfil")) {
return filter(List.of("economico", "preciso"), args[1]);
}
if (args.length == 2 && (args[0].equalsIgnoreCase("eventos")
|| args[0].equalsIgnoreCase("saudacao")) && sender.hasPermission("canalhandia.ia.perfil")) {
return filter(List.of("on", "off"), args[1]);
}
return List.of();
}
if (name.equals("canalhandia")) {
if (args.length == 1) {
List<String> options = new ArrayList<>(List.of("status", "modulos", "plataformas"));
if (sender.hasPermission(ADMIN)) {
options.addAll(List.of("modulo", "marcos", "limpar", "zoacao", "reload"));
}
return filter(options, args[0]);
}
if (args.length == 2 && args[0].equalsIgnoreCase("modulo")) {
List<String> keys = new ArrayList<>();
for (Module module : Module.values()) {
keys.add(module.key());
}
return filter(keys, args[1]);
}
if (args.length == 2 && args[0].equalsIgnoreCase("limpar")) {
return filter(List.of("cooldown", "historico", "tudo"), args[1]);
}
if (args.length == 2 && args[0].equalsIgnoreCase("zoacao")) {
return filter(List.of("listar", "modo", "padrao", "adicionar", "remover", "limpar"), args[1]);
}
if (args.length == 3 && args[0].equalsIgnoreCase("zoacao") && args[1].equalsIgnoreCase("modo")) {
return filter(List.of("igual", "contem", "comeca", "termina", "regex"), args[2]);
}
if (args.length == 3 && args[0].equalsIgnoreCase("modulo")) {
return filter(List.of("on", "off"), args[2]);
}
return List.of();
}
if (name.equals("curiosidade")) {
if (args.length == 1) {
List<String> options = new ArrayList<>(List.of("ver", "listar", "toggle",
"status", "categorias", "ajuda"));
if (sender.hasPermission(ADMIN)) {
options.addAll(List.of("modo", "intervalo", "atraso", "cooldown", "repetir",
"janela", "validade", "resumo", "minimo", "reacoes", "reacao",
"categoria"));
}
options.addAll(onlineNames());
return filter(options, args[0]);
}
if (args.length == 2) {
return switch (args[0].toLowerCase()) {
case "modo" -> filter(List.of("entrada", "intervalo", "ambos", "manual"), args[1]);
case "reacoes" -> filter(List.of("on", "off"), args[1]);
case "reacao" -> filter(List.of("add", "remover"), args[1]);
case "categoria" -> {
List<String> keys = new ArrayList<>();
for (Category category : Category.values()) {
keys.add(category.key());
}
yield filter(keys, args[1]);
}
case "ver", "listar" -> filter(onlineNames(), args[1]);
default -> List.of();
};
}
if (args.length == 3 && args[0].equalsIgnoreCase("categoria")) {
return filter(List.of("on", "off"), args[2]);
}
}
if (name.equals("chunkloader") || name.equals("ancora") || name.equals("ancoras")) {
if (args.length == 1) {
List<String> options = new ArrayList<>(List.of("info", "listar", "nome", "ativar", "desativar", "remover", "receita", "limite"));
if (sender.hasPermission(ADMIN) || sender.hasPermission("canalhandia.chunkloader.admin")) {
options.addAll(List.of("admin", "dar"));
}
return filter(options, args[0]);
}
if (args.length == 2 && (args[0].equalsIgnoreCase("remover") || args[0].equalsIgnoreCase("del")
|| args[0].equalsIgnoreCase("nome") || args[0].equalsIgnoreCase("renomear")
|| args[0].equalsIgnoreCase("ativar") || args[0].equalsIgnoreCase("desativar")
|| args[0].equalsIgnoreCase("pausar"))) {
if (sender instanceof Player player) {
List<String> identifiers = new ArrayList<>();
for (ChunkLoader loader : plugin.chunkLoaders().byOwner(player.getUniqueId().toString())) {
identifiers.add(String.valueOf(loader.id()));
if (loader.name() != null && !loader.name().isBlank()) {
identifiers.add(loader.name());
}
}
return filter(identifiers, args[1]);
}
}
if (args.length == 2 && args[0].equalsIgnoreCase("limite")) {
List<String> opts = new ArrayList<>(List.of("padrao"));
opts.addAll(onlineNames());
return filter(opts, args[1]);
}
if (args.length == 2 && args[0].equalsIgnoreCase("admin")) {
return filter(List.of("listar", "remover", "dar"), args[1]);
}
if (args.length == 2 && args[0].equalsIgnoreCase("dar")) {
return filter(onlineNames(), args[1]);
}
if (args.length == 3 && args[0].equalsIgnoreCase("admin") && args[1].equalsIgnoreCase("dar")) {
return filter(onlineNames(), args[2]);
}
}
return List.of();
}
private List<String> onlineNames() {
List<String> names = new ArrayList<>();
Bukkit.getOnlinePlayers().forEach(p -> names.add(p.getName()));
return names;
}
private static List<String> filter(List<String> options, String prefix) {
List<String> matches = new ArrayList<>();
for (String option : options) {
if (option.toLowerCase().startsWith(prefix.toLowerCase())) {
matches.add(option);
}
}
return matches;
}
}