<feed xmlns='http://www.w3.org/2005/Atom'>
<title>LunaticChat.git/platform-paper/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://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/'/>
<entry>
<title>feat(conversion): run anthy in-process through WebAssembly</title>
<updated>2026-10-11T02:53:22+00:00</updated>
<author>
<name>Charlie Root</name>
<email>root@dangofactory.local</email>
</author>
<published>2026-10-11T02:53:22+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=a1035c2f2007846d3cf5d07047f47a30815838e6'/>
<id>a1035c2f2007846d3cf5d07047f47a30815838e6</id>
<content type='text'>
Add a second engine for romaji-to-Japanese conversion that leaves the process
alone: anthy, compiled to a WebAssembly module and executed in the JVM by
Chicory's build-time AOT, so conversion needs no outbound request.

anthy/ carries the module sources, the WASI shims, the reproducible build
script, the committed AOT output and the dictionary (~23 MB, extracted into
the plugin data folder on first start; an existing file is left alone).
features.japaneseConversion.engine selects "google" or "anthy", and the anthy
block adds dictionary, workDirectory and sentenceLevel.

RomanjiConverter now sits on a KanjiConverter interface, so a whole message is
handed to the engine at once - anthy segments and reorders a sentence instead
of converting word by word - and the conversion cache is keyed by that
sentence. The engine tests drive the real module, the dictionary tests cover
the extraction, and everything else keeps running on the existing fakes.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Add a second engine for romaji-to-Japanese conversion that leaves the process
alone: anthy, compiled to a WebAssembly module and executed in the JVM by
Chicory's build-time AOT, so conversion needs no outbound request.

anthy/ carries the module sources, the WASI shims, the reproducible build
script, the committed AOT output and the dictionary (~23 MB, extracted into
the plugin data folder on first start; an existing file is left alone).
features.japaneseConversion.engine selects "google" or "anthy", and the anthy
block adds dictionary, workDirectory and sentenceLevel.

RomanjiConverter now sits on a KanjiConverter interface, so a whole message is
handed to the engine at once - anthy segments and reorders a sentence instead
of converting word by word - and the conversion cache is keyed by that
sentence. The engine tests drive the real module, the dictionary tests cover
the extraction, and everything else keeps running on the existing fakes.
</pre>
</div>
</content>
</entry>
<entry>
<title>feat: carry the sender's world in cross-server direct messages</title>
<updated>2026-09-28T14:43:14+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-09-28T14:43:14+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=388ae4ef14ec620e6f1fe2ce8a8ea8e3a816d10d'/>
<id>388ae4ef14ec620e6f1fe2ce8a8ea8e3a816d10d</id>
<content type='text'>
{world} rendered empty for a direct message from another server, which
left formats like "({world})" showing a bare pair of brackets. The relay
now carries the sender's world as an optional field, so this is a PATCH
bump (protocol 1.0.2) and either side can be updated first; an older
sender or proxy still yields an empty value.

The world is read once per message, so the sender, the spies and the
relayed copy can no longer disagree if the sender changes worlds mid-way.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
{world} rendered empty for a direct message from another server, which
left formats like "({world})" showing a bare pair of brackets. The relay
now carries the sender's world as an optional field, so this is a PATCH
bump (protocol 1.0.2) and either side can be updated first; an older
sender or proxy still yields an empty value.

The world is read once per message, so the sender, the spies and the
relayed copy can no longer disagree if the sender changes worlds mid-way.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>fix(paper): splice display names as components and share role labels</title>
<updated>2026-09-28T14:43:13+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-09-28T14:43:13+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=a22fce70db7b43aadd8baa42e87a4430a8b1e05f'/>
<id>a22fce70db7b43aadd8baa42e87a4430a8b1e05f</id>
<content type='text'>
Serializing the display name into the legacy text let its color run into
everything after it, and legacy text cannot hold hex colors or hover and
click events, so names styled by other plugins came out wrong. The name is
now spliced into the parsed component instead.

{role} is resolved only when the format uses it, since the default
formats do not and it costs a member scan and a lookup per message.
/lc channel status now shows the same localized role names instead of a
second, hard-coded [OWNER] / [MOD] vocabulary that only one of the two
places translated.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Serializing the display name into the legacy text let its color run into
everything after it, and legacy text cannot hold hex colors or hover and
click events, so names styled by other plugins came out wrong. The name is
now spliced into the parsed component instead.

{role} is resolved only when the format uses it, since the default
formats do not and it costs a member scan and a lookup per message.
/lc channel status now shows the same localized role names instead of a
second, hard-coded [OWNER] / [MOD] vocabulary that only one of the two
places translated.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>feat(paper): add world, display_name, channel_id and role chat placeholders</title>
<updated>2026-09-28T14:00:29+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-09-28T14:00:29+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=af2feee95e7f6ebaef5466402789917ae65c0796'/>
<id>af2feee95e7f6ebaef5466402789917ae65c0796</id>
<content type='text'>
Operators asked for more of the sender's context in chat formats than the
name alone. {world} covers direct messages and channel chat; the channel
details and the sender's display name and role only exist for channel
chat, so only that format accepts them.

