Skip to content

Blog

Component Cartography: Fixing moonlight in record time

A recent Discord update broke a lot of client mods, including moonlight. This is a stressful situation to be in, as a whole chunk of moonlight users now had broken clients! Discord client modding moves exceptionally fast compared to other software or games, because updates happen up to several times a day on Canary.

The most important part about these huge breaking updates is to make people’s experience have as little friction as possible. If you wanted to play some games with your friends and you opened your Discord client to a crash screen, that can ruin your mood and cause you to waste precious time getting things working. We try and move as fast as possible for this reason, while also offering people the ability to quickly unpatch moonlight if they need to.

We were able to fix moonlight within the day, which is a huge accomplishment for our team - out of the four moonlight core developers, only two were around to work on this! Thanks to several hours of almost non-stop effort, users on Discord Canary only had broken clients for a few hours, and users on Discord Stable only had broken clients for a few minutes.

If it ain’t broke, break it

The breaking change in question revolves around Discord’s components. Discord uses React for the user interface, which represents parts of the UI as reusable components. Discord has a file in their source code that exports a lot of reused components (like buttons, sliders, text, and more). We gave this module the name of discord/components/common/index.

For years, we’ve been lucky with this module, as it contains the names of all of the components. Usually, Discord code is minified, which means most variable names are a small sequence of unreadable letters instead of proper names. This module wasn’t minified, which means that we could refer to components by name like “Card” or “Checkbox” instead of “wGF” and “nn4”.

A screenshot of the unminified components module in DevTools

However, this all changed with a Discord update - one that presumably updated Rspack, their bundler of choice. This meant the components module now looked like this:

A screenshot of the minified components module in DevTools

Not good! Almost every extension that interacted with the UI in some way was now broken, because the components it was trying to use couldn’t be found. This meant that Discord crashed on startup, and Moonbase (the settings UI) was unavailable. Even our crash screen utilities broke, so the only way to fix your client was by updating through the system tray icon or opening the installer.

Not all hope was lost, though! A lucky part about this update was that we saw a lot of it coming. Discord tried to make this change several times over the past month, reverting it every time, so we always had a glimpse of what was broken and how to fix it.

Another good thing is that all of these components were imported through mappings. Several months ago, we started a project to coordinate modules in the Discord client. This allows us to locate modules and give them stable names, but it also has a special superpower within it: it can take a module’s exports and dynamically alias the export names.

This got us thinking: if we were to round up a list of most of the components that were used in moonlight extensions, and manually map them all, would everything just work without issue? It turns out, mostly, yes!

Gotta map ‘em all

This update was also pushed and reverted yesterday, so we had some time to map the components. We started by going to Discord Stable (where the update hadn’t been released yet), and we created a mapping of component names to the function source (using a JSON.stringify replacer to properly serialize it).

Next, we looked through moonlight’s source code, along with a few other extensions in the official repository, and made a list of every component that was used. We combined this with the list of components that we had typed to get a good estimation of what we needed to fix.

This was only about ~100 components that needed to be mapped overall, which wasn’t that bad. The process was easy, but tedious, as it involved getting creative with how to uniquely match the component. Let’s break down the mapping process, starting with this example find:

moonmap.addExport(name, "NumberInputStepper", {
type: ModuleExportType.Function,
find: "__invalid_subtract"
});

Imagine that you worked at a moonlight factory producing Webpack module names (which is definitely a real place, don’t ask any questions). As modules are loaded into the Discord client, a worker earlier on the factory line identifies a module as the one where the components are stored. Your job would be to take the exports of that module and press Ctrl+F on your keyboard to find the text __invalid_subtract.

JavaScript has a fun quirk where you can do .toString() on a function to get the source code of that function. This is how most of moonlight works at its core, and it works the same here. We loop through every single export, turn it into a string, and look for the find inside of it.

This process is automated to do it all in one go, completely dynamically. The module loads, it gets identified, and then its exports are scanned and remapped. The mappings sytem then emits a Webpack module with a unique name, and other modules can load it and use the components.

We had to add one small change to map these functions, which is that some components had their function inside of an object (e.g. when using refs, it would be { render: function }). We just added a recursive setting to scan functions inside of objects.

There’s also a few other mappings types, like this one:

