diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-09 21:20:48 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-01-09 21:20:48 +0900 |
| commit | 5cde814796ee841183df18b106d9f0e18f4985ab (patch) | |
| tree | 600771ed5382db7579ebacbc04bb6621b03a9a77 | |
| parent | b1c44dc64111710d4197d34e17e912e4f291602e (diff) | |
| download | LunaticChat-5cde814796ee841183df18b106d9f0e18f4985ab.tar.gz LunaticChat-5cde814796ee841183df18b106d9f0e18f4985ab.tar.bz2 LunaticChat-5cde814796ee841183df18b106d9f0e18f4985ab.zip | |
docs: Update Claude docs
| -rw-r--r-- | CLAUDE.md | 194 |
1 files changed, 176 insertions, 18 deletions
@@ -16,7 +16,7 @@ LunaticChat is a Minecraft chat plugin providing 1on1 messaging, quick reply fun 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 (not RuneCore's implementation) +3. **Use Paper's LifecycleEventManager** for command registration 4. **Chat logs must be compatible** with CoreProtect and similar logging plugins ## Project Structure @@ -61,24 +61,98 @@ LunaticChat/ **Conversion Timing**: When player sends message (`AsyncChatEvent` fires) -**Implementation Strategy**: -1. **Phase 1**: Implement with Kotlin sealed classes - - Define romaji mappings as sealed class hierarchy - - If this becomes unmanageable, proceed to Phase 2 -2. **Phase 2**: Use Map-based approach -3. **Phase 3**: External API with request limiting and caching +**Architecture**: Simple cache + Google IME API approach -**Example sealed class structure**: +``` +┌─────────────────────────────────────────┐ +│ 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 (5000) exceeded + +**Performance**: +- Cached conversions: < 1ms +- API calls: < 3000ms (first time only per phrase) +- Disk I/O: Async, no gameplay impact +- Memory footprint: ~250KB for 5000 entries + +**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 -sealed class RomajiMapping { - abstract val romaji: String - abstract val hiragana: String +class ConversionCache( + private val cacheFile: Path, + private val maxEntries: Int = 5000 +) { + private val memoryCache = ConcurrentHashMap<String, String>() + private val saveQueue = AtomicBoolean(false) - data object A : RomajiMapping() { - override val romaji = "a" - override val hiragana = "あ" + 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)) } - // ... more mappings } ``` @@ -100,6 +174,30 @@ data class PlayerChatSettings( ) ``` +**Cache Data Model**: +```kotlin +@Serializable +data class CacheData( + val version: String, + val entries: Map<String, String> +) +``` + +## Configuration + +```yaml +features: + japaneseConversion: + enabled: true + cache: + maxEntries: 5000 + saveIntervalSeconds: 300 # 5 minutes + cacheFile: "conversion-cache.json" + api: + timeout: 3000 # milliseconds + retryCount: 2 +``` + ## Future Features (Post v0.1.0) ### Cross-Server Chat (Velocity) @@ -129,8 +227,49 @@ fun onChat(event: AsyncChatEvent) { val settings = settingsManager.get(player.uniqueId) if (settings.japaneseConversionEnabled) { - val converted = romajiConverter.convert(/* message */) - // Modify message (don't cancel event for CoreProtect compatibility) + 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", 5000) + ) + 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") } } ``` @@ -141,8 +280,27 @@ fun onChat(event: AsyncChatEvent) { - 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 +- 5000 entries × ~50 bytes average = ~250KB +- Negligible impact on Minecraft server + +### Disk I/O +- **Startup**: Once (< 100ms for thousands of 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 -- Always use Japanese punctuation (,.) in responses. +- Java: 21+ +- Gradle: 9+ +- Kotlin: 2.3.0+ |