A direct message from another server renders {world} empty: the relay does
not carry the sender's world, and adding it would need a protocol bump and
a coordinated Velocity release for one placeholder. The renderer now
accepts underscores so the snake_case names match as written.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Operators asked for more of the sender's context in chat formats than the
name alone. {world} covers direct messages and channel chat; the channel
details and the sender's display name and role only exist for channel
chat, so only that format accepts them.

A direct message from another server renders {world} empty: the relay does
not carry the sender's world, and adding it would need a protocol bump and
a coordinated Velocity release for one placeholder. The renderer now
accepts underscores so the snake_case names match as written.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>refactor(paper): render chat formats through one placeholder vocabulary</title>
<updated>2026-09-28T13:21:21+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-09-28T13:21:21+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=9601f147dd38d5fc93d8fff118794990f44c3766'/>
<id>9601f147dd38d5fc93d8fff118794990f44c3766</id>
<content type='text'>
Placeholder names were spelled as string literals in three handlers, and
each handler also chose its own way of turning the result into a
Component. Adding a placeholder meant finding every call site by hand, with
nothing to catch a format left without its value.

ChatPlaceholder now owns the names, and each format has an entry point whose
parameters are exactly the placeholders it accepts, so the compiler points
at the callers when one is added. Substitution happens in a single pass, so
a value that itself contains "{message}" is no longer replaced again by the
next placeholder. All three formats now deserialize section signs into
styles, which leaves what players see unchanged.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Placeholder names were spelled as string literals in three handlers, and
each handler also chose its own way of turning the result into a
Component. Adding a placeholder meant finding every call site by hand, with
nothing to catch a format left without its value.

ChatPlaceholder now owns the names, and each format has an entry point whose
parameters are exactly the placeholders it accepts, so the compiler points
at the callers when one is added. Substitution happens in a single pass, so
a value that itself contains "{message}" is no longer replaced again by the
next placeholder. All three formats now deserialize section signs into
styles, which leaves what players see unchanged.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>fix(paper): find the newest Paper release instead of trusting releases/latest</title>
<updated>2026-09-28T12:35:52+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-09-28T12:35:52+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=780caea1697827548f2fd08ae2eb85f973187248'/>
<id>780caea1697827548f2fd08ae2eb85f973187248</id>
<content type='text'>
The update checker read releases/latest and stripped a leading "v", so any
release whose tag was not a bare vX.Y.Z (paper/v1.2.x, or a Velocity release
published last) parsed as 0.x and no update was ever reported. It now lists
recent releases and picks the highest Paper tag, skipping drafts,
prereleases and non-Paper tags, so a Velocity-only release can no longer
hide a Paper update.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The update checker read releases/latest and stripped a leading "v", so any
release whose tag was not a bare vX.Y.Z (paper/v1.2.x, or a Velocity release
published last) parsed as 0.x and no update was ever reported. It now lists
recent releases and picks the highest Paper tag, skipping drafts,
prereleases and non-Paper tags, so a Velocity-only release can no longer
hide a Paper update.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>fix(paper): stop the command category logging on every tree build</title>
<updated>2026-08-30T12:11:46+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-30T12:11:46+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=1c79bee8f3c8fe20582afa96527cd298c043daa5'/>
<id>1c79bee8f3c8fe20582afa96527cd298c043daa5</id>
<content type='text'>
Three findings from review of #296.

Brigadier evaluates a `requires` predicate when it builds the command tree it
sends to a player - on every join and every updateCommands() - not when someone
is refused. The denial line sitting there put one entry per gated node per
player in the log, burying the command results the category exists to show. It
is dropped rather than moved: a hidden node is never reached by an execution, so
there is no refusal path to log from.

`all` was honoured by `debug: all` and `/lc debug all on` but not by
`categories: [all]`, which resolved to an unknown name - leaving `enabled: true`
with nothing switched on, reported as off by /lc status, explained only by a
warning in the log. The word now lives in one place that all three read.

Two dumps in the same second left one file, because the name is only accurate to
the second and the write replaces. Reproducing a bug means dumping either side of
the step that triggers it, so the second one now takes a suffix.

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

Brigadier evaluates a `requires` predicate when it builds the command tree it
sends to a player - on every join and every updateCommands() - not when someone
is refused. The denial line sitting there put one entry per gated node per
player in the log, burying the command results the category exists to show. It
is dropped rather than moved: a hidden node is never reached by an execution, so
there is no refusal path to log from.

`all` was honoured by `debug: all` and `/lc debug all on` but not by
`categories: [all]`, which resolved to an unknown name - leaving `enabled: true`
with nothing switched on, reported as off by /lc status, explained only by a
warning in the log. The word now lives in one place that all three read.

