Skip to main content
Fifty-nine commits. The one that matters most is a single line: 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 every presence.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.get returns 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 new state.read capability — 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 each SET_ACTIVITY, which echoes what it stored — quoted here verbatim, with evt: null meaning accepted:
The 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.