moonmap.addExport(name, "ModalSize", {
type: ModuleExportType.Key,
find: "DYNAMIC"
});

The ModalSize object looks like this:

{
"SMALL": "small",
"MEDIUM": "medium",
"LARGE": "large",
"DYNAMIC": "dynamic"
}

This looks for an export that is an object that has the key of DYNAMIC in it. It also can check for object values or key/value pairs.

After a lot of work, we were able to combine these techniques to map most of what was needed. Some components were unfortunately lost, though:

  • Some were just removed directly in this update, and were no longer in the module at all. A lot of icons were unfortunately removed.
  • Some were not unique enough to identify them by value alone. For example, useModalsStore has the function string of e=>s(n,e), which is simply not unique enough to reliably match.
  • Some are still in the file, but we didn’t remap them yet. We tried our best to map everything that was used, but some things may be missing, and we’ll be working on pushing out updates for the missing components that are wanted.

Fixing functionality, fast

We became aware of the Discord update at around 6 pm UTC on the 28th, and had it fixed by about 7:30 pm UTC. We waited until 11 pm UTC to release our fixes to the moonlight nightly branch, as we were unsure if they would revert the update on Canary.

moonlight is split into two branches: Stable and Nightly. The Nightly branch is intended to be fast-paced development snapshots that matches closely with Discord Canary, and the Stable branch is intended to just work for users and match closely with Discord Stable.

We received word that Discord published the update with the relevant changes to Stable, so we began preparing for a moonlight release. Mere seconds away from releasing the update, Discord reverted their update on Stable! This meant that our main branch sat with a half-released update for a bit, so we had to wait until the morning after to fully release the update.

When we released the moonlight update for Canary (11 pm UTC), some other client mods were still broken. While it’s uncertain if we were the first client mod to fix this, we were definitely fast about it! This is in part to our bet on mappings early on in the ecosystem, and how it’s proven to be useful for these kinds of issues.

The extensions that had issues have already been updated, but most of them just worked out of the box without any changes. While some minor tweaks were required for Moonbase (our settings UI), most of it just started working after we had remapped the missing components!

While we can’t guarantee this pace for other fixes in the future (especially because we had warnings ahead of time for this one), it’s a nice look into how we handle breaking Discord updates, and our community’s mindset on getting things working ASAP. As always, moonlight is a passion project, so don’t expect us to be so fast in the future.

moonlight 1.2: Linux, browsers, installers, and more

Right after releasing moonlight API v2 we knew we wanted to work on more moonlight stuff. What was originally planned as a “1.1.1” kept growing in size until it was large enough that we decided to upgrade to a “1.2.0”. We ended up making over 100 commits in one week!

With this release, the moonlight API version still stays at 2. Assuming your extension was up to date and functioning before, it should continue to be fine now.

Let’s talk about all the new changes!

Welcome to the new website

What you’re reading right now wasn’t what it was like before. The moonlight website and documentation is now powered by Starlight, an Astro-based documentation site with an incredibly fitting name. We migrated from Docusaurus since it’s more modern, looks nicer, and builds faster. All the content is still the same, but some links might be broken.

We use the starlight-blog plugin for this blog. We even got a new RSS feed with it!

Introducing rocketship, a custom Discord Linux build

Discord on Linux has been a notably bad experience for a while now. Several clients exist to make it an easier experience, but they don’t support moonlight. So we made our own!

One of the biggest pain points is screensharing. The Discord client ships with two methods for voice connections: using WebRTC for browsers, and using their native Rust/C++ engine for desktop. The native engine support on Linux is quite bad, but WebRTC support can be better in some situations. Unfortunately, Discord’s Electron build disables the required APIs for using the WebRTC engine on desktop.

While you can just use stock Electron for a custom client, Discord’s modified Electron is desirable for its various fixes. We were able to fork Discord’s fork and remove the patch we didn’t want, keeping the others. After building Electron, we uploaded it as a GitHub release and wrote rocketship, a script to install it into Discord. rocketship downloads the official Discord Linux client, keeping its .asar and desktop entry, but removes the Electron binary and replaces it with our own.

rocketship is thus less of a packaged Discord distribution and more of an installer. rocketship can be used without moonlight, but you don’t gain any of its benefits without using moonlight. The moonlight rocketship extension can force Discord to use the WebRTC APIs and then use venmic to connect audio.

