marcos c1a6b9730f docs: document the IA module
README: add ia to the modules table, the four ia permissions, and a full
IA section covering commands, profile switching, operator corrections,
memory/context, and the key + rate-limit model. Notes the safety boundary
(no tools, reply never executed, leading slashes stripped) and which config
values are baked vs live.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 03:56:58 +00:00
2026-08-06 03:56:58 +00:00

Canalhandia

Chat-only social features for the Canalhandia Minecraft server (Paper 26.2).

Nothing here touches gameplay. No items, no world edits, no attributes, no economy. Everything is chat messages, boss bars and clickable buttons, and every module can be switched off independently.

All player-facing text is Portuguese (pt-BR).


Modules

Module What it does
curiosidades "Sabia que o Fulano já minerou 5.966 blocos de Pedra?" — a fact about a player, with reaction buttons. Fires on join by default.
adivinha The same fact with the name hidden, plus clickable player names. Reveals after 45s and names who guessed right.
luto A clickable [F] under each death message, with a count when the window closes.
enquete /enquete Pergunta | A | B — clickable voting with a live tally on the boss bar.
ranking /ranking mineracao and friends. Covers offline players too.
marcos Announces round milestones — 100 km walked, 24 hours played — the first time someone crosses one.
ia /ia <pergunta> asks an OpenAI-compatible model in chat. Optional wiki grounding, per-player memory, operator corrections.

Toggle any of them: /canalhandia modulo <nome> <on|off>


Where the data comes from

