summaryrefslogtreecommitdiff
path: root/CLAUDE.md
blob: fd482ab691b9111179129dec341fe358bf5cecec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# LunaticChat - Design Document

## Project Overview

LunaticChat is a Minecraft chat plugin providing 1on1 messaging, quick reply functionality, and romaji-to-Japanese conversion features.

## Technology Stack

- **Language**: Kotlin
- **Target Platforms**:
    - Paper (Minecraft 1.21.x+)
    - Velocity (planned for future)
- **Build Tool**: Gradle (multi-project setup)

## Core Principles

1. **Always support the latest version** while maintaining backward compatibility (e.g., 1.21.x)
2. **Maintainability**: Design for extensibility and easy maintenance
3. **Use Paper's LifecycleEventManager** for command registration
4. **Chat logs must be compatible** with CoreProtect and similar logging plugins

## Project Structure

```
LunaticChat/
├── engine/              # Core logic (shared code, chat processing, romaji conversion)
├── platform-paper/      # Paper plugin implementation
├── platform-velocity/   # Velocity plugin implementation
└── docker/              # Docker configuration
```

### Why Separate JARs?

- Paper and Velocity use different APIs
- Avoids classloader conflicts
- Clear deployment boundaries
- Gradle multi-project keeps build unified

## Features (v0.1.0)

### 1. Direct Messaging System

**Commands**:
- `/tell` (aliases: `/t`, `/msg`, `/m`, `/w`, `/whisper`)
- `/reply` (alias: `/r`)

**Requirements**:
- Use Paper's `LifecycleEventManager` for command registration
- Messages must appear in CoreProtect logs
- Use `io.papermc.paper.event.player.AsyncChatEvent` (not deprecated `AsyncPlayerChatEvent`)
- Don't cancel events; modify messages instead

### 2. Quick Reply Functionality

- `/reply` sends message to last person who messaged you
- Maintain conversation context per player

### 3. Romaji to Japanese Conversion

**Trigger**: Player's personal setting via `/jp on` or `/jp off`

**Conversion Timing**: When player sends message (`AsyncChatEvent` fires)

**Architecture**: Simple cache + Google IME API approach

```
┌─────────────────────────────────────────┐
│         Player Input (Romanji)          │
└──────────────────┬──────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────┐
│         Check Memory Cache              │
├─────────────────────────────────────────┤
│  Hit: Return cached result (< 1ms)      │
│  Miss: Call Google IME API              │
│        → Save to cache                  │
│        → Queue async disk save          │
└──────────────────┬──────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────┐
│       Converted Text (Japanese)         │
└─────────────────────────────────────────┘
```

**Key Components**:

1. **RomanjiConverter** - Main conversion coordinator
2. **ConversionCache** - Two-tier caching (memory + disk)
    - Memory: ConcurrentHashMap for instant access
    - Disk: JSON file loaded on startup, saved periodically
3. **GoogleIMEClient** - HTTP client for Google Transliterate API

**Cache Strategy**:
- Load cache from disk on plugin enable (once)
- All conversions check memory cache first
- Cache misses trigger API call and store result
- Periodic async saves (every 5 minutes) + final save on disable
- LRU eviction when max entries (500) exceeded

**Performance**:
- Cached conversions: < 1ms
- API calls: < 3000ms (first time only per phrase)
- Disk I/O: Async, no gameplay impact
- Memory footprint: ~25KB for 500 entries
- Startup load time: < 10ms

**Example Implementation**:
```kotlin
class RomanjiConverter(
    private val cache: ConversionCache,
    private val apiClient: GoogleIMEClient
) {
    suspend fun convert(input: String): String {
        // Check cache first
        cache.get(input)?.let { return it }
        
        // Call Google IME API
        val result = apiClient.convert(input)
        
        // Store in cache
        cache.put(input, result)
        
        return result
    }
}
```

**Cache Implementation**:
```kotlin
class ConversionCache(
    private val cacheFile: Path,
    private val maxEntries: Int = 500
) {
    private val memoryCache = ConcurrentHashMap<String, String>()
    private val saveQueue = AtomicBoolean(false)
    
    fun loadFromDisk() {
        if (!cacheFile.exists()) return
        val data = Json.decodeFromString<CacheData>(cacheFile.readText())
        memoryCache.putAll(data.entries)
    }
    
    fun get(key: String): String? = memoryCache[key]
    
    fun put(key: String, value: String) {
        if (memoryCache.size >= maxEntries) evictOldest()
        memoryCache[key] = value
        queueDiskSave()
    }
    
    fun saveToDisk() {
        val data = CacheData(version = "1.0", entries = memoryCache.toMap())
        cacheFile.writeText(Json.encodeToString(data))
    }
}
```