Two dumps in the same second left one file, because the name is only accurate to
the second and the write replaces. Reproducing a bug means dumping either side of
the step that triggers it, so the second one now takes a suffix.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>refactor(paper): let /lc dump name the folder instead of the file</title>
<updated>2026-08-30T11:51:20+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-30T11:51:20+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=68052ce335ae11c4cb10a388848b42571bfc65b9'/>
<id>68052ce335ae11c4cb10a388848b42571bfc65b9</id>
<content type='text'>
The reply carried the report's path so the operator could find the file. But
`lunaticchat.command.lc.dump` can be granted to someone with no filesystem access
at all, and to them a path is not something they can act on - it only says how
the server is laid out. Relativising it against the server directory narrowed
that without removing it.

Naming the folder in the message says everything a reader can use, and leaves the
exact path to the server log, which only someone with that access can read
anyway. That also makes the reply a single message rather than a message plus a
component the language files could not translate.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The reply carried the report's path so the operator could find the file. But
`lunaticchat.command.lc.dump` can be granted to someone with no filesystem access
at all, and to them a path is not something they can act on - it only says how
the server is laid out. Relativising it against the server directory narrowed
that without removing it.

Naming the folder in the message says everything a reader can use, and leaves the
exact path to the server log, which only someone with that access can read
anyway. That also makes the reply a single message rather than a message plus a
component the language files could not translate.

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Merge branch 'main' into improve/debug-feature</title>
<updated>2026-08-30T11:45:43+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-30T11:45:43+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=13e0e91ad0e583816246bf2a851f9dafdb245d58'/>
<id>13e0e91ad0e583816246bf2a851f9dafdb245d58</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>feat(paper): make the debug switch categorised, live and dumpable</title>
<updated>2026-08-30T11:43:36+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-30T11:43:36+00:00</published>
<link rel='alternate' type='text/html' href='https://www.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=85fad9cf81c45b16dc9d0c3310bdbf0eea694723'/>
<id>85fad9cf81c45b16dc9d0c3310bdbf0eea694723</id>
<content type='text'>
config.yml now accepts `debug: velocity,protocol` and a block with `enabled` and
`categories` beside the boolean it always took. Categories exist so that
instrumenting the chat path does not bury the handshake an operator is chasing
under one line per message.

The setting is read by inspecting the YAML node by hand rather than through a
data class, because one key has to accept both a scalar and a map and kaml picks
the input to build from the descriptor kind alone - a class descriptor rejects
`debug: true` before the serializer is ever called. An unknown category name is
collected rather than thrown: throwing would make ConfigManager prune the whole
`debug` key and switch off the categories the operator did spell correctly.

`debug` moves from RESTART_REQUIRED to applied, since a reload only has to
replace a DebugState. `/lc debug` switches a category for the running server and
deliberately does not write config.yml back - re-emitting a commented YAML
document from a parsed tree loses every comment and every bit of formatting the
operator put there, and a restart or `/lc reload` putting the file back in charge
is the safer default anyway.

`/lc dump` writes the report a bug report needs: versions, enabled features,
connection state and how full each store is. It carries no message text, player
name or UUID, because it exists to be pasted into a public issue by someone who
is not reading it first. Its reply names the file relative to the server
directory - the command can be granted to someone with no filesystem access, to
whom an absolute path discloses where the server is installed and nothing they
can act on.

The KtorSimpleLogger calls in ChannelMessageHandler and ChannelMessageLogger are
folded into the same logger, so there is one place that decides whether a
diagnostic line is printed.

Refs #228

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
config.yml now accepts `debug: velocity,protocol` and a block with `enabled` and
`categories` beside the boolean it always took. Categories exist so that
instrumenting the chat path does not bury the handshake an operator is chasing
under one line per message.

The setting is read by inspecting the YAML node by hand rather than through a
data class, because one key has to accept both a scalar and a map and kaml picks
the input to build from the descriptor kind alone - a class descriptor rejects
`debug: true` before the serializer is ever called. An unknown category name is
collected rather than thrown: throwing would make ConfigManager prune the whole
`debug` key and switch off the categories the operator did spell correctly.

`debug` moves from RESTART_REQUIRED to applied, since a reload only has to
replace a DebugState. `/lc debug` switches a category for the running server and
deliberately does not write config.yml back - re-emitting a commented YAML
document from a parsed tree loses every comment and every bit of formatting the
operator put there, and a restart or `/lc reload` putting the file back in charge
is the safer default anyway.

`/lc dump` writes the report a bug report needs: versions, enabled features,
connection state and how full each store is. It carries no message text, player
name or UUID, because it exists to be pasted into a public issue by someone who
is not reading it first. Its reply names the file relative to the server
directory - the command can be granted to someone with no filesystem access, to
whom an absolute path discloses where the server is installed and nothing they
can act on.

The KtorSimpleLogger calls in ChannelMessageHandler and ChannelMessageLogger are
folded into the same logger, so there is one place that decides whether a
diagnostic line is printed.

Refs #228

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