# Migration Plan — CarbonChat → ChatChat

**Date drafted:** 2026-05-07
**Operator sign-off:** s8n green-lit pre-research, awaiting final go on swap window
**Live target:** racked.ru / Paper 1.21.11 / nullstone container `minecraft-mc`
**Built artifact:** `staging/chatchat/build/ChatChat-1.0.0-SNAPSHOT-racked-1.jar` (2.2 MB)
**Pre-research:** `docs/CHAT-PLUGIN-CHATCHAT-RESEARCH.md`, `_github/auth-limbo/docs/RESEARCH-LIMBO-PLUGIN-SURVEY.md`

---

## 0. Why we're doing this

CarbonChat 3.0.0-beta.36 has a viewer-context render bug: the MM tag
`<luckperms_prefix>` resolves against the **viewing** player, not the **sending**
player. Every player sees every other player wearing their own LP rank prefix.
The PAPI-form workaround `%luckperms_prefix%` returns legacy `&` codes that
Carbon doesn't pass through its MiniMessage pipeline, so the format renders
broken or stripped.

ChatChat (HelpChat fork of DeluxeChat) renders **per-recipient** via
`MessageProcessor.process()`, calls `PlaceholderAPI.setPlaceholders(sender, …)`
explicitly, and ships a built-in `Kyorifier` that converts `&` legacy codes to
MiniMessage **before** the parser runs. Both bugs gone.

---

## 1. Pre-flight checklist

- [ ] **JAR built:** `staging/chatchat/build/ChatChat-1.0.0-SNAPSHOT-racked-1.jar` exists. If missing, run `staging/chatchat/build/build.sh` (uses podman + temurin 21).
- [ ] **Configs staged:** `staging/chatchat/configs/{channels,formats,settings,extensions}.yml` reviewed by operator.
- [ ] **LP migration script reviewed:** `staging/chatchat/scripts/lp-migration.sh` — verify perm grants match operator intent (esp. `chatchat.tag.color/hover/item` for default group).
- [ ] **Rollback ready:** `staging/chatchat/scripts/{rollback,lp-rollback}.sh` present and executable.
- [ ] **Backup taken outside this swap:** standard nightly nullstone snapshot ran in last 24h (reference `BACKUP-STRATEGY.md`).
- [ ] **No DiscordSRV in flight:** Carbon config has DSRV integration enabled but DSRV plugin is NOT loaded — no live bridge to break. ChatChat config `extensions.yml > addons.discordsrv.channels_bridging: false` matches.
- [ ] **Quiet window:** swap during low-player time (≤2 players). Currently 2 online (s8n + YOU500 on default rank).

---

## 2. Swap window — step-by-step

The full sequence is automated by `staging/chatchat/scripts/swap.sh`. Steps:

1. **Broadcast** 60s warning via RCON (`say [racked.ru] chat plugin swap in 60s …`).
2. `save-all` to flush worlds.
3. **Backup** Carbon JAR + `/data/plugins/CarbonChat/` directory into `/data/backups/chat-swap-YYYY-MM-DD-HHMMSS/` (in-container, on the named docker volume).
4. `docker compose stop mc` — server goes offline. **Hot-swap is NOT used** because Paper's plugin lifecycle for chat-event-listening plugins is unreliable post-1.20; full restart is the safe path.
5. **Remove** `carbonchat-paper-3.0.0-beta.36.jar` and rename `/data/plugins/CarbonChat` → `CarbonChat.disabled-<ts>` (kept for forensics).
6. **Drop** `ChatChat-1.0.0-SNAPSHOT-racked-1.jar` into `/data/plugins/`.
7. **Drop** translated configs into `/data/plugins/ChatChat/{channels,formats,settings,extensions}.yml`.
8. `docker compose start mc` — server boots, ChatChat reads configs, generates `users.json`/`messages.yml`/`placeholders.yml` defaults.
9. **Run `lp-migration.sh`** — drops `carbon.channel.*` and grants `chatchat.channel.{see,use}.global`, plus `chatchat.pm`, `chatchat.ignore*`, `chatchat.mention.personal*`, and safe MM tag grants.
10. **Smoke test** (manual; see §3).

**Estimated downtime:**
- Best case (clean stop → swap → start, no boot-time hang): **45–60 seconds**.
- Worst case (config syntax error forces second restart, or world I/O slow): **3–5 minutes**.

---

## 3. Smoke verification checklist

Run as soon as the server accepts connections:

