<feed xmlns='http://www.w3.org/2005/Atom'>
<title>LunaticChat.git/engine/src/test, branch main</title>
<subtitle>A next-generation chat plugin for Paper, Folia and Velocity.
</subtitle>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/'/>
<entry>
<title>refactor: keep rendering out of CommandResult</title>
<updated>2026-08-04T16:16:03+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-03T06:19:08+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=d45db164428bb4b350c879bf3f43ceb1cc581955'/>
<id>d45db164428bb4b350c879bf3f43ceb1cc581955</id>
<content type='text'>
CommandResult carried Adventure Components, which was engine's last
Minecraft dependency and the reason CLAUDE.md's "engine has no Minecraft
platform dependencies" was not quite true. It also meant a command could
not report a result without having already decided how it looks: every
site had to pick formatError versus format before it could return.

Results now carry text, and LunaticCommandBase.handleResult is the single
place that styles it - error red for Failure, normal for
SuccessWithMessage. The fail()/ok() helpers from #260 already funnelled
every call site through two functions, so this is a change to those two
plus the one command that composes its own success text.

engine's dependency list is down to kotlinx-serialization, and nothing
under engine/src references net.kyori, org.bukkit, com.velocitypowered or
io.papermc.

Not done: the review also proposed collapsing the per-command
`when (error)` blocks into one exception-to-key table. Those blocks pick
wording, not just a key - "only owners can delete this channel" reads
differently in the ban command than the delete command - so a shared table
would hand every caller the same sentence and need per-command overrides
on top. Left alone deliberately.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
CommandResult carried Adventure Components, which was engine's last
Minecraft dependency and the reason CLAUDE.md's "engine has no Minecraft
platform dependencies" was not quite true. It also meant a command could
not report a result without having already decided how it looks: every
site had to pick formatError versus format before it could return.

Results now carry text, and LunaticCommandBase.handleResult is the single
place that styles it - error red for Failure, normal for
SuccessWithMessage. The fail()/ok() helpers from #260 already funnelled
every call site through two functions, so this is a change to those two
plus the one command that composes its own success text.

engine's dependency list is down to kotlinx-serialization, and nothing
under engine/src references net.kyori, org.bukkit, com.velocitypowered or
io.papermc.

Not done: the review also proposed collapsing the per-command
`when (error)` blocks into one exception-to-key table. Those blocks pick
wording, not just a key - "only owners can delete this channel" reads
differently in the ban command than the delete command - so a shared table
would hand every caller the same sentence and need per-command overrides
on top. Left alone deliberately.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>refactor: make the direct message failure reason an enum</title>
<updated>2026-08-04T16:15:29+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-03T06:12:42+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=daf9060561656f468a0ae366e5695f5eb74f48f7'/>
<id>daf9060561656f468a0ae366e5695f5eb74f48f7</id>
<content type='text'>
DirectMessageError.reason was a String backed by two constants, so the
receiving side matched one case and let everything else fall through to
"the target is offline". Adding a third reason on the proxy would have
shipped it to Paper servers that silently reported the wrong thing - the
one string-keyed dispatch sitting next to a protocol layer whose messages
are otherwise a sealed hierarchy with exhaustiveness checking.

As an enum, the reader must decide what to show for each case, and
CrossServerDirectMessageManager's when no longer needs an else.

The wire format is unchanged: kotlinx serializes an enum as its name, so
the existing snapshots still decode. What did need care is the reverse
direction - a reason from a newer proxy would now fail to parse, where the
String version degraded. The property has a default and the codec enables
coerceInputValues, so an unknown reason lands on TARGET_OFFLINE, exactly
the old else branch. There is a compatibility test for that case.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
DirectMessageError.reason was a String backed by two constants, so the
receiving side matched one case and let everything else fall through to
"the target is offline". Adding a third reason on the proxy would have
shipped it to Paper servers that silently reported the wrong thing - the
one string-keyed dispatch sitting next to a protocol layer whose messages
are otherwise a sealed hierarchy with exhaustiveness checking.

As an enum, the reader must decide what to show for each case, and
CrossServerDirectMessageManager's when no longer needs an else.

The wire format is unchanged: kotlinx serializes an enum as its name, so
the existing snapshots still decode. What did need care is the reverse
direction - a reason from a newer proxy would now fail to parse, where the
String version degraded. The property has a default and the codec enables
coerceInputValues, so an unknown reason lands on TARGET_OFFLINE, exactly
the old else branch. There is a compatibility test for that case.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>refactor: move romaji conversion out of engine</title>
<updated>2026-08-04T16:15:29+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-03T06:09:58+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=fd56ebaa1dc2cfa8f834edb06395e8fc02375d1d'/>
<id>fd56ebaa1dc2cfa8f834edb06395e8fc02375d1d</id>
<content type='text'>
Closes #259.

engine exposed ktor through api(), so both platforms inherited the client
and its CIO engine. The only thing in engine that used ktor was
GoogleIMEClient, and the only module that used GoogleIMEClient was
platform-paper - Velocity was shipping roughly six megabytes of HTTP
client to support a Paper-only feature. Same story for
kotlinx-coroutines-core, which Velocity does not use at all.

Romaji conversion is a Paper feature, so the converter package now lives
in platform-paper alongside the ConversionCache and RomanjiConverter that
were already there. engine keeps kotlinx-serialization on api(), which is
genuine shared surface: the plugin messaging protocol is built on it.

The velocity shadow jar goes from 7,618,405 to 2,769,395 bytes, and no
longer contains io/ktor at all.

CacheData's tests were sitting inside engine's SettingsDataClassesTest,
which is unrelated to settings; they move with the class.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Closes #259.