Everything is derived from vanilla statistics. No database, no tracking code, no extra writes — the server was already recording all of it.

  • Online players use the Bukkit API (Player#getStatistic).
  • Rankings read <world>/players/stats/<uuid>.json directly, because Bukkit only exposes statistics for players who are online. usercache.json maps those UUIDs back to names, including Floodgate/Bedrock players whose UUIDs start with 00000000-0000-0000-0009.

Note the path: Paper writes to <world>/players/stats, not <world>/stats. OfflineStats checks both.


Two constraints worth knowing

These shaped the design, and anyone changing the code should know them before "fixing" what looks odd.

1. Chat messages cannot be edited after sending

There is no vanilla way to update a message that is already in the chat log. So the counts baked into the reaction buttons are frozen at send time and never change. The live numbers appear on three other surfaces instead:

  • a boss bar while the window is open (janela-reacao-segundos, default 90)
  • an action bar shown to whoever just reacted
  • a final tally line broadcast when the window closes

An earlier version only had the boss bar, and it read as broken — the buttons showed no number at all.

2. Who reacted, without filling the screen

Naming every reactor on its own chat line does not scale — five people reacting to five reactions is a wall of text. So the names live in three progressively larger places:

  • hover tooltip on each button (Java only — Bedrock cannot hover)
  • one-line closing summary naming the first resumo-nomes (default 3) and collapsing the rest into +N
  • /reacoes for the full breakdown, sent privately so chat stays clean

The same cap applies to the mourning line, so a popular death is still one line.

Relatedly, a curiosity costs one chat message, not two — the button row is appended to the headline rather than broadcast separately.

And automatic curiosities are rate-limited by intervalo-minimo-segundos (default 120). Without it, five people joining together produced five curiosities back to back. A typed /curiosidade bypasses the limit.

3. Clicks arrive late

People scroll back and click minutes after a message. Reactions therefore keep counting for reacao-validade-minutos (default 15) even after the boss bar is gone, and the last 8 reaction sets stay in memory for that reason. Silently dropping a late click looks like a bug to the player.

4. Bedrock cannot click, and cannot show emoji

Geyser cannot deliver a chat clickEvent to a Bedrock client, and most emoji render as tofu boxes there. So every clickable interaction has a typed equivalent, and every reaction carries two labels:

reacoes:
  uau:
    java: "[😮]"     # Java clients
    texto: "[UAU]"   # Bedrock clients — ASCII only
    comando: "wow"   # what a Bedrock player types

Messages with buttons are therefore built twice and sent per player (broadcastPerPlatform), not via Bukkit.broadcast. Typed fallbacks: /reagir <chave>, /legal, /wow, /top, /f, /palpite <nome>, /votar <número> — all of which act on the most recent message, so the player never needs a message id.

Changing a comando to a new name also requires adding that command to plugin.yml and restarting; Bukkit commands are static.

Detecting Bedrock: Floodgate mints UUIDs whose high 64 bits are zero (00000000-0000-0000-0009-…), so Platform.isBedrock checks getUniqueId().getMostSignificantBits() == 0. That keeps Floodgate an optional runtime dependency rather than a compile-time one. /canalhandia plataformas lists who is online and on which platform.

5. Names are translated by the client, not by us

Block, item and mob names are emitted as translatable components (Component.translatable(material.translationKey())), so a pt-BR client renders "Pedra" and an en-US client renders "Stone" from the same broadcast. There is no translation table to maintain.

The consequence: the client only supplies the singular form. Every sentence is therefore phrased so the number never has to agree with the noun — "5.966 blocos de Pedra", never "5.966 Pedras". Keep that rule when adding sentences to CuriosityFactory.

Note that the server console renders translatable components in English, so [Curiosidade] ... 16 unidades de Copper Pickaxe in latest.log does not mean players saw English.


Commands

Player-facing:

/curiosidade                       anuncia uma curiosidade agora
/curiosidade <jogador>             anuncia sobre alguém específico
/curiosidade ver [jogador]         mostra só para você
/curiosidade listar [jogador]      lista todas as curiosidades disponíveis
/curiosidade toggle                entra/sai do sorteio
/adivinha                          inicia uma rodada de "adivinhe de quem é"
/enquete Pergunta | A | B          abre uma enquete
/enquete encerrar                  encerra a enquete aberta
/ranking [categoria]               placares do servidor
/canalhandia status                mostra toda a configuração
/canalhandia modulos               lista os módulos e seu estado

Admin (canalhandia.admin):

/canalhandia modulo <nome> <on|off>       liga/desliga um módulo
/canalhandia marcos                       força uma verificação de marcos
/canalhandia limpar [cooldown|historico|tudo]
/canalhandia reload
/curiosidade modo <entrada|intervalo|ambos|manual>
/curiosidade intervalo <min>              intervalo do modo temporizado
/curiosidade atraso <seg>                 espera após o jogador entrar
/curiosidade cooldown <min>               mínimo entre citar o mesmo jogador
/curiosidade repetir <n>                  quantas recentes evitar repetir
/curiosidade janela <seg>                 duração da barra de reações
/curiosidade validade <min>               por quanto tempo cliques ainda contam
/curiosidade reacoes <on|off>
/curiosidade reacao add <chave> <rótulo>
/curiosidade reacao remover <chave>
/curiosidade categoria <nome> <on|off>

Every setter writes through to config.yml immediately, so in-game changes survive a restart.

Permissions

Permission Default Grants
canalhandia.reagir everyone react, press F
canalhandia.ver everyone ver, listar, /ranking
canalhandia.enquete everyone open polls
canalhandia.forcar op trigger curiosities and guess rounds
canalhandia.admin op change modules and all settings
canalhandia.isento nobody never be the subject
canalhandia.ia op /ia (public question, broadcast to chat)
canalhandia.ia.privado op /iap (private question, answer only to the asker)
canalhandia.ia.corrigir op /ia corrigir <resposta> — register a correction for the last answer
canalhandia.ia.perfil op /ia perfil <economico|preciso> — switch profile live

Permissions are declared explicitly in plugin.yml. An undeclared Bukkit permission falls back to op-only, which would silently stop normal players from reacting.


IA (/ia)

Chat Q&A backed by an OpenAI-compatible endpoint (default MiniMax). Gated to operator + LuckPerms-permitted players; both /ia and /iap default to op and are granted independently, so an operator can let someone ask privately without letting them broadcast.

The model can only ever produce chat text. No tool/function definitions are sent in the request, the reply is passed to sendMessage and nowhere else, and AiText.sanitise strips leading slashes so a reply cannot be mistaken for a command. A player asking it to "run /op me" gets a string back, not an executed command.

Commands

Command What it does
/ia <pergunta> Asks the model. Public by default — the question and answer broadcast. Needs canalhandia.ia.
/iap <pergunta> Asks privately — the answer goes only to the asker. Needs canalhandia.ia.privado.
/ia perfil <economico|preciso> Switches profile live. ECONOMICO skips the wiki (fast); PRECISO consults the Minecraft Wiki (slower, grounded). Needs canalhandia.ia.perfil.
/ia corrigir <resposta correta> Records a correction for the last answered question. Future similar questions get it as context — the cheap alternative to fine-tuning. Needs canalhandia.ia.corrigir.
/ia feedback ruim Flags the last answer wrong (in-memory counter shown in /canalhandia status).
/errado The [ERRADO] reaction to the last message — the typed-twin of the reaction button, for Bedrock players.

Subcommands only hijack when their second token is one they act on (a known profile key, or ruim), so /ia perfil do servidor falls through and is asked. corrigir stays greedy — a correction always reads the rest of the line.

Profile

ECONOMICO skips the wiki round trip — fast, ungrounded. PRECISO runs a forced tool call to pick a wiki term, looks it up on pt.minecraft.wiki, and injects the article as context. Switch live with /ia perfil; the choice is read per-question, so it takes effect immediately.

Operator corrections

/ia corrigir <resposta> appends to plugins/Canalhandia/correcoes.yml. When a new question shares at least one significant word (length > 4) with a recorded correction, the correction is injected as system context. Pure string matching, no model round trip.

Memory and context

  • Per-player memory: the last memoria-perguntas exchanges within memoria-minutos are replayed, for follow-ups like "e no nether?". Forgotten on quit. Baked at construction; not hot-swappable.
  • Server context: the contexto: list in config.yml is facts the model would never know (server name, Bedrock prefix, installed mods). Sent on every question.
  • Recipes: RecipeBook snapshots Bukkit.recipeIterator() at enable (main thread) and answers recipe questions from that snapshot — explaintext drops tables, so the wiki cannot supply them.

Keys and limits

The API key never lives in config.yml (committed to git). Read from the MINIMAX_API_KEY env var, or plugins/Canalhandia/minimax.key (one line, printable ASCII only — control chars are stripped so a stray newline can't land the key in a server-log header exception).

Per-player cooldown (cooldown-segundos), a server-wide daily cap (limite-diario), and a one-question-at-a-time guard per player keep the token spend bounded. canalhandia.admin skips the cooldown.

The four values url, wiki-caracteres, memoria-perguntas and memoria-minutos are baked at construction. Everything else — modelo, max-tokens, temperatura, instrucoes, perfil, contexto, the limits — is read live, so operators can hot-swap them with /canalhandia reload or the /ia perfil command without a restart.


Building

Requires JDK 25. Paper 26.2's API ships Java 25 class files, and JDK 21 fails with a misleading cannot access org.bukkit.Bukkit — that phrasing means the class-file version is too new, not that the dependency is missing.

docker run --rm -v "$PWD":/work -v "$HOME/.m2":/root/.m2 -w /work \
  maven:3.9-eclipse-temurin-25 mvn -B package

Output: target/Canalhandia-1.0.0.jar

The dependency uses Paper's newer coordinate scheme: io.papermc.paper:paper-api:26.2.build.92-stable.

Deploying

Copy the jar into the server's plugins/ and restart. There is no hot-reload path for a new jar — /canalhandia reload only re-reads config.yml.

POD=$(microk8s kubectl get pod -n minecraft -l app=crafty-controller -o name | head -1)
SRV=/crafty/servers/6e39a8b2-300b-42d6-8139-f397c23e461b
microk8s kubectl cp target/Canalhandia-1.0.0.jar minecraft/${POD#pod/}:$SRV/plugins/Canalhandia-1.0.0.jar

Source layout

File Role
Canalhandia.java Plugin entry point, scheduling, broadcasting, listeners
CanalhandiaCommand.java Every command and all click callbacks
Settings.java Typed config access; all setters persist immediately
Module.java / Category.java / Mode.java Toggleable feature, fact group, trigger mode
CuriosityFactory.java Builds the Portuguese sentences from statistics
Stats.java Defensive Bukkit statistics access
Fact.java One sentence plus its category
Reactions.java Reaction state, buttons, boss bar, tally
GuessRound.java "Adivinhe de quem é" round state
Poll.java Poll state, voting, results
Milestones.java Threshold tracking, persisted to marcos.yml
OfflineStats.java Reads stats JSON for offline players
RankingMetric.java Leaderboard columns and their formatting
Msg.java Shared chat formatting and pt-BR number/duration formatting

Adding a curiosity

Add one line to CuriosityFactory.facts(...) using the existing helpers (material, entities, distance, time, count), pick a Category, and phrase it so the count never has to agree with a translated noun.

Statistic constants get renamed between Minecraft releases, so resolve them via Stats.resolve("NEW_NAME", "OLD_NAME") — a rename then degrades one curiosity instead of breaking the whole announcement.

S
Description
Chat-only social features for the Canalhandia Minecraft server (Paper plugin)
Readme 1.9 MiB
Languages
Java 98.8%
Shell 1.2%