···11<h3 align="center">
22- <img src="./img/wordmark.png" alt="moonlight" />
22+ <picture>
33+ <source media="(prefers-color-scheme: dark)" srcset="./img/wordmark-light.png">
44+ <source media="(prefers-color-scheme: light)" srcset="./img/wordmark.png">
55+ <img src="./img/wordmark.png" alt="moonlight" />
66+ </picture>
3748<a href="https://discord.gg/FdZBTFCP6F">Discord server</a>
59\- <a href="https://github.com/moonlight-mod/moonlight">GitHub</a>
···12161317moonlight is heavily inspired by hh3 (a private client mod) and the projects before it that it is inspired by, namely EndPwn. All core code is original or used with permission from their respective authors where not copyleft.
14181515-**_This is an experimental passion project._** moonlight was not created out of malicious intent nor intended to seriously compete with other mods. Anything and everything is subject to change.
1919+**_This is an experimental passion project._** Anything and everything is subject to change, but it is stable enough for developers to experiment with.
16201721moonlight is licensed under the [GNU Lesser General Public License](https://www.gnu.org/licenses/lgpl-3.0.html) (`LGPL-3.0-or-later`). See [the documentation](https://moonlight-mod.github.io/) for more information.
···33 "apiLevel": 2,
44 "meta": {
55 "name": "No Hide Token",
66- "tagline": "Disables removal of token from localStorage when opening dev tools",
66+ "tagline": "Prevents you from being logged-out on hard-crash",
77+ "description": "Prevents you from being logged-out on hard-crash by disabling removal of token from localStorage when opening dev tools",
78 "authors": ["adryd"],
89 "tags": ["dangerZone", "development"]
910 }
···11+{
22+ "id": "rocketship",
33+ "apiLevel": 2,
44+ "meta": {
55+ "name": "Rocketship",
66+ "tagline": "Adds new features when using rocketship",
77+ "description": "**This extension only works on Linux when using rocketship:**\nhttps://github.com/moonlight-mod/rocketship\n\nAdds new features to the Discord Linux client with rocketship, such as a better screensharing experience.",
88+ "authors": ["NotNite", "Cynosphere", "adryd"]
99+ }
1010+}
···11+// https://github.com/electron/asar
22+// http://formats.kaitai.io/python_pickle/
33+import { BinaryReader } from "./util/binary";
44+55+/*
66+ The asar format is kinda bad, especially because it uses multiple pickle
77+ entries. It spams sizes, expecting us to read small buffers and parse those,
88+ but we can just take it all through at once without having to create multiple
99+ BinaryReaders. This implementation might be wrong, though.
1010+1111+ This either has size/offset or files but I can't get the type to cooperate,
1212+ so pretend this is a union.
1313+*/
1414+1515+type AsarEntry = {
1616+ size: number;
1717+ offset: `${number}`; // who designed this
1818+1919+ files?: Record<string, AsarEntry>;
2020+};
2121+2222+export default function extractAsar(file: ArrayBuffer) {
2323+ const array = new Uint8Array(file);
2424+ const br = new BinaryReader(array);
2525+2626+ // two uints, one containing the number '4', to signify that the other uint takes up 4 bytes
2727+ // bravo, electron, bravo
2828+ const _payloadSize = br.readUInt32();
2929+ const _headerSize = br.readInt32();
3030+3131+ const headerStringStart = br.position;
3232+ const headerStringSize = br.readUInt32(); // How big the block is
3333+ const actualStringSize = br.readUInt32(); // How big the string in that block is
3434+3535+ const base = headerStringStart + headerStringSize + 4;
3636+3737+ const string = br.readString(actualStringSize);
3838+ const header: AsarEntry = JSON.parse(string);
3939+4040+ const ret: Record<string, Uint8Array> = {};
4141+ function addDirectory(dir: AsarEntry, path: string) {
4242+ for (const [name, data] of Object.entries(dir.files!)) {
4343+ const fullName = path + "/" + name;
4444+ if (data.files != null) {
4545+ addDirectory(data, fullName);
4646+ } else {
4747+ br.position = base + parseInt(data.offset);
4848+ const file = br.read(data.size);
4949+ ret[fullName] = file;
5050+ }
5151+ }
5252+ }
5353+5454+ addDirectory(header, "");
5555+5656+ return ret;
5757+}
+35-31
packages/core/src/config.ts
···11import { Config } from "@moonlight-mod/types";
22-import requireImport from "./util/import";
32import { getConfigPath } from "./util/data";
33+import * as constants from "@moonlight-mod/types/constants";
44+import Logger from "./util/logger";
55+66+const logger = new Logger("core/config");
4758const defaultConfig: Config = {
69 extensions: {
···912 noTrack: true,
1013 noHideToken: true
1114 },
1212- repositories: ["https://moonlight-mod.github.io/extensions-dist/repo.json"]
1515+ repositories: [constants.mainRepo]
1316};
14171515-export function writeConfig(config: Config) {
1616- const fs = requireImport("fs");
1717- const configPath = getConfigPath();
1818- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
1919-}
2020-2121-function readConfigNode(): Config {
2222- const fs = requireImport("fs");
2323- const configPath = getConfigPath();
2424-2525- if (!fs.existsSync(configPath)) {
2626- writeConfig(defaultConfig);
2727- return defaultConfig;
1818+export async function writeConfig(config: Config) {
1919+ try {
2020+ const configPath = await getConfigPath();
2121+ await moonlightFS.writeFileString(
2222+ configPath,
2323+ JSON.stringify(config, null, 2)
2424+ );
2525+ } catch (e) {
2626+ logger.error("Failed to write config", e);
2827 }
2929-3030- let config: Config = JSON.parse(fs.readFileSync(configPath, "utf8"));
3131-3232- // Assign the default values if they don't exist (newly added)
3333- config = { ...defaultConfig, ...config };
3434- writeConfig(config);
3535-3636- return config;
3728}
38293939-export function readConfig(): Config {
3030+export async function readConfig(): Promise<Config> {
4031 webPreload: {
4132 return moonlightNode.config;
4233 }
43344444- nodePreload: {
4545- return readConfigNode();
4646- }
3535+ const configPath = await getConfigPath();
3636+ if (!(await moonlightFS.exists(configPath))) {
3737+ await writeConfig(defaultConfig);
3838+ return defaultConfig;
3939+ } else {
4040+ try {
4141+ let config: Config = JSON.parse(
4242+ await moonlightFS.readFileString(configPath)
4343+ );
4444+ // Assign the default values if they don't exist (newly added)
4545+ config = { ...defaultConfig, ...config };
4646+ await writeConfig(config);
47474848- injector: {
4949- return readConfigNode();
4848+ return config;
4949+ } catch (e) {
5050+ logger.error("Failed to read config, falling back to defaults", e);
5151+ // We don't want to write the default config here - if a user is manually
5252+ // editing their config and messes it up, we'll delete it all instead of
5353+ // letting them fix it
5454+ return defaultConfig;
5555+ }
5056 }
5151-5252- throw new Error("Called readConfig() in an impossible environment");
5357}
···22export * as Settings from "./coreExtensions/settings";
33export * as Markdown from "./coreExtensions/markdown";
44export * as ContextMenu from "./coreExtensions/contextMenu";
55+export * as Notices from "./coreExtensions/notices";
66+export * as Moonbase from "./coreExtensions/moonbase";