rocketship is still very experimental, but we encourage Linux users to try it out and report any issues!

A new installer, and a new CLI

The moonlight installer was rewritten, along with a new CLI project. Instead of using Tauri, we moved to egui for the UI.

A screenshot of the moonlight installer

Our motivation for moving from Tauri is that it’s rough to use. Linux support is flakey (we had many issues on Wayland), packaging for Windows required an installer for the installer because of WebViews, and it was just extremely heavy for a tool that needed to download some files and extract them somewhere. With egui, the moonlight installer is a single self-contained ~5 MB .exe (minus Visual C++ Redistributables).

In the process of rewriting the installer, we had to write publishing scripts for macOS and Linux. This turned out to be in a very sorry state, as most tools to create .dmg and .AppImage files are kinda bad. We ended up having to abandon Linux AppImage support because of glibc issues, and wrote our own macOS bundle script. Linux users can build the GUI installer from source, but we encourage using the CLI:

Terminal window
./rocketship.sh -b stable
moonlight-cli install stable
moonlight-cli patch ~/.local/share/Discord/Discord

A very special thank you to Emma/InvoxiPlayGames for PRing macOS CI scripts, and Eva for the original installer & macOS support. PRs for Linux AppImage support will be accepted in the future, if anyone is willing to help.

Moonbase UX improvements, and updating moonlight from Discord

Moonbase now has a notice that displays at the top of the screen when updates are available. Moonbase can check for updates to moonlight, as well as updates for the extensions you have installed.

A screenshot of a banner at the top of the Discord client, showing that moonlight can be updated and 1 extension update is available

When in Moonbase, this prompt will show at the top of the page, allowing you to update moonlight with a single click from inside Discord:

A screenshot of a notice in Moonbase, with a button to update moonlight

Moonbase can also prompt you when you install an extension with a missing dependency. If an extension is present in multiple sources, you can pick which one to install. This makes it easier to make your own libraries on custom repositories and make sure they’re installed with your extension.

A screenshot of the dependency prompt in Moonbase after installing an extension

While making these improvements, we introduced a new notice library for showing custom messages, with as many buttons as you like and custom styling. It even supports multiple notices being queued!

moonlight now runs in the browser

moonlight now has experimental support to run as a browser extension. We used ZenFS to create a local filesystem in the browser, allowing you to install and load extensions like normal. This was planned since May(!), but it needed some final touches for extension installation and core loading. The browser extension works both in Chrome and Firefox, with support for Manifest V2 and Manifest V3.

The browser extension has to block Discord from loading so we can load first, given that moonlight is asynchronous and we can’t easily tell the page to wait for us. To solve this, the extension blocks Discord’s JavaScript files from loading, loads moonlight, and then unblocks the scripts and reloads them. Clever!

Squashing bugs and making new ones

We made a lot of fixes and improvements this release, with a focus on Moonbase, the extension manager GUI for moonlight. Here’s what we were up to:

Bringing in a new core developer

We welcomed redstonekasi to the core developer team. Kasi has done exceptional work improving Moonbase and maintaining moonlight through the year, along with making lots of extensions in the official repository. The title of “core developer” doesn’t change much with their access, but we want to recognize their efforts. Thanks!

That’s all for now

That’s everything we’ve got for moonlight 1.2. Development might slow down for a bit, as a lot of work has gone into maintenance & experimenting with new projects, so what remains is mostly extension work. As always, we encourage developers to try making extensions and submitting them to the official repository if they wish - more extensions can always help.

Thanks for using moonlight!

moonlight API v2, mappings, and more

Been a while, eh? The last post we wrote for moonlight was when we first introduced it. Sounds like it’s time to change that!

moonlight development was stalled for a while this year, particularly due to lack of motivation. Fewer people than expected tinkering with moonlight gave us less reason to update it, and so it fell behind with Discord updates, eventually breaking catastrophically for several months. Thanks to redstonekasi for submitting pull requests for fixing a lot of things while none of us had the energy to.

About a week ago, Ari had the idea of making a centralized repository for Discord client mappings. This short conversation would lead to one of the greatest nerdsnipes in moonlight history.

