summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
2026-08-05fix: report queued work dropped at shutdownSho Sakuma
Cancelling the scope kills the worker coroutines but does not close their channels, so trySend kept reporting success for work nothing would ever read. The warning that exists precisely to avoid dropping a message in silence was therefore unreachable in the case it was written for. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05fix: stop treating cancellation as a conversion failureSho Sakuma
The catch-all around the Google IME call also caught the CancellationException from the caller's timeout, and then cached the unconverted hiragana. Since the words of a message are now converted concurrently, one timeout pinned every word of that message to its hiragana form for the life of the cache instead of just the word that timed out. The same swallowing let a cancelled delivery carry on past the plugin scope being cancelled, and reported the shutdown of an in-flight cross-server message as a SEVERE delivery failure. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05fix: let shutdown finish when a save failsSho Sakuma
The steps ran as one statement each, so the first exception escaped onDisable and took the rest with it - leaving the channel message logger unflushed and the Velocity connection to be torn down by the server rather than by us. Each step is independent, so a failure is now reported and the remaining ones still run. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05fix: give every data file the same atomic writeSho Sakuma
Writing to a fixed sibling only moved the interleaving from the destination to the temporary file: two saves racing there published mixed content, and the losing move then failed with the temporary file already gone. Each write now gets a unique temporary file, and falls back to a non-atomic replace on the network mounts that refuse an atomic rename. settings.yml and the conversion cache were still written in place. Both are discarded wholesale when they do not parse, so a torn file silently costs every player's settings or the whole accumulated cache. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05fix: harden the delivery queue and the writes around itSho Sakuma
Findings from reviewing the branch, in descending order of how much they could hurt. A message that threw ended the consumer loop while its channel stayed registered in the map. Every later message from that player was then buffered with nothing reading it, so their /tell and /reply stopped working silently until they reconnected - one bad message format was enough. The CoroutineExceptionHandler added earlier reported the throw but could not bring the worker back. Each item now runs inside its own guard, so a failure costs one message instead of the player's whole session. Reply targets were recorded inside the queued delivery, but /reply reads them on the command thread. /tell followed straight by /r therefore failed with "nobody to reply to" for as long as the conversion took - up to the full timeout. They are recorded before the work is queued now, which is also where the sender expects the effect to happen. Three narrower ones: - A refused trySend was discarded without a word. It can happen once the scope is cancelled at shutdown, and dropping a player's message in silence is the worst way to handle it. - Parallel conversion opened one API request per word with no ceiling, and a repeated word in one line no longer shared the cache - the sequential version got that for free by caching before the next lookup. Rate-limited replies land in convertWord's catch and quietly degrade to hiragana, so it is better not to ask that hard: distinct words only, four at a time. - coerceAtLeast(1) on the cache interval turned a configured 0 into a whole file rewrite every second, since any chat re-arms the dirty flag. A non-positive value now falls back to the documented 300 with a warning. ChannelStorage writes through a temporary file and an atomic move. Bukkit runs onDisable before cancelling scheduler tasks, so the shutdown save can overlap a still-pending debounced save, and two truncating writes to channels.json would interleave into something unparseable. Debouncing made that window much wider than the old runNow did. TestLogger now captures log(level, msg, thrown); it only overrode severe/warning/info, which do not route through each other, so anything logged with a throwable attached was invisible to every assertion. The two queue tests fail against the unguarded loop. Left as review comments: delivery in flight can re-add a reply entry for a player who just quit. resolveValidTarget filters unreachable targets on read and the entry is cleared on their next quit, so the cost is a lingering map entry, not wrong behaviour - and removing it would change what sendDirectMessage promises, which a test pins. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05fix: take the channel snapshot where the caches are mutatedSho Sakuma
Passing ::snapshot to the debounced write meant the three caches were read from the writer thread while the server thread mutated them. They are separate maps, so a snapshot could catch a channel already removed from channelsCache while its membersCache entry still existed - persisting an orphaned member list - or the reverse, persisting a channel with no members and therefore no owner. Neither crashes anything, since the readers filter on channelsCache, but they are wrong state written to channels.json and carried across restarts. The snapshot is taken on the mutating thread again and handed over through a volatile field, so the write still reads the newest state when it eventually runs rather than the state at queue time. That keeps both properties: consistent halves, and a batched write that reflects every change made during the delay. This does not give back the per-change cost the earlier commit was avoiding, because copying three maps is not what made saving expensive - the debounce already coalesces the file write, which is. Also floors the conversion cache interval at one second. It is now the only writer besides shutdown, so a non-positive value would both be rejected by runAtFixedRate and leave the cache unsaved until the server stopped. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05fix: keep dispatched delivery ordered and its failures visibleSho Sakuma
Two regressions from taking the direct message path off the tick thread in this branch. A coroutine that threw reported to the JVM default handler, so nothing reached the plugin log - and since the command had already returned Success, a failed delivery was invisible to the player and the operator alike. Before the change the exception propagated out of execute() into Brigadier. PluginCoroutineScope now carries a CoroutineExceptionHandler, which every caller that dispatches and returns depends on. Launching a coroutine per message also dropped the ordering the synchronous version had. Dispatchers.Default is a pool, so a cached romaji conversion finishing in microseconds could overtake an uncached one sent before it: the sender sees their second message first, and on the cross-server path the relay to Velocity inverts too, so the recipient does as well. With Japanese conversion on, mixing cached and uncached words is ordinary, not an edge case. A mutex would not have fixed it - two launched coroutines reach the lock in whatever order the pool starts them. PerPlayerWorkQueue instead gives each player a channel with a single consumer, so submission order is decided on the calling thread and preserved, while different players stay independent. The queue is dropped when a player quits; work already queued still runs. The ordering test fails against the previous launch-per-message code. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05perf: take the direct message path off the tick threadSho Sakuma
/tell and /reply ran romaji conversion inline. Brigadier executors run on the main thread, and convertWithRomaji wrapped a Google IME call in runBlocking with a one-second timeout, so a single direct message could hold the tick thread for up to a second - twenty ticks - whenever the words were not already cached. Conversion defaults to on for players once the feature is enabled, so this was the ordinary path, not an edge case. The conversion chain is suspending now, and the two commands dispatch delivery to the plugin scope instead of running it inline. Nothing in that chain touches world state: it sends chat components and plays client-side sounds, both of which Paper already accepts off-thread, and which this plugin already does from AsyncChatEvent. AsyncChatEvent keeps a blocking bridge, renamed convertWithRomajiBlocking so the choice is visible at the call site. That handler has to decide whether to cancel the event and what body to set before it returns, and it is already off the tick thread. The commands take their scope as a constructor parameter so tests can choose one. The new test uses StandardTestDispatcher to pin the property that matters: execute() returns before delivery has run at all. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05perf: coalesce channel writes instead of rewriting the file per changeSho Sakuma
Every channel mutation built a full snapshot - copying the channel map, every member list, and stringifying a UUID per active player - and then queued a task that pretty-printed and rewrote all of channels.json. There was no debounce, unlike the settings storage, so a fifty-player join wave meant fifty snapshots and fifty whole-file writes. Joins and quits both go through it via setPlayerChannel, and so does the self-healing branch of getPlayerChannelContext, which sits on the channel-chat message path. ChannelStorage now debounces like the other two storages, and takes the snapshot as a supplier so it is built once when the write runs rather than once per queued change. saveToStorage and saveToDisk had the same six-line snapshot construction; that is now one private function. The trade-off is the same one the settings storage already makes: a crash within the debounce window loses the last few seconds of channel state. Shutdown still writes synchronously. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05perf: answer membership questions without copying member listsSho Sakuma
isMember went through getChannelMembers, which copies a channel's whole member list defensively - to then run any{} over it and throw the copy away. getPlayerChannels did that once per channel, so asking "which channels is this player in" allocated an ArrayList per channel and scanned every one. It runs on join, and on every /lc channel join when a membership limit is configured. ChannelManager now answers both directly against the live lists: isMember scans in place, and channelIdsOf walks the membership map once. No copies, and getPlayerChannels no longer enumerates all channels separately to cross-reference them. Not done: the reviewed suggestion was a playerId -> channelIds reverse index for O(1) lookups. That means a second record of who is in what, updated by hand at five mutation sites, where drift shows up as a wrong membership answer rather than a crash. The allocation was the real cost here, and removing it does not introduce a fact stored twice. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05perf: only do spy and cache work when it will be usedSho Sakuma
Spy notification ran per recipient what it could run once: both handlers looked up general.spyMessage and rebuilt the hover component inside the forEach, and the direct message path also allocated a Set inside the filter predicate, once per spy per message. It formatted the spy copy of the message before discovering there were no spies to send it to - and spies are rare, so that was the normal case. SpyPermissionManager.notifySpies now owns the whole shape. It takes the body as a lambda so nothing is built for an empty audience, attaches the hover once, and reads the roster directly rather than through getDirectMessageSpyPlayers()'s defensive copy. It also puts "who must not see this" in one place; the two handlers had drifted to expressing it by name in one and by UUID in the other. ConversionCache tracked no dirtiness, so the periodic task re-serialized and rewrote the entire cache file on its fixed interval whether or not anyone had chatted, while every single put scheduled another full rewrite five seconds out. It now records that it changed and the periodic task is the only writer, returning immediately when there is nothing to write. A failed write marks the cache dirty again so the next tick retries. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05perf: stop writing the whole settings file on every quitSho Sakuma
PlayerQuitEvent called saveToDisk(), which KAML-serializes every player ever recorded and writes the file inline. That runs on the tick thread, costs time proportional to the total stored players rather than the online ones, and so grows for the life of the server. A logout wave stacks the writes into a visible stall. The comment above the call already said "async save"; the storage KDoc already said the synchronous path was for shutdown. Quitting changes no setting, so there is nothing new to persist - the value of saving here is flushing what an earlier toggle left pending. queueSave() does exactly that through the existing debounce, and saveToDisk() is now documented as the shutdown path and called from nowhere else. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05perf: convert the words of a message concurrentlySho Sakuma
RomanjiConverter awaited the Google IME call for one word before starting the next, though the words are independent. Callers convert under a single one-second timeout covering the whole message, so an N-word message needed N round trips inside a budget sized for roughly one: past the first word or two the remaining calls were cancelled, and because cache.put is only reached after a call returns, the discarded work was not even remembered. The next identical message repeated it. The words now run under one coroutineScope, so the message costs one round trip rather than N. The new test pins the concurrency itself rather than the timing: the fake API client records how many calls are in flight at once, which fails against the sequential version. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05perf: take per-message logging off the chat pathSho Sakuma
Every channel message wrote a fully interpolated INFO line, duplicating what the dedicated async ChannelMessageLogger already persists. Every cross-server message produced one INFO on the sending Paper server, two on Velocity, and one on each receiving Paper server - so a single global chat message cost up to four synchronous console and latest.log writes across the network, on the message path, with JUL and Logback appenders being synchronous. These are now debug, and phrased so the string is not built unless debug is on: the JUL sites take a supplier, the slf4j sites take a format plus arguments. Handshake, registration and delivery-failure lines stay at info - they fire once per server or once per failed message, and they explain something an operator needs to see. Two relay tests asserted on the text of a log line. One of them was verifying "0 servers" where the same test already verifies zero sends; the other was really checking that the payload reaches the target, so it now decodes the relayed bytes and compares them to the original message. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05perf: stop paying at startup for features that are offSho Sakuma
Both HTTP consumers - Japanese conversion and the update check - default to disabled, yet onEnable built an HttpClient(CIO) unconditionally, starting a selector and dispatcher pool that a stock install never used and nothing ever closed. It is now created on first use and closed on disable, which also fixes the leak across /reload. LanguageManager parsed and flattened every bundled language file, though getMessage only ever reads the selected one and the English fallback. It now loads exactly those two. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-03fix(deps): update dependency io.papermc.paper:paper-api to ↵renovate[bot]
v26.2.build.92-stable (#254) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-02Merge pull request #260 from m1sk9/refactor/codebase-cleanupSho Sakuma
refactor: single-source duplicated command, setting, and cross-server logic
2026-08-02test: drop a stub the invite command no longer reachesSho Sakuma
ChannelInviteCommand used to pre-check isPlayerBanned itself. That check moved into ChannelMembershipManager.inviteToChannel in 288d1e4, but the stub stayed behind, implying the command still consults the channel manager for ban state when it no longer does. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02fix: let a debounced settings write see changes made while it waitsSho Sakuma
DebouncedSaver keeps only the first callback of a burst, and queueAsyncSave closed over the snapshot taken when it was called. So if one player toggled a setting and a second toggled two seconds later, the write that fired at five seconds persisted the first snapshot and dropped the second player's change - it survived in memory until some later toggle happened to trigger another write, and was lost on a crash. Passing a supplier instead means the snapshot is taken when the write runs, which is what "batched into a single save" was always meant to mean. ConversionCache already had this shape by passing ::saveToDisk. The staleness predates the refactor, but bc8010b claimed to have closed this window; it only moved where the snapshot was built, not when. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02refactor: fold the remaining small duplicationsSho Sakuma
- MessageFormatter built the same prefix component in three functions. - LanguageManager copied kaml's YamlNode into a private YamlValue tree before flattening it, so the map case was written twice and the list-of-maps case rendered a Kotlin data class toString into a player facing string. It now folds YamlNode directly. - StatusCommand inlined `if (enabled) "toggle.on" else "toggle.off"`, which is the body of LanguageManager.getToggleText. - The three chat formats each spelled out their own chain of String.replace, with the valid placeholder names documented only in a config.yml comment. - ChannelContext carried a channelId that both construction sites filled with channel.id; it is now derived, so the two cannot disagree. - ChannelInfo and ChannelStatus each declared MAX_MEMBERS_DISPLAY = 10 and built the same truncated member line, differing only in indent. A divergence between the two constants would have been invisible. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02refactor: let each service live in exactly one placeSho Sakuma
ServiceInitializer held ten nullable fields alongside the ServiceContainer it returns. Six of them were written and never read; the remaining four made shutdown() ambiguous, reading conversionCache and channelMessageLogger from its own fields but everything else from the container it was handed. LunaticChat then mirrored seven more into public vars, one of which (channelMessageHandler) nothing read at all. Now the container is the single place a service lives: the initializer builds and returns, shutdown and the periodic task read from what they are given, and the plugin's public properties delegate rather than copy. A new service is one field instead of three, and no copy can go stale. PlayerSettingsManager had the same shape at a smaller scale: three UUID maps plus a PlayerSettingsData kept in sync by hand, where the data object was a pure derivation rebuilt - three full map copies - on every toggle. It now keeps one map and derives the snapshot at save time, which also closes the window where queueAsyncSave captured state that changes before the debounce fires. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02refactor: extract the debounced async saveSho Sakuma
YamlPlayerSettingsStorage and ConversionCache each carried a line-for-line identical AtomicBoolean-plus-runDelayed debounce, five-second constant included, so changing the save cadence meant changing it twice and noticing that it was written twice. Extracting it also removed the only reason those two classes held a JavaPlugin: they took the whole plugin to reach the scheduler. They now take the collaborator they actually use, which is both narrower and testable without a running server. ChannelStorage is deliberately left alone - it saves through runNow with no debounce at all, and giving it one would change when writes happen. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02refactor: move channel moderation rules out of the commandsSho Sakuma
The rule "owners and moderators may kick and ban" was written out six times, as `senderRole == null || senderRole == ChannelRole.MEMBER`, while ChannelMembershipManager.hasRole - which encodes the OWNER > MODERATOR > MEMBER hierarchy in one place and has tests - had no production caller at all. Letting moderators ban meant editing six files and hoping none was missed; a miss is a silent privilege change. The bypass and self-invite rules had drifted further still: the engine defines ChannelPlayerBypassBanException, ChannelPlayerBypassKickException and ChannelCannotInviteSelfException, but nothing threw them. The checks lived inline in the commands, and ChannelBanCommand carried a catch arm for an exception that could never arrive. Those three rules now live on ChannelMembershipManager and throw what the engine already declared, so every caller is held to them rather than only the command path. ChannelInviteCommand also pre-checked for a banned target, which joinChannel checks again a moment later and reports through the same message; the pre-check is gone. What stays in the commands is what belongs there: parsing an argument and choosing which message to show. ChannelSubCommand names the steps they share and derives message keys from the subcommand's own literal. The tests followed the rules: bypass and self-invite are now asserted against ChannelMembershipManager, with a bypass predicate injected so the manager stays testable without a running server. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02refactor: single-source the plugin messaging channel and dedup cacheSho Sakuma
The channel Paper and Velocity talk over was declared in seven places, in two spellings ("lunaticchat:main" and the namespace/name pair), one of them an inline literal in CrossServerChatManager that bypassed even its own file's constant. Renaming it meant finding all seven; missing one leaves both sides compiling and starting, just not talking. It now lives next to the codec that defines the wire format. The echo-suppression cache was likewise written twice, and the copies had already drifted in style - one hand-rolled the expiry sweep, the other used filter/map - while staying semantically identical. Any future change to eviction would have had to land in both, and CrossServerChatManager's copy carried a comment claiming ConcurrentHashMap iterators cannot remove(), which they can. MessageDeduplicationCache documents the one property that surprised the tests written against it: eviction orders by millisecond timestamp, so a burst inside a single millisecond evicts arbitrarily among its members. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02refactor: describe a setting once, on its SettingKeySho Sakuma
The three setting handlers were the same 53-line class three times over, differing in one copy() field, one read, and two message keys. Adding persistence, auditing or a permission check to settings meant writing it three times, and a fourth setting meant a fourth copy. A setting is its key, the messages that report it, and how it is read from and written to PlayerChatSettings - so SettingKey now carries all of that, and SettingHandler is the single mechanism that applies it. The registry keeps its role as the seam where features decide which settings exist, which is why registration is still conditional in LunaticChat. SettingHandlerTest now asserts over every SettingKey rather than repeating five near-identical tests per handler, so a new setting is covered the moment it is declared. It also pins down the property that made the old duplication dangerous: writing one setting must not disturb the others. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02refactor: express command outcomes as message keysSho Sakuma
Eighty-nine call sites spelled out the same four-line nest to say "fail with localized message X": CommandResult.Failure( MessageFormatter.formatError( languageManager.getMessage("channel.ban.noPermission"), ), ) The pairing of formatError with getMessage was a convention held together only by copy-paste, so changing how command errors are presented meant touching all eighty-nine. fail() and ok() on LunaticCommandBase own that pairing now, and the call sites read as the intent: fail("channel.ban.noPermission"). Three when(error) blocks turned out to map every arm to the same message, and ChannelCreateCommand ran two parallel whens over one error to pick a key and its parameters separately. Both were only visible once the noise around them was gone. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02refactor: give subcommands their own base typeSho Sakuma
Subcommands were LunaticCommands that could not be commands. Each of the 17 carried the same three-method preamble plus an override of buildCommand() that only threw, because the contract it inherited - @Command-driven name, aliases and description - has no meaning for a node attached under a parent literal. Accessing name on any of them would have thrown too. LunaticSubCommand now models that node directly, so the preamble and the throwing override are gone. Two knock-on wins: - The permission gate is a declared property instead of applyMethodPermission("build", ...), which looked the method up by name through reflection and returned the builder ungated whenever the lookup or the annotation was missing. A rename or a dropped annotation silently opened the command up; now omitting the gate fails to compile. - Subcommands expose the literal they register under, so ChannelCommand drives both its Brigadier tree and its help output from one list. The two 14-entry lists it maintained by hand could disagree without any compiler or test noticing. ApplyMethodPermissionTest covered the reflection helper that no longer exists; LunaticSubCommandTest covers the gate that replaced it. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02refactor: remove code that no production path reachesSho Sakuma
These were all scaffolding that drifted out of use, and each one costs a reader time before they discover it does nothing: - UUIDASStringSerializer duplicated UUIDSerializer byte for byte; the differing descriptor name never reaches the JSON/YAML wire format, so the choice between them was a coin flip for contributors. - Velocity's BuildInfo was never referenced (the plugin reads its version from PluginContainer) and read a "commit" property the build never wrote, so it would have reported "unknown" had anyone called it. - KanaConverter.TrieNode.Leaf is never constructed: buildTrie starts from a Branch and insert only ever returns Branch. Six branches guarded against a state the type system allowed but the code could not produce. With those gone, isValidRomaji and toHiragana were visibly the same trie walk, so they now share one longestMatch. - @Deprecated command handling had no annotated command to act on. - The settings backup restore looked for *.backup.* files that nothing in the repository writes, so it always fell through to empty settings. Also drops CommandContext.replyWithEvent/replyPlain, PluginCoroutineScope's unused plugin parameter, GitHubRelease fields no caller reads, and four language keys with no lookup site. Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02feat(velocity)!: build against Velocity API 4.0.0 and drop Velocity 3.5.x ↵Sho Sakuma
support Velocity moved to the 4.x generation while platform-velocity still compiled against 3.5.1, so the debug environment had to pin an older proxy than the one most users now run. The migration needed no source changes. Comparing every one of the 213 classes in the API jar with javap shows 4.0.0 is identical to 3.5.1 in its public signatures; the supported protocol range is unchanged too. The one real difference is the POM, which moves adventure-bom from 4.26.1 to 5.2.0. Support for 3.5.x is dropped even though the JAR would still load there, because keeping it meant Adventure 4.26.1 could be the runtime and engine had to stay inside the API surface both Adventure majors share -- a constraint no build step could check. Narrowing to 4.x makes every supported runtime ship Adventure 5.2.0, matching what engine already compiles against, so the constraint is gone rather than merely documented. Nothing enforces the requirement in code, matching how dropping 3.4.0 was handled in v1.1.0. Also drops two dead dependencies: kaml, declared but never imported, worth about 1 MB of shaded JAR, and the velocity-api annotationProcessor, which does nothing without Java sources or kapt.
2026-08-02feat: support Paper 26.2 (Minecraft 26.2)Sho Sakuma
Paper 26.2 bundles Adventure 5.2.0, a major bump from the 4.26.1 shipped by 26.1. Adventure 5 makes ClickEvent generic, so the raw type in CommandContext.replyWithEvent no longer compiled. Auditing the rest of the Adventure 5 removals found no other affected usage. api-version is raised to 26.2, dropping 26.1 support: the two Adventure majors are not binary compatible, so claiming 26.1 compatibility would be a lie. Also fixes two problems found while verifying on a real server: the Velocity healthcheck invoked mc-health, which does not exist in the mc-proxy image, so the container had never once passed its check; and the debug environments pinned no build at all, which meant the proxy came up as 4.1.0-SNAPSHOT while the plugin is compiled against velocity-api 3.5.1. Server versions are now derived by x from the Gradle coordinates so they cannot drift from what the plugin targets.
2026-07-31fix(deps): update ktor monorepo to v3.5.2 (#256)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-28chore(deps): update dependency @biomejs/biome to v2.5.6 (#255)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-22chore(deps): update plugin com.gradleup.shadow to v9.6.1 (#253)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-21chore(deps): update dependency @biomejs/biome to v2.5.5 (#252)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-19docs: add developer design/architecture guide (EN/JA) (#251)Sho Sakuma
* docs: add developer design/architecture guide (EN/JA) The docs site covered features and reference but had no entry point for the codebase's design. Add a Developer Guide section describing the module structure (engine shared kernel, platform-paper, platform-velocity), the protocol-version compatibility model, and the Service Container / Feature Gating pattern, so contributors can understand the architecture without reading the source first. Co-Authored-By: Claude <noreply@anthropic.com> * docs: mirror cross-server DM docs into English (#231) PR #231 added the cross-server direct messaging feature but updated only the Japanese docs. Port the same additions to the English pages (configuration, direct-message, velocity, commands) so both locales stay in sync. Co-Authored-By: Claude <noreply@anthropic.com> * style: apply biome formatting to ja.ts sidebar config The developer-guide sidebar entries were not biome-formatted, failing the build_docs CI check (format:check). Apply the formatter. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-07-16chore(deps): update plugin com.gradleup.shadow to v9.6.0 (#250)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-15chore(deps): update dependency @biomejs/biome to v2.5.4 (#249)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-14fix(deps): update kotlin monorepo to v2.4.10 (#248)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-12fix(deps): update junit-framework monorepo to v6.1.2 (#247)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-11fix(deps): update dependency com.velocitypowered:velocity-api to v3.5.1 (#246)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08chore(deps): update dependency @biomejs/biome to v2.5.3 (#245)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-06fix(deps): update dependency io.papermc.paper:paper-api to ↵renovate[bot]
v26.1.2.build.74-stable (#244) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-06chore(deps): update plugin com.gradleup.shadow to v9.5.1 (#243)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-06chore(deps): update plugin com.gradleup.shadow to v9.5.0 (#242)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-01chore(deps): update dependency @biomejs/biome to v2.5.2 (#241)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-28fix(deps): update junit-framework monorepo to v6.1.1 (#240)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-26chore(deps): update gradle to v9.6.1 (#239)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-26chore(deps): update plugin com.gradleup.shadow to v9.4.3 (#238)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-25fix(deps): update ktor monorepo to v3.5.1 (#237)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-23chore(deps): update dependency @biomejs/biome to v2.5.1 (#236)renovate[bot]
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>