<feed xmlns='http://www.w3.org/2005/Atom'>
<title>LunaticChat.git/platform-paper/src/main/resources, 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>docs: name the anthy engine in the project description</title>
<updated>2026-10-11T03:31:28+00:00</updated>
<author>
<name>Charlie Root</name>
<email>root@dangofactory.local</email>
</author>
<published>2026-10-11T03:31:28+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=d06ef045d846a751d92936c223ac8f2d2f71efb4'/>
<id>d06ef045d846a751d92936c223ac8f2d2f71efb4</id>
<content type='text'>
The tagline and both plugin descriptors now say that romaji conversion can run
inside the server through anthy, so the engine is visible wherever the project
describes itself: the README, the website hero and meta description, the API
documentation, and the Paper and Velocity plugin descriptors.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The tagline and both plugin descriptors now say that romaji conversion can run
inside the server through anthy, so the engine is visible wherever the project
describes itself: the README, the website hero and meta description, the API
documentation, and the Paper and Velocity plugin descriptors.
</pre>
</div>
</content>
</entry>
<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://openpgpkey.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://openpgpkey.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>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://openpgpkey.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): 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://openpgpkey.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>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://openpgpkey.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>
<entry>
<title>chore: update japanese i18n settings</title>
<updated>2026-08-30T04:38:34+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-30T04:38:34+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=ddc22453396ea56cb3bcb63f543a88d682e525a3'/>
<id>ddc22453396ea56cb3bcb63f543a88d682e525a3</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>refactor(paper): reword the /lc reload messages and mark the untranslated lines</title>
<updated>2026-08-10T09:43:12+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-10T09:43:12+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=8c19c4f6d9db58fe7340e72a8884fc0734c75ad7'/>
<id>8c19c4f6d9db58fe7340e72a8884fc0734c75ad7</id>
<content type='text'>
The Japanese wording was revised in place: the messages now say "the
configuration file" rather than naming config.yml at the operator, and they
follow the punctuation this file already used for in-game text. English is
brought in line with that wording, keeping its own trailing periods, which
the rest of en.yml uses on every full sentence.

The three failure headings now carry a comment saying what is printed
beneath them and why those lines are never translated. A translator has no
way to see that from the file alone, and the two reasons it cannot change
are both easy to undo by accident: the text comes from the YAML parser, so a
parser update would silently age any translation kept here, and it cannot be
passed as a placeholder because a reason holding braces of its own is broken
by the next substitution and then coloured as if the braces were ours.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The Japanese wording was revised in place: the messages now say "the
configuration file" rather than naming config.yml at the operator, and they
follow the punctuation this file already used for in-game text. English is
brought in line with that wording, keeping its own trailing periods, which
the rest of en.yml uses on every full sentence.

The three failure headings now carry a comment saying what is printed
beneath them and why those lines are never translated. A translator has no
way to see that from the file alone, and the two reasons it cannot change
are both easy to undo by accident: the text comes from the YAML parser, so a
parser update would silently age any translation kept here, and it cannot be
passed as a placeholder because a reason holding braces of its own is broken
by the next substitution and then coloured as if the braces were ours.
</pre>
</div>
</content>
</entry>
<entry>
<title>feat(paper): report /lc reload as a single-line result</title>
<updated>2026-08-10T09:18:16+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-10T09:18:16+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=4a5570973319d992f21ed01ab20e8dc9fa721d81'/>
<id>4a5570973319d992f21ed01ab20e8dc9fa721d81</id>
<content type='text'>
The reply listed every setting that had moved and every setting that still
needed a restart, as dotted keys. That buried the one bit that changes what
the operator does next - whether they have to restart - under names they had
just typed themselves and did not need read back to them.

The detail is not lost: which settings moved was already written to the
server log on every reload, which is the better home for it anyway, since a
format change can be dated there long after the chat line has scrolled away.

A pending restart now outranks "nothing changed" even when nothing could be
applied. Reporting no change to someone who did edit the file would send
them hunting for a typo they had not made.

