2416 lines
113 KiB
Java
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;
|
|
}
|
|
}
|