72840c9770
Co-Authored-By: Claude <noreply@anthropic.com>
342 lines
16 KiB
Markdown
342 lines
16 KiB
Markdown
# Canalhandia
|
|
|
|
Chat-only social features for the Canalhandia Minecraft server (Paper 26.2).
|
|
|
|
**Almost nothing here touches gameplay** — no world edits, no attributes, no
|
|
economy, and no items anywhere except one: the `luto` tribute. Pressing F to pay
|
|
respects drops the dead player's head into the mourner's inventory as a symbolic
|
|
memento (toggle: `luto.cabeca`). Everything else is chat messages and clickable
|
|
buttons, and every module can be switched off independently. (The reaction-count
|
|
boss bar was removed; live counts ride on the reactor's action bar and a closing
|
|
tally line.)
|
|
|
|
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. Pressing F drops the **dead player's head** into the mourner's inventory (once per mourner per death, never to the dead player themselves) — the one gameplay-touching feature; toggle with `luto.cabeca`. Bedrock types `/f`. |
|
|
| `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. |
|
|
| `mortes` | Replaces each death message with a comic pt-BR line (cause-based flavor + a death counter) and sends the death coordinates **privately** to the dead player on respawn (the death screen swallows chat sent during the event), so they can run back to their dropped items. Respects `keepInventory`. No storage, no command. |
|
|
| `zoacao` | A bare `f`/`F` in chat (trimmed, nothing else) is swapped for a random line from `zoacao.mensagens` — a chat gag. The player's name still prefixes it. Pure chat swap; the `luto` tribute is unaffected (paying respects still needs the `[F]` button or `/f`). Affects Bedrock chat too (it's a chat event, not a click). |
|
|
| `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 two other surfaces instead:
|
|
|
|
- an **action bar** shown to whoever just reacted
|
|
- a **final tally line** broadcast when the window closes
|
|
|
|
(A boss bar spanning the whole reaction window was tried and removed — it sat
|
|
on screen for `janela-reacao-segundos` and read as clutter, and the two
|
|
surfaces above already carry the counts.)
|
|
|
|
### 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:
|
|
|
|
```yaml
|
|
reacoes:
|
|
uau:
|
|
java: "[😮]" # Java clients
|
|
texto: "[UAU]" # Bedrock clients — ASCII only
|
|
comando: "wow" # what a Bedrock player types
|
|
```
|
|
|
|
Messages with buttons are therefore built twice and sent per player
|
|
(`broadcastPerPlatform`), not via `Bukkit.broadcast`. Typed fallbacks:
|
|
`/reagir <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.
|
|
- **Asker's stats**: when `estatisticas-jogador` is on (default), the asking
|
|
player's headline numbers (blocks mined, time played, distance walked, deaths,
|
|
mob kills) are read from their vanilla stats JSON and injected as context, so
|
|
"quantos blocos eu minerei?" gets a real answer instead of "não tenho
|
|
acesso". The stats file lags by under a minute. Gated off with
|
|
`/canalhandia` settings or `ia.estatisticas-jogador: false`.
|
|
- **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.
|
|
|
|
```bash
|
|
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`.
|
|
|
|
```bash
|
|
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 (rankings + the asker's stat summary for the IA) |
|
|
| `RankingMetric.java` | Leaderboard columns and their formatting |
|
|
| `DeathFlavor.java` | Comic pt-BR verb phrases for each death cause (used by the `mortes` module) |
|
|
| `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.
|