diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 04:36:20 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 04:36:20 +0900 |
| commit | b532dbee20a585467d320bc74eac8b52282701b8 (patch) | |
| tree | 707ffe4b24c008053fc258e4ffe651d6500ee79d /CLAUDE.md | |
| parent | 7c544224291e8ed7e23dcf97ea3cc6f42588c338 (diff) | |
| download | LunaticChat-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.md | 147 |
1 files changed, 86 insertions, 61 deletions
@@ -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. |
