<feed xmlns='http://www.w3.org/2005/Atom'>
<title>LunaticChat.git/platform-velocity, branch renovate/cloudnode-pro-modrinth-publish-digest</title>
<subtitle>A next-generation chat plugin for Paper, Folia and Velocity.
</subtitle>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/'/>
<entry>
<title>docs: stop claiming engine hands down coroutines and ktor</title>
<updated>2026-08-04T16:33:43+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-04T16:33:43+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=c940b9ad7e50e9809e1aded520f0f5c4aff0e741'/>
<id>c940b9ad7e50e9809e1aded520f0f5c4aff0e741</id>
<content type='text'>
Both platform modules described the engine dependency as providing
serialization, coroutines and ktor. Moving romaji conversion out of engine left
it exposing serialization alone, so the note now points readers at dependencies
they have to declare themselves.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Both platform modules described the engine dependency as providing
serialization, coroutines and ktor. Moving romaji conversion out of engine left
it exposing serialization alone, so the note now points readers at dependencies
they have to declare themselves.

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://www.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>perf: take per-message logging off the chat path</title>
<updated>2026-08-04T15:42:23+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-02T15:40:30+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=2d8b613a3ad48acafd683f5286e29c3f1c167275'/>
<id>2d8b613a3ad48acafd683f5286e29c3f1c167275</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>refactor: single-source the plugin messaging channel and dedup cache</title>
<updated>2026-08-02T10:27:51+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-02T10:27:51+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=bf62ebe35653492c123c729ae03cb32d8e20e2e6'/>
<id>bf62ebe35653492c123c729ae03cb32d8e20e2e6</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &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://www.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(velocity)!: build against Velocity API 4.0.0 and drop Velocity 3.5.x support</title>
<updated>2026-08-02T09:34:21+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-02T09:34:21+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=1dd2787059331db57049cb1dac8a38ceea859992'/>
<id>1dd2787059331db57049cb1dac8a38ceea859992</id>
<content type='text'>
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.</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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.</pre>
</div>
</content>
</entry>
<entry>
<title>fix(deps): update dependency com.velocitypowered:velocity-api to v3.5.1 (#246)</title>
<updated>2026-07-11T19:05:35+00:00</updated>
<author>
<name>renovate[bot]</name>
<email>29139614+renovate[bot]@users.noreply.github.com</email>
</author>
<published>2026-07-11T19:05:35+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=3ca44f9c4dc9650135d494db6c8c718e588c5373'/>
<id>3ca44f9c4dc9650135d494db6c8c718e588c5373</id>
<content type='text'>
Co-authored-by: renovate[bot] &lt;29139614+renovate[bot]@users.noreply.github.com&gt;</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Co-authored-by: renovate[bot] &lt;29139614+renovate[bot]@users.noreply.github.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://www.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>chore: Support Velocity 3.5.0</title>
<updated>2026-06-12T07:45:03+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-06-12T07:06:25+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=c1127c4183284623b64b3826b330f79edbc42c3c'/>
<id>c1127c4183284623b64b3826b330f79edbc42c3c</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>fix(deps): update dependency io.mockk:mockk to v1.14.11 (#215)</title>
<updated>2026-05-29T15:33:12+00:00</updated>
<author>
<name>renovate[bot]</name>
<email>29139614+renovate[bot]@users.noreply.github.com</email>
</author>
<published>2026-05-29T15:33:12+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=c9c29071aa669f3c209e590aad7feef2bce19bb7'/>
<id>c9c29071aa669f3c209e590aad7feef2bce19bb7</id>
<content type='text'>
Co-authored-by: renovate[bot] &lt;29139614+renovate[bot]@users.noreply.github.com&gt;</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Co-authored-by: renovate[bot] &lt;29139614+renovate[bot]@users.noreply.github.com&gt;</pre>
</div>
</content>
</entry>
</feed>
