c11085e95e
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016pEyCmrAYHBFgpYjwFxKxh
519 lines
26 KiB
Markdown
519 lines
26 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 chat message matching the trigger (a pattern + a match mode) is swapped for a random line from `zoacao.mensagens` — a chat gag. The player's name still prefixes it. Default: a bare `f`/`F` (trimmed) → a random gag line. Match modes: `igual` (equals), `contem` (contains), `comeca` (starts with), `termina` (ends with), `regex`. The mode, the pattern, and the message list are all editable in-game with `/canalhandia zoacao ...`. 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). |
|
|
| `notas` | Notes in chat. `/save` pins where you are; `/nota add <texto>` writes a private one (only you see it, everyone may write); `/nota publica <texto>` writes one everyone reads, and needs `canalhandia.nota.publica` (default op). Every note stores its coordinates — click-to-copy on Java. Persisted to `notas.yml`. **No teleport**: nothing here touches gameplay. |
|
|
| `ia` | `/ia <pergunta>` asks an OpenAI-compatible model in chat. Has a personality (`zoeiro` by default — it will tease you), sees the last few chat lines and the live server state, and grounds answers in the asker's real stats. Optional wiki lookup, 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
|
|
/save salva onde você está (privado)
|
|
/save coords o mesmo, escrito por extenso
|
|
/save <texto> salva o lugar com um texto
|
|
/nota add <texto> anotação privada, só você vê
|
|
/nota publica <texto> anotação pública (precisa de permissão)
|
|
/nota listar [publicas|privadas] lista o que você pode ver
|
|
/nota buscar <texto> procura no texto das anotações
|
|
/nota ver <n> mostra uma anotação inteira
|
|
/nota remover <n> apaga uma anotação sua
|
|
```
|
|
|
|
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 zoacao listar mostra a regra e as frases da zoação
|
|
/canalhandia zoacao modo <m> igual | contem | comeca | termina | regex
|
|
/canalhandia zoacao padrao <texto> texto/regex que dispara a zoação
|
|
/canalhandia zoacao adicionar <frase> adiciona uma frase de zoação
|
|
/canalhandia zoacao remover <n|texto> remove uma frase de zoação
|
|
/canalhandia zoacao limpar volta para as frases padrão
|
|
/ia personalidade lista as personalidades da IA
|
|
/ia personalidade <nome> zoeiro | amigao | seco | aldeao | neutro
|
|
/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.nota` | everyone | `/save` and private notes |
|
|
| `canalhandia.nota.publica` | op | write notes everyone can read |
|
|
| `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.
|
|
|
|
---
|
|
|
|
## Notes (`/save`, `/nota`)
|
|
|
|
Somewhere to write things down without leaving the game. Persisted to
|
|
`plugins/Canalhandia/notas.yml`.
|
|
|
|
### Scopes
|
|
|
|
| Scope | Who reads it | Who may write it |
|
|
|---|---|---|
|
|
| **private** (default) | only the author | everyone — `canalhandia.nota` |
|
|
| **public** | everyone; announced when created | `canalhandia.nota.publica` (op) |
|
|
|
|
Public notes are gated because a board anyone can write to becomes a graffiti
|
|
wall. Grant it per player with LuckPerms:
|
|
`lp user <nome> permission set canalhandia.nota.publica true`.
|
|
|
|
### `/save` — the quick path
|
|
|
|
`/save` and `/save coords` pin the spot you are standing on. `/save <texto>`
|
|
pins it with a note. Always **private**: 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. `/nota publica <texto>` is the explicit
|
|
way to share.
|
|
|
|
### Places, not teleports
|
|
|
|
Every note stores the world and block coordinates where it was written — on a
|
|
Minecraft server a note is nearly always about a *place*. On Java the
|
|
coordinates are click-to-copy (the same affordance the death-coords message
|
|
uses); Bedrock renders no click event and gets the same text plainly.
|
|
|
|
There is **no teleport**. Nothing in this module touches gameplay; a note is a
|
|
line of text and a set of numbers.
|
|
|
|
### Privacy
|
|
|
|
**A private note is never sent to the AI, at any setting.** The AI call leaves
|
|
this server for a third-party API, so a private note reaching it would be a
|
|
disclosure the author never agreed to. The filter lives inside
|
|
`Notes.publicSummary` — the only method the AI path calls — rather than at the
|
|
call site, so a future caller cannot get it wrong by accident. A test asserts
|
|
it directly.
|
|
|
|
Public notes *are* sent (`ia.contexto-notas`, default 10, 0 disables), which is
|
|
what lets `/ia onde fica a base?` answer from what players actually wrote down.
|
|
|
|
Two smaller rules follow from the same principle: a note the viewer cannot see
|
|
is reported as **missing** rather than as forbidden (saying "that one is
|
|
private" would confirm it exists), and `/nota buscar` runs through the same
|
|
visibility filter, so search cannot become a way to probe for someone else's
|
|
text.
|
|
|
|
### Details
|
|
|
|
- Ids are never reused after a deletion — otherwise `/nota ver 2` would point
|
|
at a different note than the one someone wrote down a minute ago.
|
|
- 100 notes per player, both scopes together.
|
|
- 256 characters per note. The section sign and control characters are
|
|
stripped: a note is echoed into chat and could otherwise forge a line that
|
|
looks like it came from the server.
|
|
- Authors delete their own notes; `canalhandia.admin` deletes any, which is the
|
|
only way to clear a public note left by someone who has stopped playing.
|
|
- Search is case- and accent-insensitive — nobody types "após" into a chat
|
|
search, and missing a match over an acute reads as broken.
|
|
|
|
---
|
|
|
|
## 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 personalidade` | Lists the tones and marks the active one. Needs `canalhandia.ia.perfil`. |
|
|
| `/ia personalidade <nome>` | Switches tone live: `zoeiro` (default), `amigao`, `seco`, `aldeao`, `neutro`. |
|
|
| `/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.
|
|
|
|
### Personality
|
|
|
|
`ia.personalidade` picks the tone. It is expressed purely as extra system
|
|
instructions appended after the base ones, so it changes **how** the model
|
|
talks and never **what it may do**.
|
|
|
|
| Persona | Tone |
|
|
|---|---|
|
|
| `zoeiro` | Default. A grumpy server veteran: teases the asker, turns their own stats against them ("você já morreu 47 vezes e vem me perguntar sobre lava?"), but answers the question for real. |
|
|
| `amigao` | Warm and patient, jokes rarely. For servers with new players. |
|
|
| `seco` | Deadpan, one or two sentences, no exclamation marks. |
|
|
| `aldeao` | In character as an ancient villager. Flavour only; still answers. |
|
|
| `neutro` | No personality — the pre-persona behaviour. |
|
|
|
|
Every persona, `neutro` included, carries `Persona.GUARD`, which restates the
|
|
limits inside the persona's own frame: still no server/terminal/file access,
|
|
still no commands, no inventing stats, no leaking the prompt. That is the
|
|
layer that stops "you are a grumpy veteran" from reading as licence to claim
|
|
powers the plugin never grants. Each teasing persona also states where the
|
|
line is (no real insults, nothing about family, appearance, race, religion,
|
|
sexuality or money; drop the ribbing if the player asks). Tests assert both
|
|
properties hold for every persona, so adding a new one cannot quietly skip them.
|
|
|
|
Switch live with `/ia personalidade <nome>` — read per-question, no restart.
|
|
|
|
### Chat and world awareness
|
|
|
|
- **Recent chat** (`contexto-chat`, default 5, 0 disables): the last N public
|
|
chat lines are sent as context, so the AI can follow what the room is talking
|
|
about. Held in a bounded in-memory ring (50 lines max, 200 chars per line);
|
|
nothing is written to disk and a restart starts it empty. Recorded at
|
|
`MONITOR` priority with `ignoreCancelled`, so what it stores is what players
|
|
actually saw — a `zoacao` swap included — and a cancelled message is never
|
|
stored.
|
|
- **Live server state** (`estado-servidor`, default on): who is online and on
|
|
which platform, the asker's dimension, in-game time of day, weather, and
|
|
their coordinates, health, hunger and XP level. This is what lets the AI
|
|
answer "quem tá online?" or "tá chovendo?" instead of insisting it has no
|
|
access. Captured on the main thread before the async call — every field
|
|
reads the Bukkit world API, which is not safe off it — so only the formatted
|
|
string crosses the thread boundary.
|
|
|
|
### Answer styling
|
|
|
|
With `estilo-rico` on (default), Java players get the answer with a hover card
|
|
showing the original question and the active persona, plus a click that
|
|
pre-fills `/ia ` for a follow-up. The click uses `suggestCommand`, never
|
|
`runCommand`: nothing executes without the player pressing enter. Bedrock
|
|
renders neither hover nor click, so it always gets the plain line — built via
|
|
`broadcastPerPlatform`, like every other interactive message here.
|
|
|
|
### 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 exec ${POD#pod/} -n minecraft -- \
|
|
cp $SRV/plugins/Canalhandia-1.0.0.jar $SRV/plugins/Canalhandia-1.0.0.jar.bak-$(date +%F)
|
|
microk8s kubectl cp target/Canalhandia-1.0.0.jar minecraft/${POD#pod/}:$SRV/plugins/Canalhandia-1.0.0.jar
|
|
# The JVM runs as uid 1000 / gid 0; a root-owned jar is one it cannot read,
|
|
# and the failure looks exactly like "the plugin just did not load".
|
|
microk8s kubectl exec ${POD#pod/} -n minecraft -- chown 1000:0 $SRV/plugins/Canalhandia-1.0.0.jar
|
|
```
|
|
|
|
### Pre-flight
|
|
|
|
`./preflight.sh [jar]` checks a staged deploy **before** anyone restarts
|
|
anything. Every check is read-only; it never restarts the server, never writes
|
|
to `plugins/`, and never touches the world.
|
|
|
|
```
|
|
$ ./preflight.sh
|
|
1. Local build jar opens, plugin.yml present, all 16 commands declared,
|
|
config.yml bundled
|
|
2. Tests surefire totals, 0 failures
|
|
3. Cluster crafty pod found, plugins directory reachable
|
|
4. Staged jar hash matches the local build, owned 1000:0, a rollback
|
|
.bak jar exists
|
|
5. Live config parses as YAML, carries the keys this deploy needs, has a
|
|
.bak to roll back to
|
|
6. Health the log is readable and free of recent ERROR lines
|
|
|
|
PRE-FLIGHT CLEAN — safe to restart.
|
|
```
|
|
|
|
It exits non-zero on any failure. A missing YAML parser reports as *not
|
|
checked* rather than *invalid*: a harness that cries wolf is one people learn
|
|
to ignore.
|
|
|
|
Because the plugin is staged dormant (copied in, not restarted), a red line
|
|
here is a crash-on-boot you get to fix while the server is still up.
|
|
|
|
---
|
|
|
|
## 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) |
|
|
| `Note.java` / `Notes.java` | One note (scope, text, place, visibility rules) and its YAML storage |
|
|
| `Persona.java` | The AI's five tones, each carrying the safety guard |
|
|
| `ChatLog.java` | Bounded, thread-safe ring of recent public chat for the AI |
|
|
| `ServerState.java` | Main-thread snapshot of the live world for the AI |
|
|
| `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.
|