start_all
looked in one plugin directory and there are two, so every first-party plugin
Cordial has ever shipped was listed in Settings, granted, switched on, and never
run.
Built-in plugins now start
plugin_host::start_all discovered manifest::plugin_root() — the user
directory, ~/.local/share/cordial/plugins. The system directory, where every
plugin that ships with Cordial is installed, was read by the settings window to
list them and by flags::collect to read their flag files, and by nothing at
all to run them.
So Discord Rich Presence had never worked, and could not have. The plugin was
present, consented to and granted everything it asked for; its process was never
spawned.
It was found by reading a profile rather than the code. plugin.log had lines
from fps-flex and from nothing else, ever — and fps-flex turned out to be
the one plugin with a copy in the user directory, because it had been
installed by hand during testing. Everything else existed only under
/usr/share/cordial/plugins.
This is also why built-in plugins could not be tested during development.
Packaging did not help; they were never started from anywhere.
Discord Rich Presence
Working, and with rather more than it had. It recovers. The plugin set a presence twice — once at launch, once at ready — and then nothing. Cordial’s broker reconnects to Discord’s socket on everypresence.set, and says so in a comment explaining why it has no retry loop of
its own: the caller is meant to be the retry. Nothing ever was. If Discord was
not running at those two instants, presence never appeared and waiting did not
help. It now re-sends every twenty seconds, which is inside Discord’s rate limit
and picks Discord up within half a minute of it being opened.
A game’s own presence reaches Discord. bloxstrap_rpc had parsed
BloxstrapRPC since presence landed and nothing had ever delivered it — the
module had no callers at all. Cordial now watches the engine’s log, folds the
partial updates a game sends, and publishes them to the plugin.
Two buttons, of the two Discord allows: “Join server” pointing at a
roblox:// deep link naming the actual server instance, or “See game page” when
the instance is not known, and always Cordial’s own link. The deep-link shape
was read from Bloxstrap’s source (MIT) rather than guessed — it had been guessed
at twice before in this project and got wrong both times.
And it no longer says “Starting up” forever. That state only advanced on a
client.ready event that does not always arrive. Discord already renders
“Playing Cordial” from the application itself, so both fields were saying, less
reliably, what the header said anyway.
Controllers
On by default, and with a switch in Settings for turning them off. The caveat is unchanged and worth repeating: the button glyphs Roblox draws may name the wrong brand. Which integer selects which brand is still unestablished. The buttons work; Sober has the same fault (#584, #1810) and nobody there reports a button that stopped working.CORDIAL_GAMEPAD_TYPE=<n>
overrides it, and if you find the value that draws your pad’s own glyphs, please
say so — it settles this for everybody.
A phantom controller was fixed on the way. joydev binds to anything
advertising ABS_X/ABS_Y, so a keyboard remapper’s virtual mouse became
/dev/input/js0 and Roblox was told a pad had arrived with nothing plugged in.
Cordial now applies udev’s own test — a button in BTN_JOYSTICK..BTN_THUMBR —
rather than a list of device names to reject.
Settings
- Frame pacing. Mailbox, FIFO, Immediate, Automatic. Mailbox is the default: FIFO shipped as the default for about an hour and was reported floaty within it, which is a queue against the display clock and is what FIFO is. FIFO saves power and is one row away, with the cost stated on the row.
- FastFlags, edited as text in the same format Bloxstrap uses, so a list pastes straight in. Applying writes the canonical document back into the box, so tabs and stray indentation normalise themselves.
- Controllers, on or off.
- Session: quit when you leave a game, and whether to pass the browser’s sign-in ticket (below).
- Developing a plugin: load one from the folder you are writing it in.
Plugins
- Load unpacked. Point at the folder holding a plugin’s
plugin.json— not a folder plugins are kept in — and it loads from where you are editing it. - It reloads as you edit, using Deno’s own
--watch. Only unpacked plugins: an installed one does not change under a running client. - A plugin that failed to start says so on its row, instead of failing to a
println!that a packaged launch has no terminal for. - A session state API.
state.getreturns the place, universe, server instance, server address, user id and join time. Cordial keeps it so that plugins do not each fold the event stream into a private copy and disagree. It needs a newstate.readcapability —lifecycle.read’s consent text ends “Not what you play”, and this is the one that is. hide-gui, a new built-in. Roblox gates its own interface-hiding shortcuts behind an account being in a particular group; this names one. Bloxstrap’s group is the default because that is the one the shortcuts are documented against. The shortcuts only work if your account is in the group, and Cordial cannot check — if nothing happens, that is the first thing to look at.
Signing in from a browser launch, and why it is off
A play link from roblox.com carries a one-time authentication ticket. That is what makes clicking play sign you in on Windows. Cordial has always dropped it. There is now a switch. It is off, and stays off until somebody tests it, because two things are true at once: it hands a live credential to the engine, and it is not established that this engine accepts one. The old code said the Android client “has no use for” the ticket, which was asserted without a citation and does not survive contact with the binary —AuthTicket,
redeem, /v1/authentication-ticket/redeem and
WebLoginProtocolHandleAuthTicket are all in it. That proves the strings exist
and nothing more. Sober runs the same client and does not do this either.
If you turn it on and clicking play on the website signs you in, that is a
finding worth reporting.
Everything else
Escape has always released a captured pointer, and keeps it released for as long as the game goes on asking — a game that grabs the cursor and will not let go is one key away. That worked for weeks and was documented nowhere. apt, dnf and pacman repositories are published and installing. The system cursor no longer appears when a text box takes focus. A web-view dialog now takes the clicks aimed at it. ADR-029 records what an overlay actually is here, after five separate bugs each blamed on the wrong one of its three parts.What is still broken
Everything 0.12.0 and 0.12.1 listed, unchanged: the startup freeze when signed in, the black canvas inside an experience, and the second half of the CachyOS crash. Text in a box disappears when you click off it. This is a regression and it is ours. The engine draws a text box’s contents once it loses focus — measured here on 2026-08-03, with a table — and something between then and now stopped it. The editor overlay did not exist when that was measured, so the blur path is new since. Not diagnosed. The editor lags a text box that Roblox is animating. Its geometry is polled at 10 Hz against a 60 fps animation, so while a box slides the editor trails it — and clicking off is blocked until it settles, because the region GTK accepts clicks in is at the editor’s last known position. Three fixes in this release had not been seen working when these notes were written. Two of them have since been. See the addendum at the end. The one that stands: the web-view dialog’s clicks and cursor. Those cannot be exercised by the development control surface at all, for the reason 0.12.1’s notes gave — its click verbs call the engine’s input entry points directly, and the checks that matter live in the Wayland pointer listener, which only fires for a real seat.gamepadType’s ordinals are still unestablished. The probe was run and the
screens reachable before signing in draw no per-type glyphs at all, so that
instrument reads out nothing. Settling it needs a controller and a game.
Addendum, 2026-09-01: the Discord presence was tested in a real game
Written after the release, because the notes above say two things that are no longer true and a release note that stays wrong is worse than one that admits a correction. Cordial was run against a signed-in profile and joined a real experience. The measurement is Discord’s own reply to eachSET_ACTIVITY, which echoes what it
stored — quoted here verbatim, with evt: null meaning accepted:
gameInstanceId is the server the client actually joined, byte for byte
the same UUID the engine log named three lines earlier. That is the whole
chain: engine log, log watcher, plugin, broker, Discord.
Thirteen activities went across in one session and every one was accepted: the
launch presence with one button, the game presence with two, the twenty-second
heartbeats — which kept the correct server rather than drifting — and the clear
on shutdown, which Discord confirmed with data: null.
A game’s own BloxstrapRPC reaches Discord too, with details, state and
the game’s timeStart replacing Cordial’s while both buttons survive. This
one is a qualified result and the qualification matters: the experience used
for the join emits no [BloxstrapRPC] lines at all, so the line was appended
to the engine log file the live watcher was really tailing. That exercises
Cordial’s parse, fold, publish and broker path against a real log and a running
client. It does not show that any particular game emits such a line, which is
the game’s business and not Cordial’s.
One bug was found by trying to measure this. The broker read Discord’s
reply and threw it away, so an activity Discord refused — which it refuses
with an equally well-formed frame carrying evt: "ERROR" — was reported to the
plugin as ok and written to the plugin log as ok. Every “came back: ok” in
a log from 0.13.0 or earlier means “Discord answered”, not “Discord accepted”.
Fixed after the release; the log tells the truth from the next version.