The failure path still names each unreadable setting and the reason it was
rejected, because that is a list the operator cannot reconstruct and needs
in order to fix the file.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The reply listed every setting that had moved and every setting that still
needed a restart, as dotted keys. That buried the one bit that changes what
the operator does next - whether they have to restart - under names they had
just typed themselves and did not need read back to them.

The detail is not lost: which settings moved was already written to the
server log on every reload, which is the better home for it anyway, since a
format change can be dated there long after the chat line has scrolled away.

A pending restart now outranks "nothing changed" even when nothing could be
applied. Reporting no change to someone who did edit the file would send
them hunting for a typo they had not made.

The failure path still names each unreadable setting and the reason it was
rejected, because that is a list the operator cannot reconstruct and needs
in order to fix the file.
</pre>
</div>
</content>
</entry>
<entry>
<title>feat(paper): add /lc reload to apply message formats without a restart</title>
<updated>2026-08-09T23:31:20+00:00</updated>
<author>
<name>Sho Sakuma</name>
<email>me@m1sk9.dev</email>
</author>
<published>2026-08-09T23:31:20+00:00</published>
<link rel='alternate' type='text/html' href='https://openpgpkey.dangofactory.net/cgit/cgit.cgi/LunaticChat.git/commit/?id=f08d4d8f0557581d93063cd9f6e750ab3ac9cc17'/>
<id>f08d4d8f0557581d93063cd9f6e750ab3ac9cc17</id>
<content type='text'>
Changing a message format meant taking the server down, because config.yml
was read once in onEnable and every consumer held the value it was built
with. Operators tune these formats far more often than anything else in the
file, and a restart is a disproportionate price for a colour code.

Only messageFormat is applied. The rest of config.yml settles things once,
at startup - which services exist, which commands and listeners are
registered, which files are opened - and none of that can be redone while
the server runs: Paper registers commands only through its COMMANDS
lifecycle event, Bukkit cannot unregister a listener, and the conversion
cache saver keeps no task handle to cancel. Rebuilding the services would
leave the old ones still wired up, so a changed setting the server cannot
pick up is reported back as needing a restart rather than silently ignored.

MessageFormatHolder is deliberately narrower than LunaticChatConfiguration:
taking it says the value is live, taking the configuration says it was
frozen at startup. A holder over the whole tree would hold a configuration
matching no version of the file, since only messageFormat is ever swapped.

Reload reads config.yml more strictly than startup does, because it has an
option startup does not - leaving the running configuration alone. Startup
falls a single unreadable setting back to its default so the server can come
up; reload refuses the file, names every unreadable setting at once, and
keeps what the server already had. A document holding no settings is refused
for the same reason: it cannot be told apart from a config.yml caught
halfway through being written, and resetting every format over that is worse
than doing nothing.

Closes #266

Co-Authored-By: Claude &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Changing a message format meant taking the server down, because config.yml
was read once in onEnable and every consumer held the value it was built
with. Operators tune these formats far more often than anything else in the
file, and a restart is a disproportionate price for a colour code.

Only messageFormat is applied. The rest of config.yml settles things once,
at startup - which services exist, which commands and listeners are
registered, which files are opened - and none of that can be redone while
the server runs: Paper registers commands only through its COMMANDS
lifecycle event, Bukkit cannot unregister a listener, and the conversion
cache saver keeps no task handle to cancel. Rebuilding the services would
leave the old ones still wired up, so a changed setting the server cannot
pick up is reported back as needing a restart rather than silently ignored.

MessageFormatHolder is deliberately narrower than LunaticChatConfiguration:
taking it says the value is live, taking the configuration says it was
frozen at startup. A holder over the whole tree would hold a configuration
matching no version of the file, since only messageFormat is ever swapped.

Reload reads config.yml more strictly than startup does, because it has an
option startup does not - leaving the running configuration alone. Startup
falls a single unreadable setting back to its default so the server can come
up; reload refuses the file, names every unreadable setting at once, and
keeps what the server already had. A document holding no settings is refused
for the same reason: it cannot be told apart from a config.yml caught
halfway through being written, and resetting every format over that is worse
than doing nothing.

Closes #266

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