With the new features we’re rolling out, this means new concepts of an “API level”. Extensions that do not meet the provided API level will not load. If you don’t care about the shiny new toys, jump here for more info on that.

LunAST: AST-based mapping and patching

The concept of centralized mappings started with LunAST (Lunar + AST), a library for manipulating Webpack modules with various ESTree-related tools. LunAST enables you to patch and traverse modules using an AST, which is much more flexible than the current text-based patching with RegEx/strings.

Here’s the first LunAST patch we ever wrote, as an idea for how it works. This makes it say “balls” when you click on an image preview (very professional, I know):

import type { AST } from "@moonlight-mod/types";
moonlight.lunast.register({
name: "ImagePreview",
find: ".Messages.OPEN_IN_BROWSER",
process({ id, ast, lunast, markDirty }) {
const getters = lunast.utils.getPropertyGetters(ast);
const replacement = lunast.utils
.magicAST(`return require("common_react").createElement(
"div",
{
style: {
color: "white",
},
},
"balls"
)`)!;
for (const data of Object.values(getters)) {
if (!lunast.utils.is.identifier(data.expression)) continue;
const node = data.scope.getOwnBinding(data.expression.name);
if (!node) continue;
const body = node.path.get<AST.BlockStatement>("body");
body.replaceWith(replacement);
}
markDirty();
return true;
}
});

This is a very powerful tool, but we aren’t sure how well it’ll work for extension developers just yet. If you have a particularly complicated patch in your extension codebase, see if LunAST can help you. Some notes, though:

  • AST parsing is expensive! Use find when possible to filter for modules to parse.
  • AST patching might break other text-based patches. If you encounter any weirdness, let us know.
  • Text-based patching is not going away, and you should not use AST patching everywhere. This is purely a tool for the harder stuff.

moonmap: dynamic remapping of Webpack modules

moonmap allows you to find a module, create a proper name for it, and create named exports from minified variable names. This feature was originally in LunAST, but we decided to expand on it and bring it into its own library. Here’s a snippet from the mappings project (which we’ll get into later):

const name = "discord/utils/HTTPUtils";
moonlight.moonmap.register({
name,
find: '.set("X-Audit-Log-Reason",',
process({ id, moonmap }) {
moonmap.addModule(id, name);
moonmap.addExport(name, "HTTP", {
type: ModuleExportType.Key,
find: "patch"
});
return true;
}
});

You can then just spacepack.require("discord/utils/HTTPUtils").HTTP, and it just works! Magic:tm:.

mappings: client mod agnostic Discord mappings

mappings combines moonmap and LunAST into one project to map out the Discord client. This is an idea that was tested in HH3, and we believe that it’s stable by now.

The biggest feature about the mappings is that they aren’t locked into the moonlight patcher system. Any client mod that can implement moonmap and LunAST into their patching system can use the mappings repository with no extra effort. We hope this can save some duplicated effort across the client modding community.

import load from "@moonlight-mod/mappings";
load(moonmap, lunast);
// later, after the modules finish initializing
const Dispatcher = require("discord/Dispatcher").default;

There’s still a lot of work to be done for typing the untyped modules, and for adding new modules. We migrated a majority of types in the Common extension into this library, and most of the things in Common have been removed.

What a version bump means for you

As a user

All extensions you are currently using (minus the extensions built into moonlight) will stop working. The developers of those extensions will need to update them, and if those extensions are on the official repository, they will need to be resubmitted and reviewed.

As an extension developer

Your extensions will need to be updated. See this new page in the documentation.

As another client mod developer

All of the libraries mentioned above can be used by your own code now. Have fun!

The future of moonlight

moonlight, like the PlayStation 5, is pointless when there’s nothing to install onto it. As with last time, we encourage developers to try making extensions. Let us know if there’s anything we can improve, and submit your extension to the official repository if you’d like.

We don’t consider this a “moonlight 2.0” as much as “moonlight API version 2”. There’s no groundbreaking rewrite going on here, just some new libraries to play with.

(Re)introducing moonlight: Yet another Discord mod

A few days ago, we released a new Discord client mod called moonlight. moonlight is the end result of our ideas and experiences from Discord client modding, influenced by several years of working on other client modding projects. We invite developers to try out moonlight and report feedback on what we can improve.