## Data Persistence

**No SQL databases** - Use JSON file storage instead

**Storage Strategy**:
- Player settings stored as JSON files
- UUID-based file naming
- In-memory cache with periodic saves
- Use kotlinx.serialization for JSON handling

**Data Model**:
```kotlin
data class PlayerChatSettings(
    val uuid: UUID,
    val japaneseConversionEnabled: Boolean = false
)
```

**Cache Data Model**:
```kotlin
@Serializable
data class CacheData(
    val version: String,
    val entries: Map<String, String>
)
```

## Configuration

```yaml
features:
  japaneseConversion:
    enabled: true
    cache:
      maxEntries: 500
      saveIntervalSeconds: 300  # 5 minutes
      cacheFile: "conversion-cache.json"
    api:
      timeout: 3000  # milliseconds
      retryCount: 2
```

## Future Features (Post v0.1.0)

### Cross-Server Chat (Velocity)

- Broadcast normal messages across all servers
- Enable `/tell` for 1on1 chat across servers

### Channel Chat System

- Players can create custom channels
- Chat within specific channels
- Channel management commands

## Implementation Order

1. **Setup multi-project structure** (Paper/Velocity extensibility)
2. **Implement JSON-based data persistence**
3. **Implement `/tell` and `/reply` commands**
4. **Implement romaji conversion system**

## Event Handling

```kotlin
@EventHandler(priority = EventPriority.HIGHEST)
fun onChat(event: AsyncChatEvent) {
    val player = event.player
    val settings = settingsManager.get(player.uniqueId)
    
    if (settings.japaneseConversionEnabled) {
        val plainText = (event.message() as? TextComponent)?.content() ?: return
        val converted = runBlocking { romajiConverter.convert(plainText) }
        event.message(Component.text(converted))
    }
}
```

## Plugin Lifecycle

```kotlin
class LunaticChat : JavaPlugin() {
    private lateinit var romanjiConverter: RomanjiConverter
    
    override fun onEnable() {
        // Load cache on startup
        val cache = ConversionCache(
            cacheFile = dataFolder.resolve("conversion-cache.json").toPath(),
            maxEntries = config.getInt("features.japaneseConversion.cache.maxEntries", 500)
        )
        cache.loadFromDisk()
        
        // Initialize converter
        val apiClient = GoogleIMEClient(
            timeout = config.getInt("features.japaneseConversion.api.timeout", 3000).milliseconds
        )
        romanjiConverter = RomanjiConverter(cache, apiClient)
        
        // Periodic save task
        val saveInterval = config.getLong(
            "features.japaneseConversion.cache.saveIntervalSeconds", 300
        ) * 20L // Convert seconds to ticks
        
        server.scheduler.runTaskTimerAsynchronously(this, {
            cache.saveToDisk()
        }, saveInterval, saveInterval)
        
        logger.info("Japanese conversion system initialized")
    }
    
    override fun onDisable() {
        // Final save on shutdown
        cache.saveToDisk()
        logger.info("Cache saved on shutdown")
    }
}
```

## Notes

- **AsyncChatEvent** uses Paper's Component API - handle accordingly
- Command aliases must be properly registered
- Settings file location: `plugins/LunaticChat/settings/`
- Cache settings in memory to avoid frequent file I/O
- Cache file location: `plugins/LunaticChat/conversion-cache.json`
- All disk I/O is async to prevent blocking game thread

## Performance Considerations

### Memory Usage
- 500 entries × ~50 bytes average = ~25KB
- Parse-time memory consumption: < 100KB
- Negligible impact on Minecraft server

### Disk I/O
- **Startup**: Once (< 10ms for 500 entries)
- **Runtime**: Periodic saves every 5 minutes (async)
- **Shutdown**: Once (final save)

### Network
- No network calls after cache hit
- Each unique phrase calls Google API only once

## Development Environment

- Shell: Fish
- Java: 21+
- Gradle: 9+
- Kotlin: 2.3.0+