engine exposed ktor through api(), so both platforms inherited the client
and its CIO engine. The only thing in engine that used ktor was
GoogleIMEClient, and the only module that used GoogleIMEClient was
platform-paper - Velocity was shipping roughly six megabytes of HTTP
client to support a Paper-only feature. Same story for
kotlinx-coroutines-core, which Velocity does not use at all.

Romaji conversion is a Paper feature, so the converter package now lives
in platform-paper alongside the ConversionCache and RomanjiConverter that
were already there. engine keeps kotlinx-serialization on api(), which is
genuine shared surface: the plugin messaging protocol is built on it.

The velocity shadow jar goes from 7,618,405 to 2,769,395 bytes, and no
longer contains io/ktor at all.

CacheData's tests were sitting inside engine's SettingsDataClassesTest,
which is unrelated to settings; they move with the class.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>refactor: fold the remaining small duplications</title>
<updated>2026-08-02T10:45:33+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-02T10:45:33+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=7bcaaf9a305c2a8608a420c8f05521bd2de089dd'/>
<id>7bcaaf9a305c2a8608a420c8f05521bd2de089dd</id>
<content type='text'>
- 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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
- 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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>refactor: remove code that no production path reaches</title>
<updated>2026-08-02T10:10:56+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-02T10:10:46+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=bc0d7d522e169f3eeb8d5b632ee673392060694b'/>
<id>bc0d7d522e169f3eeb8d5b632ee673392060694b</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>feat: add cross-server direct messaging via Velocity</title>
<updated>2026-06-17T07:15:40+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-06-17T07:15:40+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=c678bd15324dad0519cb1d9a4c58202a981ee8e2'/>
<id>c678bd15324dad0519cb1d9a4c58202a981ee8e2</id>
<content type='text'>
Allow /tell and /reply to reach players on other Paper servers behind a
Velocity proxy using the "&lt;player&gt;@&lt;server&gt;" target syntax.

Engine (protocol bumped 1.0.0 -&gt; 1.0.1, optional sub-channels):
- Add DirectMessageRelay, DirectMessageError, PresenceSnapshot/PresenceEntry
  and PresenceRequest messages plus codec branches.

Velocity:
- CrossServerDirectMessageRelay routes a DM to the target server (or returns
  a delivery error to the source).
- PresenceTracker broadcasts proxy-wide presence snapshots on join/quit/switch
  and on request.

Paper:
- RemotePlayerRegistry caches proxy presence for completion and remote target
  resolution.
- CrossServerDirectMessageManager handles send/receive/error and dedup.
- DirectMessageHandler reply state generalized to ReplyTarget (Local/Remote)
  so /reply works across servers.
- TellCommand parses "name@server", completes local names and remote
  name@server targets, and uses exact local name matching.
- New crossServerDirectMessage config flag and i18n keys (en/ja).

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Allow /tell and /reply to reach players on other Paper servers behind a
Velocity proxy using the "&lt;player&gt;@&lt;server&gt;" target syntax.

Engine (protocol bumped 1.0.0 -&gt; 1.0.1, optional sub-channels):
- Add DirectMessageRelay, DirectMessageError, PresenceSnapshot/PresenceEntry
  and PresenceRequest messages plus codec branches.

Velocity:
- CrossServerDirectMessageRelay routes a DM to the target server (or returns
  a delivery error to the source).
- PresenceTracker broadcasts proxy-wide presence snapshots on join/quit/switch
  and on request.

Paper:
- RemotePlayerRegistry caches proxy presence for completion and remote target
  resolution.
- CrossServerDirectMessageManager handles send/receive/error and dedup.
- DirectMessageHandler reply state generalized to ReplyTarget (Local/Remote)
  so /reply works across servers.
- TellCommand parses "name@server", completes local names and remote
  name@server targets, and uses exact local name matching.
- New crossServerDirectMessage config flag and i18n keys (en/ja).

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>test: Add protocol version test-case</title>
<updated>2026-04-04T12:53:56+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-04-04T12:53:56+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=a20c385528fc1d7515ded0c0978cda05e66c3c98'/>
<id>a20c385528fc1d7515ded0c0978cda05e66c3c98</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>feat: Improving Velocity's Cycling Compatibility</title>
<updated>2026-04-04T11:26:37+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-04-04T11:26:37+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=2b0829856c9a24b90a9248d28c7de84101b83232'/>
<id>2b0829856c9a24b90a9248d28c7de84101b83232</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>feat!: Remove ChatMode feature, followback channel chat mode</title>
<updated>2026-03-17T11:34:18+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-03-17T11:34:18+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=634dd975038bb5b845c28ca6c96aa285ef94e965'/>
<id>634dd975038bb5b845c28ca6c96aa285ef94e965</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>test: expand test coverage across all modules</title>
<updated>2026-02-25T15:17:21+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-02-25T15:17:21+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=70c785eee4c61601ba7fffa17cc8b992b4793491'/>
<id>70c785eee4c61601ba7fffa17cc8b992b4793491</id>
<content type='text'>
- Add 23 new test files covering exceptions, data classes, permissions,
  setting handlers, commands, and cross-server relay
- Change command execute methods from private to internal visibility
  to enable unit testing (16 command files)
- Engine coverage: 65.5% → 92.4%
- Platform-paper coverage: 16.8% → 40.0%
- Platform-velocity coverage: 62.4% → 76.8%

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
- Add 23 new test files covering exceptions, data classes, permissions,
  setting handlers, commands, and cross-server relay
- Change command execute methods from private to internal visibility
  to enable unit testing (16 command files)
- Engine coverage: 65.5% → 92.4%
- Platform-paper coverage: 16.8% → 40.0%
- Platform-velocity coverage: 62.4% → 76.8%

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
</feed>