- [ ] `rcon-cli list` returns the player list (server alive).
- [ ] s8n logs in. Sends `hello` in chat. **Operator's own client** shows `[⛧] s8n — hello` (their own prefix). Confirms self-render works.
- [ ] YOU500 logs in (or remains logged in via cross-session). Sends a chat line. **Operator sees** `YOU500 — <message>` (no prefix because Adventurer prefix is set to `value=false` in his LP nodes — that's the existing rank state, not a regression). Confirms per-recipient render works WITHOUT inheriting s8n's owner prefix. **This is the bug we're fixing.**
- [ ] Operator runs `/msg YOU500 ping` → both sides see correct sender/recipient names (private-messages format).
- [ ] Operator runs `/r pong` from YOU500's session via test client (or socially: ask YOU500 to confirm `/msg s8n` works).
- [ ] `/ignore <test-name>` succeeds and `/ignorelist` shows the entry.
- [ ] LP perm check: `lp group default permission check chatchat.channel.use.global` returns `true`.
- [ ] No MiniMessage parse errors in the latest server log (`docker logs minecraft-mc 2>&1 | tail -200 | grep -iE "minimessage|parse|error"`).
- [ ] Console log shows readable plain-text chat lines (no MM-tag noise).

---

## 4. Rollback procedure

Triggered by any smoke-test failure (especially: chat doesn't render at all, players can't speak, or the prefix bug reappears). Run `staging/chatchat/scripts/rollback.sh`:

1. Locate latest `chat-swap-<ts>` backup dir on the data volume.
2. `docker compose stop mc`.
3. Remove ChatChat JAR + dir.
4. Restore Carbon JAR + `CarbonChat/` from backup.
5. Run `lp-rollback.sh` to restore `carbon.channel.*` perms and drop `chatchat.*`.
6. `docker compose start mc`.
7. Re-run `lp-rollback.sh` (online this time, ensures clean state).

Rollback downtime: same envelope as forward swap (45–90s).

The viewer-context bug is back, but that's the **status quo we tolerate today** — it's not a regression, it's a known-tolerated condition we attempted to fix. Defer the fix and re-research the failure mode.

---

## 5. Risk register (top 3)

| # | Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| **1** | ChatChat fork has no formal release; HelpChat repo is sleepy (last commit Apr 2025). The built JAR is essentially HEAD-of-main pinned by us. Could have latent 1.21.11 incompatibility nobody's found yet. | Medium | High (chat broken) | Test smoke checklist immediately. Rollback plan is mature. Pinned commit preserved in `/tmp/chatchat-build/.git`; we can re-build at any commit. |
| **2** | Config translation gaps — ChatChat splits format into `parts:` (channel/prefix/name/divider/message), Carbon used a single MM string. Edge cases like LP suffixes, unicode names, Essentials-nick formatting may render off. | Medium | Medium (cosmetic, not functional) | Smoke checklist includes operator + player visual check. Format tweaks are config-only, no rebuild needed; reload via RCON `reload confirm` post-swap. |
| **3** | Paper 1.21.11 + plugin compiled against Spigot 1.21.4 API drops a NoSuchMethodError on a removed Bukkit API call (rare but happens — Adventure platform API is the usual culprit). | Low | High (server crash on first chat event) | If chat triggers crash: stop server, run `rollback.sh`. Carbon's deprecated AsyncPlayerChatEvent works on 1.21.11 today, so identical-event ChatChat **should** too. |

**Lower-tier risks tracked but not actioned:**

- DiscordSRV bridge regression — N/A, plugin not loaded.
- MiniPlaceholders LP-Expansion drop — N/A, we switched to PAPI form which Kyorifier handles.
- Item-tag rename `<itemlink>` → `<item>` — no players use it today.
- User data carry-over — Carbon nicknames live in `nickname-settings.use-carbon-nicknames=true`; they'll be lost on Carbon disable. **EssentialsX nicknames are unaffected** (different store), and EssentialsX is what `%player_displayname%` reads from in our new config, so visible nicknames survive.

---

## 6. Post-swap follow-ups

- Update `docs/PLUGINS.md` to swap CarbonChat → ChatChat in the plugin table.
- Update `docs/PERMISSIONS.md` with the new `chatchat.*` perm node tree.
- Add ChatChat to the racked.ru THANKS.md (HelpChat fork + AGPL/MIT credit).
- After 7 days of stable operation, delete `/data/plugins/CarbonChat.disabled-<ts>` and the chat-swap backup dir.
- File issue at HelpChat/ChatChat for any 1.21.11 quirks observed (give back to upstream).

---

## 7. Files in this migration

```
staging/chatchat/
├── build/
│   ├── build.sh                                  # podman + temurin 21 reproducible build
│   └── ChatChat-1.0.0-SNAPSHOT-racked-1.jar      # GITIGNORED — rebuild from build.sh
├── configs/
│   ├── channels.yml                              # global channel, single rank-tier format
│   ├── extensions.yml                            # towny/dsrv/grief disabled, ess vanish on
│   ├── formats.yml                               # console-format + default fallback
│   └── settings.yml                              # PMs, mentions, item-format
└── scripts/
    ├── build.sh                                  # (in build/)
    ├── lp-migration.sh                           # carbon.* → chatchat.* perm grants
    ├── lp-rollback.sh                            # reverse of lp-migration.sh
    ├── rollback.sh                               # full revert: stop, restore Carbon, perm-rollback, start
    └── swap.sh                                   # full swap orchestrator with confirm gates
```