Yet another Discord mod

Tens if not hundreds of Discord desktop client mods have come and gone, so why make another? Simple; the freedom to innovate and experiment. With moonlight, we’re free to do what we want and experiment with the ideas that we’ve had for years without any limitations. Creating moonlight has offered a fresh slate to build our own systems and play with what we like most, and allows us to try out new things with zero consequence.

moonlight functions off of Webpack patching (really Rspack, but whatever) - extension developers can modify minified Discord code through string/RegExp replacements, swap entire Webpack modules out, and insert their own Webpack modules. With moonlight’s Webpack module loading, you can import and export in a file like normal, which will be transformed into a Webpack module. This system of patching was inspired and iterated through several other client mods we’ve used and worked on; notably yelm, EndPwn, and HH3. This patching system is also seen in some modern client mods like Vencord.

Despite moonlight being inspired from some other client mods, we don’t intend for it to seriously compete with the current playing field; it will mostly be an experimental playground for the foreseeable future. Don’t let this stop you from trying it, though! We’ll do our best to maintain moonlight and establish a stable API as soon as possible.

Getting started with moonlight

For installing moonlight, refer to manual instructions or our experimental GUI installer. For extension development, see this documentation entry. The documentation tab has lots of information on setting up development environments for extensions and moonlight itself.

If you feel like anything’s unclear, please let us know in our Discord server or make a pull request to this website.

The stable and not-so-stable

moonlight has a lot of features we’re currently working on that may change at any moment. At the current moment, its API is under heavy development and breaking changes will be made. With the help of other developers to try it out and contribute ideas and feedback, we aim to stabilize it soon.

moonlight functions under two release channels: a stable channel (published to GitHub releases on every version), and a nightly channel (published to GitHub Pages through GitHub Actions on every commit to the develop branch). In this experimental development period, we suggest developers use nightly when possible (either through the “Nightly” dropdown in the moonlight installer, or by checking out the develop branch in Git).

Embracing the freedom to develop

We care about the freedom to do what you want with extension development. moonlight is open source under the LGPL-3.0-or-later license.

Extensions can be loaded from disk, the official extension repository, or remote extension repositories. Anyone can create their own extension repository and publish their own extensions; while submitting to the official extension repository is encouraged, it isn’t required. Custom extension repositories are a single .json file that contain URLs to .asar files. The sample extension publishes the extensions you write as an extension repository to GitHub Pages - everything you need to create a custom extension repository is done for you automatically.

moonlight also comes with a set of core extensions that come pre-installed into the Discord client (but you can disable them). Some of them are libraries (like Common) and some of them are simple utility (like Moonbase, the settings GUI). These libraries have types, which can be utilized when using the Webpack require function, or the special import prefix in ESM Webpack modules. Using third party libraries is as simple as adding a .d.ts to your project.

What’s with HH3?

You may have seen some mentions of an “HH3” on the landing page or this blog post. This is a private clientmod we work on, currently on its third iteration (hence the name). HH3 functions very similarly to moonlight, having its own Webpack module patching and insertion system. Some parts of moonlight are based off of HH3 or its extensions; we have been cleared for approval to use these parts or wrote them ourselves. For the (very likely small amount of) HH3 users reading this post, let’s make it clear that HH3 is not dead, and moonlight is not a successor or competitor to HH3. We will continue to use and maintain both.

Extended thanks and acknowledgment to Mary and twilight sparkle, who’ve helped build and shape HH3 and what came before it.

What’s next for moonlight

Our roadmap for what we want to do with moonlight is powered by the community. We want to offer developers as much freedom as possible to send feedback and pitch ideas for what we can do. Some guidelines, though:

  • Please submit your extensions to the official repository (even if the merge times are upsetting), because having them in one place will help coordinate things.
  • If you’re going to be developing libraries, please make pull requests to the moonlight core extensions! Your code can benefit everyone.
  • Please report bugs on GitHub issues, or if you must, in the Discord server.
  • Please don’t share this with all of your friends and go “OMG NEW CLIENT MOD”. Having a bunch of end users trying to use a client mod that’s an active work in progress is a massive headache.

Thanks for reading, and hopefully you enjoy moonlight! <3