summaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-08-05 04:36:20 +0900
committerSho Sakuma <me@m1sk9.dev>2026-08-05 04:36:20 +0900
commitb532dbee20a585467d320bc74eac8b52282701b8 (patch)
tree707ffe4b24c008053fc258e4ffe651d6500ee79d /CLAUDE.md
parent7c544224291e8ed7e23dcf97ea3cc6f42588c338 (diff)
downloadLunaticChat-b532dbee20a585467d320bc74eac8b52282701b8.tar.gz
LunaticChat-b532dbee20a585467d320bc74eac8b52282701b8.tar.bz2
LunaticChat-b532dbee20a585467d320bc74eac8b52282701b8.zip
docs: rewrite CLAUDE.md against the current codebase
The file had drifted far enough to mislead rather than help. It documented `./x start` without the platform argument the script now requires, pointed at a `docs/` directory that is `website/`, and placed the romaji converter in engine after it moved to platform-paper. It also said nothing about the conventions that are easiest to violate: the annotation-driven command framework, the Folia thread boundaries, and the per-setting config fallback that must not be traded back for null-assertions. Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md147
1 files changed, 86 insertions, 61 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 666eb78..89700d6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,17 +4,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
-LunaticChat is a Minecraft chat plugin supporting Paper, Folia, and Velocity servers. It provides direct messaging, channel chat, and romaji-to-Japanese conversion. Velocity proxy support enables global chat relay across multiple servers.
+LunaticChat is a Minecraft chat plugin supporting Paper, Folia, and Velocity servers. It provides direct messaging, channel chat, and romaji-to-Japanese conversion. Velocity proxy support relays global chat and (optionally) direct messages across backend servers.
- **Language**: Kotlin (JVM 25)
- **Build**: Gradle 9+ with Kotlin DSL
-- **Tooling**: mise (Java zulu-25, Bun 1.3.11)
+- **Tooling**: mise provisions Java and Bun (versions in `mise.toml`)
+- **Docs site**: VitePress in `website/`, deployed to Cloudflare Workers (`lc.m1sk9.dev`)
## Common Commands
### Build
```bash
-./gradlew clean build # Full build
+./gradlew clean build # Full build (build depends on shadowJar)
./gradlew :platform-paper:shadowJar # Paper/Folia plugin JAR
./gradlew :platform-velocity:shadowJar # Velocity plugin JAR
```
@@ -23,8 +24,8 @@ LunaticChat is a Minecraft chat plugin supporting Paper, Folia, and Velocity ser
```bash
./gradlew test # Run all tests
./gradlew :engine:test # Run engine tests only
-./gradlew :platform-paper:test # Run paper tests only
./gradlew :platform-paper:test --tests "*ChannelManagerTest" # Single test class
+./gradlew test jacocoTestReport # What CI runs (coverage goes to Codecov)
```
### Lint
@@ -33,98 +34,122 @@ LunaticChat is a Minecraft chat plugin supporting Paper, Folia, and Velocity ser
./gradlew ktlintFormat # Auto-format
```
-### Debug Server (Docker)
+### Documentation site (`website/`, Bun)
```bash
-./x start # Velocity + 2 Paper servers
-./x folia start # Single Folia server
-./x rcon s1 # RCON console to Paper s1
-./x help # All available commands
+bun install
+bun run dev # VitePress dev server
+bun run build # Also what CI verifies
+bun run check # Biome format + lint, writing fixes
+bun run format:check # What CI runs (biome ci .)
```
+### API docs (Dokka)
+```bash
+./gradlew :dokka:dokkaGenerate # Aggregated HTML into dokka/build/dokka/html
+```
+
+### Debug server (Docker)
+
+`./x <action> <platform> [--stable]` — the platform argument is required.
+
+```bash
+./x start velocity # Velocity proxy + Paper s1 & s2 (localhost:25577)
+./x start paper # Single Paper server (localhost:25565)
+./x start folia # Single Folia server (localhost:25565)
+./x rcon velocity s1 # RCON console to backend s1
+./x rcon paper # RCON console to the single Paper server
+./x log velocity # Follow logs
+./x clean folia # Stop and drop volumes
+./x help
+```
+
+Builds default to a nightly version derived from the git short hash; `--stable` builds a stable release. Server versions are derived from the `paper-api` / `velocity-api` coordinates in the module `build.gradle.kts` files — those are the single source of truth, so never duplicate a Minecraft version into the compose files.
+
## Architecture
### Module Structure
```
-engine/ → Platform-agnostic core (models, converters, protocol, exceptions)
-platform-paper/ → Paper & Folia plugin (commands, listeners, config, services)
-platform-velocity/ → Velocity proxy plugin (global chat relay between servers)
+engine/ → Platform-agnostic shared kernel (models, protocol, permissions, exceptions)
+platform-paper/ → Paper & Folia plugin (commands, listeners, config, services, converter)
+platform-velocity/ → Velocity proxy plugin (cross-server relay)
dokka/ → API documentation aggregator (no Kotlin source)
-docs/ → VitePress documentation site
+website/ → VitePress documentation site
```
-**Dependency flow**: `platform-paper` and `platform-velocity` both depend on `engine`. The engine has no Minecraft platform dependencies.
+**Dependency flow**: `platform-paper` and `platform-velocity` both depend on `engine` as equal peers. `engine` has no Minecraft, Adventure, or Brigadier dependency — its only dependency is `kotlinx-serialization-json`, exposed via `api()`. Keep it that way: `engine` must stay testable on a bare JVM.
+
+`engine` exists to be the single source of truth for anything both sides must agree on — the wire protocol, persistence schemas, permission nodes, `CommandResult`, and the domain exception vocabulary.
+
+### Key Packages
+
+| Module | Package | Purpose |
+|--------|---------|---------|
+| engine | `protocol` | `PluginMessage`, `PluginMessageCodec`, `ProtocolVersion` — the Paper↔Velocity wire contract |
+| engine | `chat.channel` | Channel domain model, roles, log entries (`@Serializable` persistence schema) |
+| engine | `settings` | Player settings DTOs with UUID serialization |
+| engine | `permission` | `LunaticChatPermissionNode` — permission node strings, referenced by type from `@Permission` |
+| engine | `command` | `CommandResult` sealed hierarchy (`toBrigadierResult()` returns `Int`, no Brigadier dependency) |
+| engine | `exception` | Domain exception hierarchy (mostly channel errors) |
+| paper | `command` | Annotation-driven Brigadier command framework (`core`, `annotation`, `impl`, `setting`) |
+| paper | `chat.handler` | DirectMessage / ChannelMessage / ChannelNotification handlers |
+| paper | `chat.channel` | `ChannelManager` (source of truth), membership, storage, NDJSON logger |
+| paper | `converter` | Romaji→Japanese: `KanaConverter`, `GoogleIMEClient` (Ktor), `ConversionCache` |
+| paper | `config` | `config.yml` → `LunaticChatConfiguration` via KAML |
+| paper | `velocity` | Plugin-messaging client, cross-server chat/DM, remote player registry |
+| paper | `i18n` | `LanguageManager` (KAML), `MessageFormatter`, `ChatFormat` |
+| velocity | `messaging` | `PluginMessageHandler`, cross-server chat and DM relays |
+| velocity | `presence` | `PresenceTracker` — which backend a player is on |
### Key Architectural Patterns
-**Service Container** (`platform-paper`): All services are held in an immutable `ServiceContainer` data class. `ServiceInitializer` handles initialization order and conditional feature setup. Optional features (channel chat, Velocity integration, Japanese conversion) are nullable fields.
+**Service Container + Feature Gating** (`platform-paper`): manual DI, no framework. `ServiceInitializer` constructs services in dependency order and owns shutdown; the immutable `ServiceContainer` data class holds them. A disabled feature's service is `null`, so feature presence is expressed in the type system and command/listener/setting registration branches on null checks. Adding a feature means: config flag → conditional construction in `ServiceInitializer` → nullable field on `ServiceContainer` → null-checked registration. Never reintroduce `!!` to work around this.
-**Feature Gating**: Features are toggled via `config.yml`. The `ServiceInitializer` conditionally creates services based on config, and the `ServiceContainer` holds them as nullable properties.
+**Annotation-driven commands**: `@Command(name, aliases, description)`, `@Permission(KClass<out LunaticChatPermissionNode>)`, and `@PlayerOnly` are read via Kotlin reflection (hence the `kotlin-reflect` dependency) and mapped onto a Brigadier tree. Root commands extend `LunaticCommand` and carry `@Command`; nested subcommands extend `LunaticSubCommand` and declare `literal` / `permissionNode` / `aliases` as properties instead. `CommandRegistry` builds the trees inside Paper's `LifecycleEvents.COMMANDS`. Handlers return `CommandResult`; `handleResult()` turns it into an Adventure message plus the Brigadier int.
-**Plugin Messaging Protocol**: Cross-server communication uses a custom `PluginMessageCodec` in the engine module. Paper servers encode/decode messages via this codec, and Velocity relays global chat between servers. DM and channel chat are local to each Paper server only.
+**Chat routing** (`PlayerChatListener`): a leading `!` forces global. Active channel and no `!` → cancel the event and deliver via `ChannelMessageHandler` (local to the server). Otherwise → global chat, additionally relayed through Velocity when cross-server chat is enabled. Channel chat is always server-local; global chat and (when `crossServerDirectMessage` is on) direct messages cross the proxy.
-### Key Packages (engine)
+**Folia-safe concurrency**: async work goes through `plugin.server.asyncScheduler` or `PluginCoroutineScope` (SupervisorJob); Bukkit API calls come back via `scheduler.runTask`. `PerPlayerWorkQueue` serializes each player's outgoing messages so a cached conversion cannot overtake an uncached one. Never block the tick thread — the only sanctioned `runBlocking` is the bounded `withTimeoutOrNull` in `RomajiConversionHelper`.
-| Package | Purpose |
-|---------|---------|
-| `chat.channel` | Channel data model and validation |
-| `converter` | Google IME API client for romaji conversion |
-| `exception` | Custom exception hierarchy (23 types) |
-| `protocol` | Plugin messaging codec for Velocity communication |
-| `settings` | Player settings models with UUID serialization |
+**Persistence**: in-memory cache + debounced async write (`DebouncedSaver`) + synchronous save on shutdown, with `Path.writeTextAtomically()` for every write, since a shutdown save and a pending debounced save can race on the same file. Formats: KAML/YAML for languages and player settings, JSON for `channels.json` and the conversion cache, NDJSON for channel logs.
-### Key Packages (platform-paper)
+**Config robustness**: `ConfigManager` deserializes `config.yml` with KAML and, on a failure, prunes the offending key and retries so only that setting falls back to its default. A malformed config must never throw out of `onEnable` and disable the plugin. `LenientBoolean` keeps YAML 1.1 spellings (`yes`/`no`/`on`/`off`) working, since Bukkit used to accept them.
-| Package | Purpose |
-|---------|---------|
-| `chat.handler` | DirectMessage, ChannelMessage, ChannelNotification handlers |
-| `command` | Command registration and execution |
-| `config` | YAML config loading via KAML |
-| `i18n` | Language support (EN, JA) |
-| `listener` | Event listeners (chat, join/quit, plugin messages) |
-| `velocity` | Cross-server integration |
+**i18n**: `resources/languages/{en,ja}.yml` are loaded and flattened to dotted keys. Lookup falls back selected-language → EN; a missing `en.yml` entry is fatal. Add every new user-facing string to both files.
## Versioning & Release
-### Version Management
-
-Paper and Velocity have independent versions, managed in `gradle.properties`:
-
-```properties
-paperVersion=1.0.0
-velocityVersion=1.0.0
-```
-
-### Release Tags
+Paper and Velocity have independent versions in `gradle.properties` (`paperVersion`, `velocityVersion`).
| Tag Pattern | Workflow | Target |
|-------------|----------|--------|
-| `paper/v1.1.0` | `release-paper.yaml` | Paper/Folia JAR only |
-| `velocity/v1.0.1` | `release-velocity.yaml` | Velocity JAR only |
-| `v1.2.0` | `release.yaml` | Both (e.g., engine changes) |
+| `paper/v1.3.0` | `release-paper.yaml` | Paper/Folia JAR only |
+| `velocity/v1.2.0` | `release-velocity.yaml` | Velocity JAR only |
+| `v1.3.0` | `release.yaml` | Both (e.g. engine changes) |
+
+Use the `release` skill (`.claude/skills/release/SKILL.md`) to run the pre-release checks and propose tags.
### Protocol Version (`engine/protocol/ProtocolVersion.kt`)
-Paper-Velocity compatibility is gated by protocol version only. Plugin version is not used for compatibility checks.
+Paper–Velocity compatibility is gated by protocol version only; plugin version is never used for compatibility checks. MAJOR must match exactly, remote MINOR must fall in `MIN_SUPPORTED_MINOR..MINOR`, PATCH is ignored.
| Level | When to bump | Deployment order |
|-------|-------------|-----------------|
-| **PATCH** | Add optional fields, new sub-channels | Any order |
-| **MINOR** | Add required fields | Velocity first, then Paper |
-| **MAJOR** | Wire format changes | Simultaneous |
+| **PATCH** | Optional fields with defaults, ignorable new sub-channels | Any order |
+| **MINOR** | Required fields, sub-channels whose absence degrades behavior | Velocity first, then Paper |
+| **MAJOR** | Removals, renames, wire format changes | Simultaneous |
-`MIN_SUPPORTED_MINOR` controls the acceptance window for older MINOR versions.
-
-### Backward Compatibility
-
-- `PluginMessageCodec` uses `ignoreUnknownKeys = true` to ignore unknown fields
-- `ProtocolBackwardCompatibilityTest` verifies backward compat via JSON snapshots
-- Protocol changes must include snapshot tests
+Adding a message type: data class in `PluginMessage` → constant in `PluginMessageCodec.SubChannel` → encode/decode branches → a snapshot in `ProtocolBackwardCompatibilityTest`. Backward compatibility rests on `ignoreUnknownKeys = true` plus defaulted fields; protocol changes must ship with snapshot tests.
## Code Conventions
-- Follows [Kotlin Coding Conventions](https://kotlinlang.org/docs/coding-conventions.html), enforced by Ktlint
-- PRs must pass `./gradlew ktlintCheck`
+- Follows [Kotlin Coding Conventions](https://kotlinlang.org/docs/coding-conventions.html), enforced by Ktlint; PRs must pass `./gradlew ktlintCheck`
+- Comments explain **why not** — a constraint, trade-off, or deliberately avoided approach that the code cannot state itself. The existing comments in `AtomicWrite.kt`, `ConfigManager.kt`, and `PerPlayerWorkQueue.kt` are the reference style
- Tests use JUnit 5 + MockK; coroutine tests use `kotlinx-coroutines-test`
+- Serialization is kotlinx-serialization, with KAML for YAML
- Shadow JAR output: `LunaticChat-{version}.jar` (Paper), `LunaticChat-{version}-velocity.jar` (Velocity)
-- Serialization uses kotlinx-serialization with KAML for YAML config files
+- New Velocity-side dependencies are weighed against JAR size: `platform-velocity` deliberately pulls in no Ktor
+
+## Deeper Documentation
+
+`website/src/docs/developers/` holds long-form design docs (`architecture.md`, `engine.md`, `platform-paper.md`, `platform-velocity.md`, `resource.md`). Read them for rationale, but verify against the source — they lag behind refactors (they still place `converter` in `engine` and describe `ConfigManager` as reading Bukkit's `FileConfiguration`). Update them in the same change when you move a boundary they describe.