diff --git a/README.md b/README.md index 7906cec..5a5bd9a 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ public class MyScript : MonoBehaviour ## Installation +> You can also install Dissonity via [OpenUPM](https://openupm.com/packages/com.furnyr.dissonity/) or with each `.unitypackage` file found in the [Releases](https://github.com/Furnyr/Dissonity/releases) tab. + 1. Create a new Unity project (Unity 2021.3 or later, Unity 6 recommended) 2. Open the package manager and install the package from this git URL: `https://github.com/Furnyr/Dissonity.git?path=/unity#v2` 3. Use the pop-up dialog to select a configuration file @@ -71,7 +73,7 @@ Dissonity is now installed! But you still need to configure a few things: ## Configuration 1. Open the configuration file in Assets/Dissonity/DissonityConfiguration.cs -2. Set your app id in `.ClientId` (find it [here](https://discord.com/developers/applications)) +2. Set your app id in `.ClientId` (find it or create a Discord app [here](https://discord.com/developers/applications)) Up and running! If you want to test your activity within Unity: @@ -89,6 +91,9 @@ Dissonity helps in the process to make the game, but you will still need to host If you're not sure how to continue, read the documentation. +> [!TIP] +> If you are also using a framework that provides Discord functionality, you should read [Third-party support](http://dissonity.dev/guides/v2/third-party-support). + ## Documentation diff --git a/hirpc-interface/package.json b/hirpc-interface/package.json index 6d0adc4..2590ed0 100644 --- a/hirpc-interface/package.json +++ b/hirpc-interface/package.json @@ -1,6 +1,6 @@ { "name": "@dissonity/hirpc-interface", - "version": "0.4.0", + "version": "0.5.0", "description": "A hiRPC implementation for Unity WebGL", "scripts": { "build": "pnpm tsc && node scripts/build.js", diff --git a/hirpc-interface/src/app_loader.ts b/hirpc-interface/src/app_loader.ts index 9a9ae57..60e6b14 100644 --- a/hirpc-interface/src/app_loader.ts +++ b/hirpc-interface/src/app_loader.ts @@ -26,11 +26,9 @@ let baseUrl = `${window.location.protocol}//${window.location.host}${getPath()}` if (!baseUrl.endsWith("/")) baseUrl += "/"; let outsideDiscord = false; -let proxyPrefixAdded = false; let needsProxyPrefix = false; let loaderPath = "Build/{{{ LOADER_FILENAME }}}"; -const versionCheckPath = baseUrl + ".proxy/version.json"; const proxyBridgeImport = "dso_proxy_bridge/"; const normalBridgeImport = "dso_bridge/"; @@ -45,23 +43,27 @@ let initialHeight = window.innerHeight; // Set up paths before anything async function initialize() { - async function fileExists(url: string) { - const response = await fetch(url, { method: "HEAD" }); - return response.ok; - } - async function updatePaths() { - let { pathname } = window.location; - - // Handle URL override - const pathSegments = pathname.split("/"); // "/.proxy/staging" -> ["", ".proxy", "staging"] - pathSegments.shift(); - - proxyPrefixAdded = pathSegments[0] == ".proxy"; - const prefixData = sessionStorage.getItem("dso_needs_prefix") as SessionStorage["dso_needs_prefix"]; - needsProxyPrefix = !proxyPrefixAdded && prefixData != "false" && (prefixData == "true" || await fileExists(versionCheckPath)); - outsideDiscord = !proxyPrefixAdded && !needsProxyPrefix; + //? Not outside Discord + if (window.location.hostname.endsWith(".discordsays.com")) { + sessionStorage.setItem("dso_outside_discord", "false" as NonNullable); + } + + else { + outsideDiscord = true; + sessionStorage.setItem("dso_outside_discord", "true" as NonNullable); + } + + //? Doesn't need prefix + if (outsideDiscord || window.location.pathname.startsWith("/.proxy")) { + sessionStorage.setItem("dso_needs_prefix", "false" as NonNullable); + } + + else { + needsProxyPrefix = true; + sessionStorage.setItem("dso_needs_prefix", "true" as NonNullable); + } // Add .proxy if (needsProxyPrefix) { @@ -87,10 +89,9 @@ async function initialize() { async function handleHiRpc() { //? Module already created - // Nested const isNested = window.parent != window.parent.parent; - if (isNested || typeof window.parent?.dso_hirpc == "object") { + if (isNested && typeof window.parent?.dso_hirpc == "object") { //\ Add shallow references to this window to use later Object.defineProperty(window, "dso_hirpc", { @@ -138,7 +139,7 @@ async function handleHiRpc() { // window.dso_hirpc is defined after this line const hiRpc = new window.Dissonity.HiRpc.default() as HiRpcModule; - await initialize(hiRpc, false); + await initialize(hiRpc, false || hiRpc.getBuildVariables().LAZY_HIRPC_LOAD); resolve(hiRpc); } diff --git a/hirpc-interface/src/plugin.ts b/hirpc-interface/src/plugin.ts index 67b8f95..a1defd0 100644 --- a/hirpc-interface/src/plugin.ts +++ b/hirpc-interface/src/plugin.ts @@ -16,10 +16,27 @@ mergeInto(LibraryManager.library, { const hiRpc = window.dso_hirpc as HiRpcModule; - hiRpc.openDownwardFlow((stringifiedData: string) => { - - SendMessage("_DissonityBridge", "_HiRpcInput", stringifiedData); - }); + // Load module now if LAZY_HIRPC_LOAD is set to true. + // This loads the module with zero hash accesses. Hash access is already locked at this point either way. + if (hiRpc.getBuildVariables().LAZY_HIRPC_LOAD) { + hiRpc.load(0) + .then(openFlow) + .catch(err => { + + // So if something weird happens we can see what's going on + console.log(err); + }); + } + + else { + openFlow(); + } + + function openFlow() { + hiRpc.openDownwardFlow((stringifiedData: string) => { + SendMessage("_DissonityBridge", "_HiRpcInput", stringifiedData); + }); + } }, //@unity-bridge @@ -112,12 +129,12 @@ mergeInto(LibraryManager.library, { //@unity-api DsoSendToRpc: function (stringifiedMessage: string): void { - const { data, app_hash } = JSON.parse(UTF8ToString(stringifiedMessage)); + const { data } = JSON.parse(UTF8ToString(stringifiedMessage)); const hiRpc = window.dso_hirpc as HiRpcModule; // The array is formed again at the hiRPC layer - hiRpc.sendToRpc(app_hash, data[0], data[1]); + hiRpc.sendToRpc(data[0], data[1]); }, //@unity-api diff --git a/hirpc-kit/CHANGELOG.md b/hirpc-kit/CHANGELOG.md index 078e0e5..e46c6dc 100644 --- a/hirpc-kit/CHANGELOG.md +++ b/hirpc-kit/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this package will be documented in this file. +## [0.1.5] - 2025-06-10 + +### Added + +- Support for LAZY_HIRPC_LOAD and hiRPC v0.6.0 + ## [0.1.4] - 2025-05-30 ### Added diff --git a/hirpc-kit/dist/index.d.mts b/hirpc-kit/dist/index.d.mts index 54c9f4b..32db659 100644 --- a/hirpc-kit/dist/index.d.mts +++ b/hirpc-kit/dist/index.d.mts @@ -30,9 +30,10 @@ interface PatchUrlMappingsConfig { * * Each variable is separated by § (alt 21 win) (\u00A7) */ -declare class BuildVariables$1 { +declare class BuildVariables$2 { #private; DISABLE_INFO_LOGS: boolean; + LAZY_HIRPC_LOAD: boolean; CLIENT_ID: string; DISABLE_CONSOLE_LOG_OVERRIDE: boolean; MAPPINGS: Mapping[]; @@ -49,7 +50,7 @@ type RpcInputPayload = { nonce?: string; args?: unknown; }; -type BuildVariables = InstanceType; +type BuildVariables$1 = InstanceType; /** * Main hiRPC class. After instantiation, the instance will be located in window.dso_hirpc. @@ -58,7 +59,7 @@ type BuildVariables = InstanceType; * - dso_bridge/ * - dso_proxy_bridge/ */ -declare class HiRpc { +declare class HiRpcV0_5 { #private; constructor(); /** @@ -128,8 +129,99 @@ declare class HiRpc { closeDownwardFlow(hash: string): void; } +declare class _BuildVariables { + #private; + DISABLE_INFO_LOGS: boolean; + CLIENT_ID: string; + DISABLE_CONSOLE_LOG_OVERRIDE: boolean; + MAPPINGS: Mapping[]; + PATCH_URL_MAPPINGS_CONFIG: PatchUrlMappingsConfig; + OAUTH_SCOPES: string[]; + TOKEN_REQUEST_PATH: string; + SERVER_REQUEST: string; + constructor(); +} +type BuildVariables = InstanceType; + +/** + * Main hiRPC class. After instantiation, the instance will be located in window.dso_hirpc. + * + * Imports that must be defined: + * - dso_bridge/ + * - dso_proxy_bridge/ + */ +declare class HiRpc { + #private; + constructor(); + /** + * Load the hiRPC module once per runtime. + * + * Depends on the external RPC state tracked via `sessionStorage`. + * + * ```js + * // Trigger a parallel load: + * sessionStorage.setItem("dso_connected", true); + * + * // Trigger a soft load: + * sessionStorage.setItem("dso_connected", true); + * sessionStorage.setItem("dso_authenticated", true); + * ``` + */ + load(maxAccessCount?: number): Promise; + getBuildVariables(): BuildVariables$1; + patchUrlMappings(hash: string, mappings: Mapping[], config?: PatchUrlMappingsConfig): void; + formatPrice(hash: string, price: { + amount: number; + currency: string; + }, locale?: string): string | undefined; + getQueryObject(): Record; + getNonce(): string; + getVersions(): { + embedded_app_sdk: string; + hirpc: string; + }; + /** + * Request a hash to access restricted functionality. + * + * Call this method after hiRPC `load` and before loading the game build. + */ + requestHash(): Promise; + /** + * Send data to Discord through RPC. + */ + sendToRpc(opcode: Opcode | undefined, payload: RpcInputPayload): Promise; + /** + * **Only used inside the game build.** + * + * Send data to the JavaScript layer. + */ + sendToJs(appHash: string, channel: string, payload: unknown): void; + /** + * Send data to the game build through a hiRPC channel. + */ + sendToApp(hash: string, channel: string, payload: unknown): Promise; + addAppListener(hash: string, channel: string, listener: (data: unknown) => void): void; + removeAppListener(hash: string, channel: string, listener: (data: unknown) => void): void; + /** + * Lock hash access before opening the downward flow. After this call, `requestHash` will return null. + */ + lockHashAccess(): void; + /** + * Define the function that will be used to send data to the game build. + * + * The first message sent will be the `MultiEvent` after authentication. Depending on the RPC state, it could contain partial data. + * + * Hash access will be locked after this call, if it wasn't locked already. + */ + openDownwardFlow(appSender: (data: string) => void): Promise; + /** + * Free the app sender. Hash access will continue to be locked. + */ + closeDownwardFlow(hash: string): void; +} + type StartWith = `${V}${string}`; -type HiRpcShape = V extends StartWith<"0.5"> ? HiRpc : UnknownHiRpc; +type HiRpcShape = V extends StartWith<"0.5"> ? HiRpcV0_5 : V extends StartWith<"0.6"> ? HiRpc : UnknownHiRpc; declare enum RpcOpcode { Handshake = 0, @@ -148,4 +240,4 @@ declare function setupHiRpc(_hiRpcVersion: V): Promise; +type BuildVariables$1 = InstanceType; /** * Main hiRPC class. After instantiation, the instance will be located in window.dso_hirpc. @@ -58,7 +59,7 @@ type BuildVariables = InstanceType; * - dso_bridge/ * - dso_proxy_bridge/ */ -declare class HiRpc { +declare class HiRpcV0_5 { #private; constructor(); /** @@ -128,8 +129,99 @@ declare class HiRpc { closeDownwardFlow(hash: string): void; } +declare class _BuildVariables { + #private; + DISABLE_INFO_LOGS: boolean; + CLIENT_ID: string; + DISABLE_CONSOLE_LOG_OVERRIDE: boolean; + MAPPINGS: Mapping[]; + PATCH_URL_MAPPINGS_CONFIG: PatchUrlMappingsConfig; + OAUTH_SCOPES: string[]; + TOKEN_REQUEST_PATH: string; + SERVER_REQUEST: string; + constructor(); +} +type BuildVariables = InstanceType; + +/** + * Main hiRPC class. After instantiation, the instance will be located in window.dso_hirpc. + * + * Imports that must be defined: + * - dso_bridge/ + * - dso_proxy_bridge/ + */ +declare class HiRpc { + #private; + constructor(); + /** + * Load the hiRPC module once per runtime. + * + * Depends on the external RPC state tracked via `sessionStorage`. + * + * ```js + * // Trigger a parallel load: + * sessionStorage.setItem("dso_connected", true); + * + * // Trigger a soft load: + * sessionStorage.setItem("dso_connected", true); + * sessionStorage.setItem("dso_authenticated", true); + * ``` + */ + load(maxAccessCount?: number): Promise; + getBuildVariables(): BuildVariables$1; + patchUrlMappings(hash: string, mappings: Mapping[], config?: PatchUrlMappingsConfig): void; + formatPrice(hash: string, price: { + amount: number; + currency: string; + }, locale?: string): string | undefined; + getQueryObject(): Record; + getNonce(): string; + getVersions(): { + embedded_app_sdk: string; + hirpc: string; + }; + /** + * Request a hash to access restricted functionality. + * + * Call this method after hiRPC `load` and before loading the game build. + */ + requestHash(): Promise; + /** + * Send data to Discord through RPC. + */ + sendToRpc(opcode: Opcode | undefined, payload: RpcInputPayload): Promise; + /** + * **Only used inside the game build.** + * + * Send data to the JavaScript layer. + */ + sendToJs(appHash: string, channel: string, payload: unknown): void; + /** + * Send data to the game build through a hiRPC channel. + */ + sendToApp(hash: string, channel: string, payload: unknown): Promise; + addAppListener(hash: string, channel: string, listener: (data: unknown) => void): void; + removeAppListener(hash: string, channel: string, listener: (data: unknown) => void): void; + /** + * Lock hash access before opening the downward flow. After this call, `requestHash` will return null. + */ + lockHashAccess(): void; + /** + * Define the function that will be used to send data to the game build. + * + * The first message sent will be the `MultiEvent` after authentication. Depending on the RPC state, it could contain partial data. + * + * Hash access will be locked after this call, if it wasn't locked already. + */ + openDownwardFlow(appSender: (data: string) => void): Promise; + /** + * Free the app sender. Hash access will continue to be locked. + */ + closeDownwardFlow(hash: string): void; +} + type StartWith = `${V}${string}`; -type HiRpcShape = V extends StartWith<"0.5"> ? HiRpc : UnknownHiRpc; +type HiRpcShape = V extends StartWith<"0.5"> ? HiRpcV0_5 : V extends StartWith<"0.6"> ? HiRpc : UnknownHiRpc; declare enum RpcOpcode { Handshake = 0, @@ -148,4 +240,4 @@ declare function setupHiRpc(_hiRpcVersion: V): Promise = `${V}${string}`; export type HiRpcShape = - V extends StartWith<"0.5"> ? LatestHiRpc : + V extends StartWith<"0.5"> ? HiRpcV0_5 : + V extends StartWith<"0.6"> ? LatestHiRpc : UnknownHiRpc; \ No newline at end of file diff --git a/hirpc-kit/types/index.d.ts b/hirpc-kit/types/index.d.ts index 7f85500..1871e96 100644 --- a/hirpc-kit/types/index.d.ts +++ b/hirpc-kit/types/index.d.ts @@ -47,7 +47,7 @@ export default class HiRpc { /** * Send data to Discord through RPC. */ - sendToRpc(hash: string, opcode: Opcode | undefined, payload: RpcInputPayload): Promise; + sendToRpc(opcode: Opcode | undefined, payload: RpcInputPayload): Promise; /** * **Only used inside the game build.** * diff --git a/hirpc-kit/types/modules/build_variables.d.ts b/hirpc-kit/types/modules/build_variables.d.ts index 3e2552d..1415b70 100644 --- a/hirpc-kit/types/modules/build_variables.d.ts +++ b/hirpc-kit/types/modules/build_variables.d.ts @@ -9,6 +9,7 @@ import type { Mapping, PatchUrlMappingsConfig } from "../official_types"; export default class BuildVariables { #private; DISABLE_INFO_LOGS: boolean; + LAZY_HIRPC_LOAD: boolean; CLIENT_ID: string; DISABLE_CONSOLE_LOG_OVERRIDE: boolean; MAPPINGS: Mapping[]; diff --git a/hirpc-kit/types/types.d.ts b/hirpc-kit/types/types.d.ts index f8b57dc..030139b 100644 --- a/hirpc-kit/types/types.d.ts +++ b/hirpc-kit/types/types.d.ts @@ -44,3 +44,7 @@ export type MultiEvent = { authenticate: string; response: string; }; +export type RpcSource = { + source: Window | null; + sourceOrigin: string; +}; diff --git a/hirpc-kit/types/versions/v0_5.d.ts b/hirpc-kit/types/versions/v0_5.d.ts new file mode 100644 index 0000000..72addd8 --- /dev/null +++ b/hirpc-kit/types/versions/v0_5.d.ts @@ -0,0 +1,93 @@ +import { Opcode } from "../enums"; +import type { Mapping, PatchUrlMappingsConfig } from "../official_types"; +import type { RpcInputPayload } from "../types"; +/** + * Main hiRPC class. After instantiation, the instance will be located in window.dso_hirpc. + * + * Imports that must be defined: + * - dso_bridge/ + * - dso_proxy_bridge/ + */ +export default class HiRpcV0_5 { + #private; + constructor(); + /** + * Load the hiRPC module once per runtime. + * + * Depends on the external RPC state tracked via `sessionStorage`. + * + * ```js + * // Trigger a parallel load: + * sessionStorage.setItem("dso_connected", true); + * + * // Trigger a soft load: + * sessionStorage.setItem("dso_connected", true); + * sessionStorage.setItem("dso_authenticated", true); + * ``` + */ + load(maxAccessCount?: number): Promise; + getBuildVariables(): BuildVariables; + patchUrlMappings(hash: string, mappings: Mapping[], config?: PatchUrlMappingsConfig): void; + formatPrice(hash: string, price: { + amount: number; + currency: string; + }, locale?: string): string | undefined; + getQueryObject(): Record; + getNonce(): string; + getVersions(): { + embedded_app_sdk: string; + hirpc: string; + }; + /** + * Request a hash to access restricted functionality. + * + * Call this method after hiRPC `load` and before loading the game build. + */ + requestHash(): Promise; + /** + * Send data to Discord through RPC. + */ + sendToRpc(hash: string, opcode: Opcode | undefined, payload: RpcInputPayload): Promise; + /** + * **Only used inside the game build.** + * + * Send data to the JavaScript layer. + */ + sendToJs(appHash: string, channel: string, payload: unknown): void; + /** + * Send data to the game build through a hiRPC channel. + */ + sendToApp(hash: string, channel: string, payload: unknown): Promise; + addAppListener(hash: string, channel: string, listener: (data: unknown) => void): void; + removeAppListener(hash: string, channel: string, listener: (data: unknown) => void): void; + /** + * Lock hash access before opening the downward flow. After this call, `requestHash` will return null. + */ + lockHashAccess(): void; + /** + * Define the function that will be used to send data to the game build. + * + * The first message sent will be the `MultiEvent` after authentication. Depending on the RPC state, it could contain partial data. + * + * Hash access will be locked after this call, if it wasn't locked already. + */ + openDownwardFlow(appSender: (data: string) => void): Promise; + /** + * Free the app sender. Hash access will continue to be locked. + */ + closeDownwardFlow(hash: string): void; +} + +declare class _BuildVariables { + #private; + DISABLE_INFO_LOGS: boolean; + CLIENT_ID: string; + DISABLE_CONSOLE_LOG_OVERRIDE: boolean; + MAPPINGS: Mapping[]; + PATCH_URL_MAPPINGS_CONFIG: PatchUrlMappingsConfig; + OAUTH_SCOPES: string[]; + TOKEN_REQUEST_PATH: string; + SERVER_REQUEST: string; + constructor(); +} +type BuildVariables = InstanceType; \ No newline at end of file diff --git a/hirpc/CHANGELOG.md b/hirpc/CHANGELOG.md index a8dfc2e..c06dfc3 100644 --- a/hirpc/CHANGELOG.md +++ b/hirpc/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this package will be documented in this file. +## [0.6.0] - 2025-06-10 + +### Added + +- LAZY_HIRPC_LOAD build variable +- Support for lazy hiRPC loading + +### Changed + +- sendToRpc doesn't require hash access + ## [0.5.2] - 2025-04-12 ### Added diff --git a/hirpc/package.json b/hirpc/package.json index 8be99fb..849540b 100644 --- a/hirpc/package.json +++ b/hirpc/package.json @@ -1,6 +1,6 @@ { "name": "@dissonity/hirpc", - "version": "0.5.2", + "version": "0.6.0", "description": "Discord RPC library for activities made in game engines", "scripts": { "lint": "eslint", diff --git a/hirpc/scripts/update_variables.js b/hirpc/scripts/update_variables.js index c49a029..4726fb9 100644 --- a/hirpc/scripts/update_variables.js +++ b/hirpc/scripts/update_variables.js @@ -52,6 +52,9 @@ function main() { variables.CLIENT_ID = rl( required + chalk.yellow("CLIENT_ID (snowflake): ")); + + variables.LAZY_HIRPC_LOAD = rl( + hasDefault + required + chalk.yellow("LAZY_HIRPC_LOAD (True/False): ")); variables.DISABLE_CONSOLE_LOG_OVERRIDE = rl( hasDefault + required + chalk.yellow("DISABLE_CONSOLE_LOG_OVERRIDE (True/False): ")); @@ -76,6 +79,7 @@ function main() { else { variables.DISABLE_INFO_LOGS = "d"; variables.CLIENT_ID = "123456789987654321"; + variables.LAZY_HIRPC_LOAD = "d"; variables.DISABLE_CONSOLE_LOG_OVERRIDE = "d"; variables.MAPPINGS = ""; variables.PATCH_URL_MAPPINGS_CONFIG = "d"; @@ -96,6 +100,7 @@ function main() { //? Default value if (variables[key].toLowerCase() == "d") { + if (key == "LAZY_HIRPC_LOAD") variables[key] = "True"; if (key == "DISABLE_INFO_LOGS") variables[key] = "False"; if (key == "DISABLE_CONSOLE_LOG_OVERRIDE") variables[key] = "True"; if (key == "PATCH_URL_MAPPINGS_CONFIG") variables[key] = '{"patchFetch":true,"patchWebSocket":true,"patchXhr":true,"patchSrcAttributes":false}'; diff --git a/hirpc/src/index.ts b/hirpc/src/index.ts index 5a0e5da..d586844 100644 --- a/hirpc/src/index.ts +++ b/hirpc/src/index.ts @@ -52,6 +52,23 @@ export default class HiRpc { return window.dso_hirpc as this; } + //? Clear RPC session storage + const query = this.getQueryObject(); + const saveInstanceId = sessionStorage.getItem("dso_instance_id") as SessionStorage["dso_instance_id"]; + if (query.instance_id != saveInstanceId) { + + // If these ids don't match, it means that the RPC external session values can be outdated. + + // In this case, RPC session storage is only used to prevent + // initialization triggering multiple times. + + // Meaningless casts to provoke an error if the SessionStorage property changes. + sessionStorage.removeItem("dso_connected" as NonNullable); + sessionStorage.removeItem("dso_authenticated" as NonNullable); + + sessionStorage.setItem("dso_instance_id", query.instance_id as NonNullable); + } + //\ Save instance in window Object.defineProperty(window, "dso_hirpc", { value: this, @@ -104,24 +121,6 @@ export default class HiRpc { //\ Ready to request hash this.#state.loaded = true; this.#state.maxAccessCount = maxAccessCount; - - //? Clear RPC session storage - const query = this.getQueryObject(); - const saveInstanceId = sessionStorage.getItem("dso_instance_id") as SessionStorage["dso_instance_id"]; - if (query.instance_id != saveInstanceId) { - - // If these ids don't match, it means that the hiRPC kit isn't enabled, - // so the RPC external session values can be outdated. - - // In this case, RPC session storage is only used to prevent - // initialization triggering multiple times. - - // Meaningless casts to provoke an error if the SessionStorage property changes. - sessionStorage.removeItem("dso_connected" as NonNullable); - sessionStorage.removeItem("dso_authenticated" as NonNullable); - - sessionStorage.setItem("dso_instance_id", query.instance_id as NonNullable); - } return new Promise(async (resolve, reject) => { @@ -339,9 +338,9 @@ export default class HiRpc { /** * Send data to Discord through RPC. */ - async sendToRpc(hash: string, opcode = Opcode.Frame, payload: RpcInputPayload): Promise { + async sendToRpc(opcode = Opcode.Frame, payload: RpcInputPayload): Promise { - if (!this.#hashes.verifyHash(hash)) return; + if (this.#state.stateCode == StateCode.OutsideDiscord) return; await this.#state.readyPromise; diff --git a/hirpc/src/modules/build_variables.ts b/hirpc/src/modules/build_variables.ts index 8b473a2..25557a9 100644 --- a/hirpc/src/modules/build_variables.ts +++ b/hirpc/src/modules/build_variables.ts @@ -14,6 +14,7 @@ export default class BuildVariables { // After build post-processing, constants look like "[[[ CLIENT_ID ]]] 123454321§" #DISABLE_INFO_LOGS = '[[[ DISABLE_INFO_LOGS ]]]§'; + #LAZY_HIRPC_LOAD = '[[[ LAZY_HIRPC_LOAD ]]]§'; #CLIENT_ID = '[[[ CLIENT_ID ]]]§'; #DISABLE_CONSOLE_LOG_OVERRIDE = '[[[ DISABLE_CONSOLE_LOG_OVERRIDE ]]]§'; #MAPPINGS = '[[[ MAPPINGS ]]]§'; // Keep these single quotes ('), (") breaks the string when the JSON is loaded. @@ -23,6 +24,7 @@ export default class BuildVariables { #SERVER_REQUEST = '[[[ SERVER_REQUEST ]]]§'; // Keep these single quotes ('), (") breaks the string when the JSON is loaded. DISABLE_INFO_LOGS: boolean; + LAZY_HIRPC_LOAD: boolean; CLIENT_ID: string; DISABLE_CONSOLE_LOG_OVERRIDE: boolean; MAPPINGS: Mapping[]; @@ -33,6 +35,7 @@ export default class BuildVariables { constructor() { this.DISABLE_INFO_LOGS = this.#parseBuildVariable(this.#DISABLE_INFO_LOGS, "boolean") as boolean; + this.LAZY_HIRPC_LOAD = this.#parseBuildVariable(this.#LAZY_HIRPC_LOAD, "boolean") as boolean; this.CLIENT_ID = this.#parseBuildVariable(this.#CLIENT_ID, "string") as string; this.DISABLE_CONSOLE_LOG_OVERRIDE = this.#parseBuildVariable(this.#DISABLE_CONSOLE_LOG_OVERRIDE, "boolean") as boolean; this.MAPPINGS = this.#parseBuildVariable(this.#MAPPINGS, "json_array") as Mapping[]; diff --git a/hirpc/src/modules/rpc.ts b/hirpc/src/modules/rpc.ts index d06b9e2..db4f52b 100644 --- a/hirpc/src/modules/rpc.ts +++ b/hirpc/src/modules/rpc.ts @@ -1,6 +1,6 @@ import { CLOSE_NORMAL, HANDSHAKE_UNKNOWN_VERSION_NUMBER } from "../constants"; import { Opcode, RpcCommands, RpcEvents, StateCode } from "../enums"; -import { InteropMessage, RpcMessage } from "../types"; +import { InteropMessage, RpcMessage, RpcSource } from "../types"; import { State } from "./state"; import { BuildVariables } from "../types"; @@ -13,6 +13,7 @@ import { OfficialUtils } from "./official_utils"; export class Rpc { #state: State; + #source: RpcSource; #utils: OfficialUtils; #allowedOrigins = new Set([ typeof window != "undefined" @@ -32,6 +33,7 @@ export class Rpc { constructor(state: State, utils: OfficialUtils) { this.#state = state; + this.#source = this.#getRpcSource(); this.#utils = utils; // Ensure the class context is preserved in the message event @@ -289,31 +291,9 @@ export class Rpc { send(opcode: Opcode, payload: unknown): void { - // The hiRPC needs to work inside a nested iframe. So it can be two iframes in the Discord client. - - let source: Window; - let sourceOrigin: string; - - const isNested = window.parent != window.parent.parent; - if (isNested) { - - const parent = window.parent.parent; - const activity = window.parent; - - source = parent.opener ?? parent; - sourceOrigin = !!activity.document.referrer ? activity.document.referrer : "*"; - } - - else { - - const parent = window.parent; - const activity = window; + const { source, sourceOrigin } = this.#source; - source = parent.opener ?? parent; - sourceOrigin = !!activity.document.referrer ? activity.document.referrer : "*"; - } - - source.postMessage([opcode, payload], sourceOrigin); + source!.postMessage([opcode, payload], sourceOrigin); } getNonce() { @@ -339,6 +319,52 @@ export class Rpc { }); } + #getRpcSource(): RpcSource { + + if (typeof window == "undefined") return { source: null, sourceOrigin: "*" }; + + // The hiRPC needs to work inside a nested iframe. So it can be two iframes in the Discord client. + + let source: Window; + let sourceOrigin: string; + + const isNested = window.parent != window.parent.parent; + if (isNested) { + + const thisParent = window.parent.parent; + const activity = window.parent; + + try { + source = thisParent.opener ?? thisParent; + } + catch { + + // In case a SecurityError occurs + source = thisParent; + } + + sourceOrigin = !!activity.document.referrer ? activity.document.referrer : "*"; + } + + else { + + const thisParent = window.parent; + const activity = window; + + try { + source = thisParent.opener ?? thisParent; + } + catch (_) { + + // In case a SecurityError occurs + source = thisParent; + } + sourceOrigin = !!activity.document.referrer ? activity.document.referrer : "*"; + } + + return { source, sourceOrigin }; + } + // Literal implementation of overrideConsoleLogging from the official SDK #overrideConsoleLogging(): void { diff --git a/hirpc/src/types.ts b/hirpc/src/types.ts index f44e630..d59280d 100644 --- a/hirpc/src/types.ts +++ b/hirpc/src/types.ts @@ -74,4 +74,9 @@ export type MultiEvent = { authorize: string, authenticate: string, response: string +} + +export type RpcSource = { + source: Window | null, + sourceOrigin: string } \ No newline at end of file diff --git a/unity/CHANGELOG.md b/unity/CHANGELOG.md index 7c23b63..036903b 100644 --- a/unity/CHANGELOG.md +++ b/unity/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to this package will be documented in this file. +## [2.1.0] - 2025-06-10 + +## Added + +- LazyHiRpcLoad option in the Advanced Configuration +- Api.Commands.GetRelationships (requires Discord approval) +- Api.Subscribe.RelationshipUpdate (requires Discord approval) +- Models.Relationship +- Relationships section in the Discord Mock + +## Changed + +- Minor performance improvements related to the hiRPC Interface + +## Fixed + +- The game doesn't turn into a black screen when loading the index.html + ## [2.0.1] - 2025-05-30 ### Added diff --git a/unity/Editor/Assets/AdvancedConfig.txt b/unity/Editor/Assets/AdvancedConfig.txt index 2783fb9..018f09a 100644 --- a/unity/Editor/Assets/AdvancedConfig.txt +++ b/unity/Editor/Assets/AdvancedConfig.txt @@ -12,6 +12,7 @@ public class DissonityConfiguration : SdkConfiguration 0; public override string[] OauthScopes => new string[] { OauthScope.Identify, /*, your-oauth-scopes*/ }; public override string TokenRequestPath => "/api/token"; + public override bool LazyHiRpcLoad => false; // Logging public override bool DisableConsoleLogOverride => true; diff --git a/unity/Editor/Assets/Template/Dissonity/Bridge/dissonity_build_variables.js.txt b/unity/Editor/Assets/Template/Dissonity/Bridge/dissonity_build_variables.js.txt index 7ef3f6b..998fb39 100644 --- a/unity/Editor/Assets/Template/Dissonity/Bridge/dissonity_build_variables.js.txt +++ b/unity/Editor/Assets/Template/Dissonity/Bridge/dissonity_build_variables.js.txt @@ -1 +1 @@ -(()=>{"use strict";var r={d:(i,t)=>{for(var e in t)r.o(t,e)&&!r.o(i,e)&&Object.defineProperty(i,e,{enumerable:!0,get:t[e]})},o:(r,i)=>Object.prototype.hasOwnProperty.call(r,i),r:r=>{'undefined'!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(r,Symbol.toStringTag,{value:'Module'}),Object.defineProperty(r,'__esModule',{value:!0})}},i={};r.r(i),r.d(i,{default:()=>t});class t{#r='[[[ DISABLE_INFO_LOGS ]]]§';#i='[[[ CLIENT_ID ]]]§';#t='[[[ DISABLE_CONSOLE_LOG_OVERRIDE ]]]§';#e='[[[ MAPPINGS ]]]§';#s='[[[ PATCH_URL_MAPPINGS_CONFIG ]]]§';#a='[[[ OAUTH_SCOPES ]]]§';#E='[[[ TOKEN_REQUEST_PATH ]]]§';#_='[[[ SERVER_REQUEST ]]]§';constructor(){this.DISABLE_INFO_LOGS=this.#l(this.#r,"boolean"),this.CLIENT_ID=this.#l(this.#i,"string"),this.DISABLE_CONSOLE_LOG_OVERRIDE=this.#l(this.#t,"boolean"),this.MAPPINGS=this.#l(this.#e,"json_array"),this.PATCH_URL_MAPPINGS_CONFIG=this.#l(this.#s,"json"),this.OAUTH_SCOPES=this.#l(this.#a,"string[]"),this.TOKEN_REQUEST_PATH=this.#l(this.#E,"string"),this.SERVER_REQUEST=this.#l(this.#_,"string")}#l(r,i){let t;try{t=r.split("]]] ")[1].slice(0,-1)}catch(r){throw new Error("Build variable undefined")}if("string"==i)return t;if("boolean"==i){if(/1|true/i.test(r))return!0;if(/0|false/i.test(r))return!1;throw new Error("Invalid boolean string")}if("string[]"==i){const r=t.split(",");return 1==r.length&&""==r[0]?[]:r}if("json"==i||"json_array"==i)try{return JSON.parse(t)}catch(r){return"json_array"==i?[]:null}throw new Error("Invalid parse type")}}(self.Dissonity=self.Dissonity||{}).BuildVariables=i})(); \ No newline at end of file +(()=>{"use strict";var i={d:(r,t)=>{for(var e in t)i.o(t,e)&&!i.o(r,e)&&Object.defineProperty(r,e,{enumerable:!0,get:t[e]})},o:(i,r)=>Object.prototype.hasOwnProperty.call(i,r),r:i=>{'undefined'!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(i,Symbol.toStringTag,{value:'Module'}),Object.defineProperty(i,'__esModule',{value:!0})}},r={};i.r(r),i.d(r,{default:()=>t});class t{#i='[[[ DISABLE_INFO_LOGS ]]]§';#r='[[[ LAZY_HIRPC_LOAD ]]]§';#t='[[[ CLIENT_ID ]]]§';#e='[[[ DISABLE_CONSOLE_LOG_OVERRIDE ]]]§';#s='[[[ MAPPINGS ]]]§';#_='[[[ PATCH_URL_MAPPINGS_CONFIG ]]]§';#a='[[[ OAUTH_SCOPES ]]]§';#E='[[[ TOKEN_REQUEST_PATH ]]]§';#l='[[[ SERVER_REQUEST ]]]§';constructor(){this.DISABLE_INFO_LOGS=this.#O(this.#i,"boolean"),this.LAZY_HIRPC_LOAD=this.#O(this.#r,"boolean"),this.CLIENT_ID=this.#O(this.#t,"string"),this.DISABLE_CONSOLE_LOG_OVERRIDE=this.#O(this.#e,"boolean"),this.MAPPINGS=this.#O(this.#s,"json_array"),this.PATCH_URL_MAPPINGS_CONFIG=this.#O(this.#_,"json"),this.OAUTH_SCOPES=this.#O(this.#a,"string[]"),this.TOKEN_REQUEST_PATH=this.#O(this.#E,"string"),this.SERVER_REQUEST=this.#O(this.#l,"string")}#O(i,r){let t;try{t=i.split("]]] ")[1].slice(0,-1)}catch(i){throw new Error("Build variable undefined")}if("string"==r)return t;if("boolean"==r){if(/1|true/i.test(i))return!0;if(/0|false/i.test(i))return!1;throw new Error("Invalid boolean string")}if("string[]"==r){const i=t.split(",");return 1==i.length&&""==i[0]?[]:i}if("json"==r||"json_array"==r)try{return JSON.parse(t)}catch(i){return"json_array"==r?[]:null}throw new Error("Invalid parse type")}}(self.Dissonity=self.Dissonity||{}).BuildVariables=r})(); \ No newline at end of file diff --git a/unity/Editor/Assets/Template/Dissonity/Bridge/dissonity_hirpc.js.txt b/unity/Editor/Assets/Template/Dissonity/Bridge/dissonity_hirpc.js.txt index 6fce3fe..0d991f3 100644 --- a/unity/Editor/Assets/Template/Dissonity/Bridge/dissonity_hirpc.js.txt +++ b/unity/Editor/Assets/Template/Dissonity/Bridge/dissonity_hirpc.js.txt @@ -1 +1 @@ -(()=>{"use strict";var e,t,s={d:(e,t)=>{for(var i in t)s.o(t,i)&&!s.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:t[i]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{'undefined'!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:'Module'}),Object.defineProperty(e,'__esModule',{value:!0})}},i={};function n(e){console.log(`%c[DissonityHiRpc]%c ${e}`,"color:#8177f6;font-weight: bold;","color:initial;")}function a(e){console.error(`%c[DissonityHiRpc]%c ${e}`,"color:#8177f6;font-weight: bold;","color:initial;")}s.r(i),s.d(i,{default:()=>P}),function(e){e[e.Unfunctional=0]="Unfunctional",e[e.OutsideDiscord=1]="OutsideDiscord",e[e.Errored=2]="Errored",e[e.Loading=3]="Loading",e[e.Stable=4]="Stable"}(e||(e={})),function(e){e[e.Handshake=0]="Handshake",e[e.Frame=1]="Frame",e[e.Close=2]="Close",e[e.Hello=3]="Hello"}(t||(t={}));const r=Object.freeze({DISPATCH:"DISPATCH",AUTHORIZE:"AUTHORIZE",AUTHENTICATE:"AUTHENTICATE"}),o=Object.freeze({READY:"READY",ERROR:"ERROR"}),c=Object.freeze({MOBILE:"mobile",DESKTOP:"desktop"}),h="0.5.2",d="dissonity",l="2.0.0";class p{#e;#t=!1;#s=null;#i=()=>null;#n=()=>null;constructor(e){this.#e=e}async generateHash(e=!1){if(e){if(null!=this.#n(null))throw new Error("App hash is locked")}else{if(null!=this.#s)return this.#s;if(null!=this.#i(null))throw new Error("Access hash is locked")}const t=window.crypto.getRandomValues(new Uint8Array(16)),s=(new TextEncoder).encode(Date.now().toString()),i=new Uint8Array(t.length+s.length);i.set(s),i.set(t,s.length);const n=await window.crypto.subtle.digest("SHA-256",i),a=Array.from(new Uint8Array(n)).map((e=>e.toString(16).padStart(2,"0"))).join("");return e?this.#n=e=>null!=e&&this.compareHashes(e,a):(this.#s=a,this.#i=e=>null!=e&&this.compareHashes(e,a)),a}async requestHash(){return this.#t||this.#e.accessCount>=this.#e.maxAccessCount?null:(this.#e.accessCount++,await this.generateHash())}compareHashes(e,t){if(e.length!=t.length)return!1;let s=0;for(let i=0;i({ready:"",authorize:"",authenticate:"",response:""})}}class f{formatPrice(e,t="en-US"){const{amount:s,currency:i}=e;return Intl.NumberFormat(t,{style:'currency',currency:i}).format(function(e,t){const s=m[t];if(null==s)return console.warn(`Unexpected currency ${t}`),e;return e/10**s}(s,i))}patchUrlMappings(e,{patchFetch:t=!0,patchWebSocket:s=!0,patchXhr:i=!0,patchSrcAttributes:n=!1}={}){if('undefined'!=typeof window){if(t){const t=window.fetch;window.fetch=function(s,i){if(s instanceof Request){const n=D({url:b(s.url),mappings:e}),{url:a,...r}=i??{};return Object.keys(Request.prototype).forEach((e=>{if('url'!==e)try{r[e]=s[e]}catch(t){console.warn(`Remapping fetch request key "${e}" failed`,t)}})),new Promise(((e,i)=>{try{s.blob().then((i=>{'HEAD'!==s.method.toUpperCase()&&'GET'!==s.method.toUpperCase()&&i.size>0&&(r.body=i),e(t(new Request(n,r)))}))}catch(e){i(e)}}))}const n=D({url:s instanceof URL?s:b(s),mappings:e});return t(n,i)}}if(s){class t extends WebSocket{constructor(t,s){super(D({url:t instanceof URL?t:b(t),mappings:e}),s)}}window.WebSocket=t}if(i){const t=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(s,i,n,a,r){const o=D({url:b(i),mappings:e});t.apply(this,[s,o,n,a,r])}}if(n){const t={attributeFilter:['src'],childList:!0,subtree:!0};new MutationObserver((function(t){for(const s of t)'attributes'===s.type&&'src'===s.attributeName?A(s.target,e):'childList'===s.type&&s.addedNodes.forEach((t=>{A(t,e),y(t,e)}))})).observe(window.document,t),window.document.querySelectorAll('[src]').forEach((t=>{A(t,e)}))}}}}var g;!function(e){e.AED="aed",e.AFN="afn",e.ALL="all",e.AMD="amd",e.ANG="ang",e.AOA="aoa",e.ARS="ars",e.AUD="aud",e.AWG="awg",e.AZN="azn",e.BAM="bam",e.BBD="bbd",e.BDT="bdt",e.BGN="bgn",e.BHD="bhd",e.BIF="bif",e.BMD="bmd",e.BND="bnd",e.BOB="bob",e.BOV="bov",e.BRL="brl",e.BSD="bsd",e.BTN="btn",e.BWP="bwp",e.BYN="byn",e.BYR="byr",e.BZD="bzd",e.CAD="cad",e.CDF="cdf",e.CHE="che",e.CHF="chf",e.CHW="chw",e.CLF="clf",e.CLP="clp",e.CNY="cny",e.COP="cop",e.COU="cou",e.CRC="crc",e.CUC="cuc",e.CUP="cup",e.CVE="cve",e.CZK="czk",e.DJF="djf",e.DKK="dkk",e.DOP="dop",e.DZD="dzd",e.EGP="egp",e.ERN="ern",e.ETB="etb",e.EUR="eur",e.FJD="fjd",e.FKP="fkp",e.GBP="gbp",e.GEL="gel",e.GHS="ghs",e.GIP="gip",e.GMD="gmd",e.GNF="gnf",e.GTQ="gtq",e.GYD="gyd",e.HKD="hkd",e.HNL="hnl",e.HRK="hrk",e.HTG="htg",e.HUF="huf",e.IDR="idr",e.ILS="ils",e.INR="inr",e.IQD="iqd",e.IRR="irr",e.ISK="isk",e.JMD="jmd",e.JOD="jod",e.JPY="jpy",e.KES="kes",e.KGS="kgs",e.KHR="khr",e.KMF="kmf",e.KPW="kpw",e.KRW="krw",e.KWD="kwd",e.KYD="kyd",e.KZT="kzt",e.LAK="lak",e.LBP="lbp",e.LKR="lkr",e.LRD="lrd",e.LSL="lsl",e.LTL="ltl",e.LVL="lvl",e.LYD="lyd",e.MAD="mad",e.MDL="mdl",e.MGA="mga",e.MKD="mkd",e.MMK="mmk",e.MNT="mnt",e.MOP="mop",e.MRO="mro",e.MUR="mur",e.MVR="mvr",e.MWK="mwk",e.MXN="mxn",e.MXV="mxv",e.MYR="myr",e.MZN="mzn",e.NAD="nad",e.NGN="ngn",e.NIO="nio",e.NOK="nok",e.NPR="npr",e.NZD="nzd",e.OMR="omr",e.PAB="pab",e.PEN="pen",e.PGK="pgk",e.PHP="php",e.PKR="pkr",e.PLN="pln",e.PYG="pyg",e.QAR="qar",e.RON="ron",e.RSD="rsd",e.RUB="rub",e.RWF="rwf",e.SAR="sar",e.SBD="sbd",e.SCR="scr",e.SDG="sdg",e.SEK="sek",e.SGD="sgd",e.SHP="shp",e.SLL="sll",e.SOS="sos",e.SRD="srd",e.SSP="ssp",e.STD="std",e.SVC="svc",e.SYP="syp",e.SZL="szl",e.THB="thb",e.TJS="tjs",e.TMT="tmt",e.TND="tnd",e.TOP="top",e.TRY="try",e.TTD="ttd",e.TWD="twd",e.TZS="tzs",e.UAH="uah",e.UGX="ugx",e.USD="usd",e.USN="usn",e.USS="uss",e.UYI="uyi",e.UYU="uyu",e.UZS="uzs",e.VEF="vef",e.VND="vnd",e.VUV="vuv",e.WST="wst",e.XAF="xaf",e.XAG="xag",e.XAU="xau",e.XBA="xba",e.XBB="xbb",e.XBC="xbc",e.XBD="xbd",e.XCD="xcd",e.XDR="xdr",e.XFU="xfu",e.XOF="xof",e.XPD="xpd",e.XPF="xpf",e.XPT="xpt",e.XSU="xsu",e.XTS="xts",e.XUA="xua",e.YER="yer",e.ZAR="zar",e.ZMW="zmw",e.ZWL="zwl"}(g||(g={}));const m={[g.AED]:2,[g.AFN]:2,[g.ALL]:2,[g.AMD]:2,[g.ANG]:2,[g.AOA]:2,[g.ARS]:2,[g.AUD]:2,[g.AWG]:2,[g.AZN]:2,[g.BAM]:2,[g.BBD]:2,[g.BDT]:2,[g.BGN]:2,[g.BHD]:3,[g.BIF]:0,[g.BMD]:2,[g.BND]:2,[g.BOB]:2,[g.BOV]:2,[g.BRL]:2,[g.BSD]:2,[g.BTN]:2,[g.BWP]:2,[g.BYR]:0,[g.BYN]:2,[g.BZD]:2,[g.CAD]:2,[g.CDF]:2,[g.CHE]:2,[g.CHF]:2,[g.CHW]:2,[g.CLF]:0,[g.CLP]:0,[g.CNY]:2,[g.COP]:2,[g.COU]:2,[g.CRC]:2,[g.CUC]:2,[g.CUP]:2,[g.CVE]:2,[g.CZK]:2,[g.DJF]:0,[g.DKK]:2,[g.DOP]:2,[g.DZD]:2,[g.EGP]:2,[g.ERN]:2,[g.ETB]:2,[g.EUR]:2,[g.FJD]:2,[g.FKP]:2,[g.GBP]:2,[g.GEL]:2,[g.GHS]:2,[g.GIP]:2,[g.GMD]:2,[g.GNF]:0,[g.GTQ]:2,[g.GYD]:2,[g.HKD]:2,[g.HNL]:2,[g.HRK]:2,[g.HTG]:2,[g.HUF]:2,[g.IDR]:2,[g.ILS]:2,[g.INR]:2,[g.IQD]:3,[g.IRR]:2,[g.ISK]:0,[g.JMD]:2,[g.JOD]:3,[g.JPY]:0,[g.KES]:2,[g.KGS]:2,[g.KHR]:2,[g.KMF]:0,[g.KPW]:2,[g.KRW]:0,[g.KWD]:3,[g.KYD]:2,[g.KZT]:2,[g.LAK]:2,[g.LBP]:2,[g.LKR]:2,[g.LRD]:2,[g.LSL]:2,[g.LTL]:2,[g.LVL]:2,[g.LYD]:3,[g.MAD]:2,[g.MDL]:2,[g.MGA]:2,[g.MKD]:2,[g.MMK]:2,[g.MNT]:2,[g.MOP]:2,[g.MRO]:2,[g.MUR]:2,[g.MVR]:2,[g.MWK]:2,[g.MXN]:2,[g.MXV]:2,[g.MYR]:2,[g.MZN]:2,[g.NAD]:2,[g.NGN]:2,[g.NIO]:2,[g.NOK]:2,[g.NPR]:2,[g.NZD]:2,[g.OMR]:3,[g.PAB]:2,[g.PEN]:2,[g.PGK]:2,[g.PHP]:2,[g.PKR]:2,[g.PLN]:2,[g.PYG]:0,[g.QAR]:2,[g.RON]:2,[g.RSD]:2,[g.RUB]:2,[g.RWF]:0,[g.SAR]:2,[g.SBD]:2,[g.SCR]:2,[g.SDG]:2,[g.SEK]:2,[g.SGD]:2,[g.SHP]:2,[g.SLL]:2,[g.SOS]:2,[g.SRD]:2,[g.SSP]:2,[g.STD]:2,[g.SVC]:2,[g.SYP]:2,[g.SZL]:2,[g.THB]:2,[g.TJS]:2,[g.TMT]:2,[g.TND]:3,[g.TOP]:2,[g.TRY]:2,[g.TTD]:2,[g.TWD]:2,[g.TZS]:2,[g.UAH]:2,[g.UGX]:0,[g.USD]:2,[g.USN]:2,[g.USS]:2,[g.UYI]:0,[g.UYU]:2,[g.UZS]:2,[g.VEF]:2,[g.VND]:0,[g.VUV]:0,[g.WST]:2,[g.XAF]:0,[g.XAG]:0,[g.XAU]:0,[g.XBA]:0,[g.XBB]:0,[g.XBC]:0,[g.XBD]:0,[g.XCD]:2,[g.XDR]:0,[g.XFU]:0,[g.XOF]:0,[g.XPD]:0,[g.XPF]:0,[g.XPT]:0,[g.XSU]:0,[g.XTS]:0,[g.XUA]:0,[g.YER]:2,[g.ZAR]:2,[g.ZMW]:2,[g.ZWL]:2};const w=/\{([a-z]+)\}/g,S='/.proxy';function y(e,t){e.hasChildNodes()&&e.childNodes.forEach((e=>{A(e,t),y(e,t)}))}function A(e,t){if(e instanceof HTMLElement&&e.hasAttribute('src')){const s=e.getAttribute('src'),i=b(s??'');if(i.host===window.location.host)return;if('script'===e.tagName.toLowerCase())!function(e,{url:t,mappings:s}){const i=D({url:t,mappings:s});if(t.toString()!==i.toString()){const i=document.createElement(e.tagName);i.innerHTML=e.innerHTML;for(const t of e.attributes)i.setAttribute(t.name,t.value);i.setAttribute('src',D({url:t,mappings:s}).toString()),e.after(i),e.remove()}}(e,{url:i,mappings:t});else{const n=D({url:i,mappings:t}).toString();n!==s&&e.setAttribute('src',n)}}}function D({url:e,mappings:t}){const s=new URL(e.toString());!s.hostname.includes('discordsays.com')&&!s.hostname.includes('discordsez.com')||s.pathname.startsWith(S)||(s.pathname=S+s.pathname);for(const i of t){const t=R({originalURL:s,prefix:i.prefix,target:i.target,prefixHost:window.location.host});if(null!=t&&t?.toString()!==e.toString())return t}return s}function b(e,t=window.location.protocol,s=window.location.host){return new URL(e,`${t}//${s}`)}function R({originalURL:e,prefix:t,prefixHost:s,target:i}){const n=new URL(`https://${i}`),a=function(e){const t=e.replace(w,((e,t)=>`(?<${t}>[\\w-]+)`));return new RegExp(`${t}(/|$)`)}(n.host.replace(/%7B/g,'{').replace(/%7D/g,'}')),r=e.toString().match(a);if(null==r)return e;const o=new URL(e.toString());return o.host=s,o.pathname=t.replace(w,((e,t)=>{const s=r.groups?.[t];if(null==s)throw new Error('Misconfigured route.');return s})),o.pathname+='/'===o.pathname?e.pathname.slice(1):e.pathname,!o.hostname.includes('discordsays.com')&&!o.hostname.includes('discordsez.com')||o.pathname.startsWith(S)||(o.pathname=S+o.pathname),o.pathname=o.pathname.replace(n.pathname,''),e.pathname.endsWith('/')&&!o.pathname.endsWith('/')&&(o.pathname+='/'),o}class E{#e;#a;#r=new Set(["undefined"!=typeof window?window.location.origin:"","https://discord.com","https://discordapp.com","https://ptb.discord.com","https://ptb.discordapp.com","https://canary.discord.com","https://canary.discordapp.com","https://staging.discord.co","http://localhost:3333","https://pax.discord.com","null"]);constructor(e,t){this.#e=e,this.#a=t,this.receive=this.receive.bind(this),this.authentication=this.authentication.bind(this)}parseMajorMobileVersion(e){if(e&&e.includes("."))try{return parseInt(e.split(".")[0])}catch{return-1}return-1}async receive(s){if(!this.#r.has(s.origin))return;const i=s.data,c=i?.[0];switch(c){case t.Handshake:case t.Hello:case t.Close:break;case t.Frame:{if(this.#e.stateCode==e.Stable){await this.#e.appSenderPromise;const e={hirpc_state:this.#e.stateCode,rpc_message:i};return void this.#e.appSender(this.serializePayload(e))}const t=window.dso_build_variables,s=i?.[1],c=s?.data,h=s?.evt,d=s?.cmd;if(d==r.DISPATCH&&h==o.READY){sessionStorage.setItem("dso_connected","true");const e=this.#e.getMultiEvent();e.ready=this.serializePayload(c),this.#e.getMultiEvent=()=>e,t.DISABLE_CONSOLE_LOG_OVERRIDE||this.#o();try{const e=t.MAPPINGS,s=t.PATCH_URL_MAPPINGS_CONFIG;e.length>0&&(t.DISABLE_INFO_LOGS||n(`Patching url mappings... (${e.length})`),this.#a.patchUrlMappings(e,s))}catch(e){a(`Something went wrong patching the URL mappings: ${e}`)}t.DISABLE_INFO_LOGS||n("Connected to RPC!"),this.#e.dispatchReady()}else if(d==r.AUTHORIZE&&h!=o.ERROR){const e=this.#e.getMultiEvent();e.authorize=this.serializePayload(c),this.#e.getMultiEvent=()=>e}else if(d==r.AUTHENTICATE){sessionStorage.setItem("dso_authenticated","true");const e=this.#e.getMultiEvent();e.authenticate=this.serializePayload(c),this.#e.getMultiEvent=()=>e,this.#e.dispatchAuth()}}}}async authentication(s){if(!this.#r.has(s.origin))return;const i=window.dso_build_variables,c=s.data,h=c?.[0],d=c?.[1],l=d?.data,p=d?.evt,u=d?.cmd;if(h==t.Frame)switch(u){case r.AUTHORIZE:{if(p==o.ERROR)return this.#e.stateCode=e.Errored,this.#e.errorMessage="User unauthorized scopes",window.removeEventListener("message",this.receive),window.removeEventListener("message",this.authentication),void this.send(t.Close,{code:1e3,message:"User unauthorized scopes",nonce:this.getNonce()});i.DISABLE_INFO_LOGS||n("Authorized!");const s=i.TOKEN_REQUEST_PATH,c=i.SERVER_REQUEST;let h={code:l.code};if(""!=c){const e=JSON.parse(c);delete e.code,h={code:l.code,...e}}const d=await fetch(`/.proxy${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:this.serializePayload(h)}),u=await d.json();if(!u.token){const t="The server JSON response didn't include a 'token' field";return a(t),this.#e.stateCode=e.Errored,void(this.#e.errorMessage=t)}const f=this.#e.getMultiEvent();f.response=this.serializePayload(u),this.#e.getMultiEvent=()=>f,this.send(t.Frame,{cmd:r.AUTHENTICATE,nonce:this.getNonce(),args:{access_token:u.token}});break}case r.AUTHENTICATE:window.removeEventListener("message",this.authentication),i.DISABLE_INFO_LOGS||n("Authenticated!")}}send(e,t){let s,i;if(window.parent!=window.parent.parent){const e=window.parent.parent,t=window.parent;s=e.opener??e,i=t.document.referrer?t.document.referrer:"*"}else{const e=window.parent,t=window;s=e.opener??e,i=t.document.referrer?t.document.referrer:"*"}s.postMessage([e,t],i)}getNonce(){const e=new Array(36);for(let t=0;t<36;t++)e[t]=Math.floor(16*Math.random());return e[14]=4,e[19]=e[19]&=-5,e[19]=e[19]|=8,e[8]=e[13]=e[18]=e[23]="-",e.map((e=>e.toString(16))).join("")}serializePayload(e){return JSON.stringify(e,((e,t)=>"bigint"==typeof t?t.toString():t))}#o(){const e=(e,s)=>{this.send(t.Frame,{cmd:"CAPTURE_LOG",nonce:this.getNonce(),args:{level:e,message:s}})};["log","warn","debug","info","error"].forEach((t=>{const s=console[t],i=console;s&&(console[t]=function(){const n=[].slice.call(arguments),a=""+n.join(" ");e(t,a),s.apply(i,n)})}))}}class P{#e;#c;#a;#h;constructor(){if(this.#e=new u,this.#c=new p(this.#e),this.#a=new f,this.#h=new E(this.#e,this.#a),this.#e.readyPromise=new Promise((e=>{this.#e.dispatchReady=e})),this.#e.authPromise=new Promise((e=>{this.#e.dispatchAuth=e})),this.#e.appSenderPromise=new Promise((e=>{this.#e.dispatchAppSender=e})),"undefined"!=typeof window){if(window.dso_hirpc instanceof P)return window.dso_hirpc;Object.defineProperty(window,"dso_hirpc",{value:this,writable:!1,configurable:!1}),Object.freeze(window.dso_hirpc);this.getBuildVariables().DISABLE_INFO_LOGS||this.#d(),window.addEventListener("message",this.#h.receive)}}async load(t=1){if(this.#e.loaded)throw new Error("hiRPC Module already loaded");if("undefined"==typeof window)throw this.#e.stateCode=e.Unfunctional,new Error("Cannot load hiRPC Module outside of a web environment");this.#e.loaded=!0,this.#e.maxAccessCount=t;const s=this.getQueryObject(),i=sessionStorage.getItem("dso_instance_id");return s.instance_id!=i&&(sessionStorage.removeItem("dso_connected"),sessionStorage.removeItem("dso_authenticated"),sessionStorage.setItem("dso_instance_id",s.instance_id)),new Promise((async(t,s)=>{if("true"==sessionStorage.getItem("dso_outside_discord"))return this.#e.stateCode=e.OutsideDiscord,void t();const i=this.getQueryObject();if(!i.frame_id||!i.instance_id||!i.platform)return this.#e.stateCode=e.OutsideDiscord,void t();const n=sessionStorage.getItem("dso_connected");if("false"==n||null==n)await this.#l();else{if("true"!=n)return void s(new Error(`Invalid session storage item: { dso_connected: ${n} }`));this.#e.dispatchReady()}const a=sessionStorage.getItem("dso_authenticated");if("false"==a||null==a)await this.#p();else{if("true"!=a)return void s(new Error(`Invalid session storage item: { dso_authenticated: ${a} }`));this.#e.dispatchAuth()}this.#e.stateCode=e.Stable,t()}))}async#l(){const e=this.getQueryObject(),s=this.getBuildVariables(),i=s.CLIENT_ID,a=s.DISABLE_INFO_LOGS,r=this.#h.parseMajorMobileVersion(e.mobile_app_version),o={v:1,encoding:"json",client_id:i,frame_id:e.frame_id};(e.platform===c.DESKTOP||r>=250)&&(o.sdk_version=l),a||n("Connecting..."),this.#h.send(t.Handshake,o),await this.#e.readyPromise}async#p(){window.addEventListener("message",this.#h.authentication);const e=this.getBuildVariables();this.#h.send(t.Frame,{cmd:r.AUTHORIZE,nonce:this.getNonce(),args:{client_id:e.CLIENT_ID,scope:e.OAUTH_SCOPES,response_type:"code",prompt:"none",state:""}}),await this.#e.authPromise}#d(){n(`hiRPC! version ${h}`)}getBuildVariables(){if("object"==typeof window.dso_build_variables)return window.dso_build_variables;if("object"==typeof window.Dissonity.BuildVariables)return Object.defineProperty(window,"dso_build_variables",{value:new window.Dissonity.BuildVariables.default,writable:!1,configurable:!1}),Object.freeze(window.dso_build_variables),window.dso_build_variables;{const t="Unable to access build variables. Import them through /dissonity_build_variables.js";throw a(t),this.#e.stateCode=e.Errored,this.#e.errorMessage=t,new Error(t)}}patchUrlMappings(e,t,s){this.#c.verifyHash(e)&&this.#a.patchUrlMappings(t,s)}formatPrice(e,t,s){if(this.#c.verifyHash(e))return this.#a.formatPrice(t,s)}getQueryObject(){const e=window.location.search.includes("?")?window.location.search.replace("?","").split("&"):window.parent.location.search.replace("?","").split("&"),t={};if(0!=e.length)for(let s=0;s{this.#e.dispatchAppSender=e})),this.#e.appSender=null)}}(self.Dissonity=self.Dissonity||{}).HiRpc=i})(); \ No newline at end of file +(()=>{"use strict";var e,t,s={d:(e,t)=>{for(var i in t)s.o(t,i)&&!s.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:t[i]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{'undefined'!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:'Module'}),Object.defineProperty(e,'__esModule',{value:!0})}},i={};function n(e){console.log(`%c[DissonityHiRpc]%c ${e}`,"color:#8177f6;font-weight: bold;","color:initial;")}function r(e){console.error(`%c[DissonityHiRpc]%c ${e}`,"color:#8177f6;font-weight: bold;","color:initial;")}s.r(i),s.d(i,{default:()=>P}),function(e){e[e.Unfunctional=0]="Unfunctional",e[e.OutsideDiscord=1]="OutsideDiscord",e[e.Errored=2]="Errored",e[e.Loading=3]="Loading",e[e.Stable=4]="Stable"}(e||(e={})),function(e){e[e.Handshake=0]="Handshake",e[e.Frame=1]="Frame",e[e.Close=2]="Close",e[e.Hello=3]="Hello"}(t||(t={}));const a=Object.freeze({DISPATCH:"DISPATCH",AUTHORIZE:"AUTHORIZE",AUTHENTICATE:"AUTHENTICATE"}),o=Object.freeze({READY:"READY",ERROR:"ERROR"}),c=Object.freeze({MOBILE:"mobile",DESKTOP:"desktop"}),h="0.6.0",d="dissonity",l="2.0.0";class p{#e;#t=!1;#s=null;#i=()=>null;#n=()=>null;constructor(e){this.#e=e}async generateHash(e=!1){if(e){if(null!=this.#n(null))throw new Error("App hash is locked")}else{if(null!=this.#s)return this.#s;if(null!=this.#i(null))throw new Error("Access hash is locked")}const t=window.crypto.getRandomValues(new Uint8Array(16)),s=(new TextEncoder).encode(Date.now().toString()),i=new Uint8Array(t.length+s.length);i.set(s),i.set(t,s.length);const n=await window.crypto.subtle.digest("SHA-256",i),r=Array.from(new Uint8Array(n)).map((e=>e.toString(16).padStart(2,"0"))).join("");return e?this.#n=e=>null!=e&&this.compareHashes(e,r):(this.#s=r,this.#i=e=>null!=e&&this.compareHashes(e,r)),r}async requestHash(){return this.#t||this.#e.accessCount>=this.#e.maxAccessCount?null:(this.#e.accessCount++,await this.generateHash())}compareHashes(e,t){if(e.length!=t.length)return!1;let s=0;for(let i=0;i({ready:"",authorize:"",authenticate:"",response:""})}}class f{formatPrice(e,t="en-US"){const{amount:s,currency:i}=e;return Intl.NumberFormat(t,{style:'currency',currency:i}).format(function(e,t){const s=m[t];if(null==s)return console.warn(`Unexpected currency ${t}`),e;return e/10**s}(s,i))}patchUrlMappings(e,{patchFetch:t=!0,patchWebSocket:s=!0,patchXhr:i=!0,patchSrcAttributes:n=!1}={}){if('undefined'!=typeof window){if(t){const t=window.fetch;window.fetch=function(s,i){if(s instanceof Request){const n=D({url:R(s.url),mappings:e}),{url:r,...a}=i??{};return Object.keys(Request.prototype).forEach((e=>{if('url'!==e)try{a[e]=s[e]}catch(t){console.warn(`Remapping fetch request key "${e}" failed`,t)}})),new Promise(((e,i)=>{try{s.blob().then((i=>{'HEAD'!==s.method.toUpperCase()&&'GET'!==s.method.toUpperCase()&&i.size>0&&(a.body=i),e(t(new Request(n,a)))}))}catch(e){i(e)}}))}const n=D({url:s instanceof URL?s:R(s),mappings:e});return t(n,i)}}if(s){class t extends WebSocket{constructor(t,s){super(D({url:t instanceof URL?t:R(t),mappings:e}),s)}}window.WebSocket=t}if(i){const t=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(s,i,n,r,a){const o=D({url:R(i),mappings:e});t.apply(this,[s,o,n,r,a])}}if(n){const t={attributeFilter:['src'],childList:!0,subtree:!0};new MutationObserver((function(t){for(const s of t)'attributes'===s.type&&'src'===s.attributeName?A(s.target,e):'childList'===s.type&&s.addedNodes.forEach((t=>{A(t,e),y(t,e)}))})).observe(window.document,t),window.document.querySelectorAll('[src]').forEach((t=>{A(t,e)}))}}}}var g;!function(e){e.AED="aed",e.AFN="afn",e.ALL="all",e.AMD="amd",e.ANG="ang",e.AOA="aoa",e.ARS="ars",e.AUD="aud",e.AWG="awg",e.AZN="azn",e.BAM="bam",e.BBD="bbd",e.BDT="bdt",e.BGN="bgn",e.BHD="bhd",e.BIF="bif",e.BMD="bmd",e.BND="bnd",e.BOB="bob",e.BOV="bov",e.BRL="brl",e.BSD="bsd",e.BTN="btn",e.BWP="bwp",e.BYN="byn",e.BYR="byr",e.BZD="bzd",e.CAD="cad",e.CDF="cdf",e.CHE="che",e.CHF="chf",e.CHW="chw",e.CLF="clf",e.CLP="clp",e.CNY="cny",e.COP="cop",e.COU="cou",e.CRC="crc",e.CUC="cuc",e.CUP="cup",e.CVE="cve",e.CZK="czk",e.DJF="djf",e.DKK="dkk",e.DOP="dop",e.DZD="dzd",e.EGP="egp",e.ERN="ern",e.ETB="etb",e.EUR="eur",e.FJD="fjd",e.FKP="fkp",e.GBP="gbp",e.GEL="gel",e.GHS="ghs",e.GIP="gip",e.GMD="gmd",e.GNF="gnf",e.GTQ="gtq",e.GYD="gyd",e.HKD="hkd",e.HNL="hnl",e.HRK="hrk",e.HTG="htg",e.HUF="huf",e.IDR="idr",e.ILS="ils",e.INR="inr",e.IQD="iqd",e.IRR="irr",e.ISK="isk",e.JMD="jmd",e.JOD="jod",e.JPY="jpy",e.KES="kes",e.KGS="kgs",e.KHR="khr",e.KMF="kmf",e.KPW="kpw",e.KRW="krw",e.KWD="kwd",e.KYD="kyd",e.KZT="kzt",e.LAK="lak",e.LBP="lbp",e.LKR="lkr",e.LRD="lrd",e.LSL="lsl",e.LTL="ltl",e.LVL="lvl",e.LYD="lyd",e.MAD="mad",e.MDL="mdl",e.MGA="mga",e.MKD="mkd",e.MMK="mmk",e.MNT="mnt",e.MOP="mop",e.MRO="mro",e.MUR="mur",e.MVR="mvr",e.MWK="mwk",e.MXN="mxn",e.MXV="mxv",e.MYR="myr",e.MZN="mzn",e.NAD="nad",e.NGN="ngn",e.NIO="nio",e.NOK="nok",e.NPR="npr",e.NZD="nzd",e.OMR="omr",e.PAB="pab",e.PEN="pen",e.PGK="pgk",e.PHP="php",e.PKR="pkr",e.PLN="pln",e.PYG="pyg",e.QAR="qar",e.RON="ron",e.RSD="rsd",e.RUB="rub",e.RWF="rwf",e.SAR="sar",e.SBD="sbd",e.SCR="scr",e.SDG="sdg",e.SEK="sek",e.SGD="sgd",e.SHP="shp",e.SLL="sll",e.SOS="sos",e.SRD="srd",e.SSP="ssp",e.STD="std",e.SVC="svc",e.SYP="syp",e.SZL="szl",e.THB="thb",e.TJS="tjs",e.TMT="tmt",e.TND="tnd",e.TOP="top",e.TRY="try",e.TTD="ttd",e.TWD="twd",e.TZS="tzs",e.UAH="uah",e.UGX="ugx",e.USD="usd",e.USN="usn",e.USS="uss",e.UYI="uyi",e.UYU="uyu",e.UZS="uzs",e.VEF="vef",e.VND="vnd",e.VUV="vuv",e.WST="wst",e.XAF="xaf",e.XAG="xag",e.XAU="xau",e.XBA="xba",e.XBB="xbb",e.XBC="xbc",e.XBD="xbd",e.XCD="xcd",e.XDR="xdr",e.XFU="xfu",e.XOF="xof",e.XPD="xpd",e.XPF="xpf",e.XPT="xpt",e.XSU="xsu",e.XTS="xts",e.XUA="xua",e.YER="yer",e.ZAR="zar",e.ZMW="zmw",e.ZWL="zwl"}(g||(g={}));const m={[g.AED]:2,[g.AFN]:2,[g.ALL]:2,[g.AMD]:2,[g.ANG]:2,[g.AOA]:2,[g.ARS]:2,[g.AUD]:2,[g.AWG]:2,[g.AZN]:2,[g.BAM]:2,[g.BBD]:2,[g.BDT]:2,[g.BGN]:2,[g.BHD]:3,[g.BIF]:0,[g.BMD]:2,[g.BND]:2,[g.BOB]:2,[g.BOV]:2,[g.BRL]:2,[g.BSD]:2,[g.BTN]:2,[g.BWP]:2,[g.BYR]:0,[g.BYN]:2,[g.BZD]:2,[g.CAD]:2,[g.CDF]:2,[g.CHE]:2,[g.CHF]:2,[g.CHW]:2,[g.CLF]:0,[g.CLP]:0,[g.CNY]:2,[g.COP]:2,[g.COU]:2,[g.CRC]:2,[g.CUC]:2,[g.CUP]:2,[g.CVE]:2,[g.CZK]:2,[g.DJF]:0,[g.DKK]:2,[g.DOP]:2,[g.DZD]:2,[g.EGP]:2,[g.ERN]:2,[g.ETB]:2,[g.EUR]:2,[g.FJD]:2,[g.FKP]:2,[g.GBP]:2,[g.GEL]:2,[g.GHS]:2,[g.GIP]:2,[g.GMD]:2,[g.GNF]:0,[g.GTQ]:2,[g.GYD]:2,[g.HKD]:2,[g.HNL]:2,[g.HRK]:2,[g.HTG]:2,[g.HUF]:2,[g.IDR]:2,[g.ILS]:2,[g.INR]:2,[g.IQD]:3,[g.IRR]:2,[g.ISK]:0,[g.JMD]:2,[g.JOD]:3,[g.JPY]:0,[g.KES]:2,[g.KGS]:2,[g.KHR]:2,[g.KMF]:0,[g.KPW]:2,[g.KRW]:0,[g.KWD]:3,[g.KYD]:2,[g.KZT]:2,[g.LAK]:2,[g.LBP]:2,[g.LKR]:2,[g.LRD]:2,[g.LSL]:2,[g.LTL]:2,[g.LVL]:2,[g.LYD]:3,[g.MAD]:2,[g.MDL]:2,[g.MGA]:2,[g.MKD]:2,[g.MMK]:2,[g.MNT]:2,[g.MOP]:2,[g.MRO]:2,[g.MUR]:2,[g.MVR]:2,[g.MWK]:2,[g.MXN]:2,[g.MXV]:2,[g.MYR]:2,[g.MZN]:2,[g.NAD]:2,[g.NGN]:2,[g.NIO]:2,[g.NOK]:2,[g.NPR]:2,[g.NZD]:2,[g.OMR]:3,[g.PAB]:2,[g.PEN]:2,[g.PGK]:2,[g.PHP]:2,[g.PKR]:2,[g.PLN]:2,[g.PYG]:0,[g.QAR]:2,[g.RON]:2,[g.RSD]:2,[g.RUB]:2,[g.RWF]:0,[g.SAR]:2,[g.SBD]:2,[g.SCR]:2,[g.SDG]:2,[g.SEK]:2,[g.SGD]:2,[g.SHP]:2,[g.SLL]:2,[g.SOS]:2,[g.SRD]:2,[g.SSP]:2,[g.STD]:2,[g.SVC]:2,[g.SYP]:2,[g.SZL]:2,[g.THB]:2,[g.TJS]:2,[g.TMT]:2,[g.TND]:3,[g.TOP]:2,[g.TRY]:2,[g.TTD]:2,[g.TWD]:2,[g.TZS]:2,[g.UAH]:2,[g.UGX]:0,[g.USD]:2,[g.USN]:2,[g.USS]:2,[g.UYI]:0,[g.UYU]:2,[g.UZS]:2,[g.VEF]:2,[g.VND]:0,[g.VUV]:0,[g.WST]:2,[g.XAF]:0,[g.XAG]:0,[g.XAU]:0,[g.XBA]:0,[g.XBB]:0,[g.XBC]:0,[g.XBD]:0,[g.XCD]:2,[g.XDR]:0,[g.XFU]:0,[g.XOF]:0,[g.XPD]:0,[g.XPF]:0,[g.XPT]:0,[g.XSU]:0,[g.XTS]:0,[g.XUA]:0,[g.YER]:2,[g.ZAR]:2,[g.ZMW]:2,[g.ZWL]:2};const w=/\{([a-z]+)\}/g,S='/.proxy';function y(e,t){e.hasChildNodes()&&e.childNodes.forEach((e=>{A(e,t),y(e,t)}))}function A(e,t){if(e instanceof HTMLElement&&e.hasAttribute('src')){const s=e.getAttribute('src'),i=R(s??'');if(i.host===window.location.host)return;if('script'===e.tagName.toLowerCase())!function(e,{url:t,mappings:s}){const i=D({url:t,mappings:s});if(t.toString()!==i.toString()){const i=document.createElement(e.tagName);i.innerHTML=e.innerHTML;for(const t of e.attributes)i.setAttribute(t.name,t.value);i.setAttribute('src',D({url:t,mappings:s}).toString()),e.after(i),e.remove()}}(e,{url:i,mappings:t});else{const n=D({url:i,mappings:t}).toString();n!==s&&e.setAttribute('src',n)}}}function D({url:e,mappings:t}){const s=new URL(e.toString());!s.hostname.includes('discordsays.com')&&!s.hostname.includes('discordsez.com')||s.pathname.startsWith(S)||(s.pathname=S+s.pathname);for(const i of t){const t=b({originalURL:s,prefix:i.prefix,target:i.target,prefixHost:window.location.host});if(null!=t&&t?.toString()!==e.toString())return t}return s}function R(e,t=window.location.protocol,s=window.location.host){return new URL(e,`${t}//${s}`)}function b({originalURL:e,prefix:t,prefixHost:s,target:i}){const n=new URL(`https://${i}`),r=function(e){const t=e.replace(w,((e,t)=>`(?<${t}>[\\w-]+)`));return new RegExp(`${t}(/|$)`)}(n.host.replace(/%7B/g,'{').replace(/%7D/g,'}')),a=e.toString().match(r);if(null==a)return e;const o=new URL(e.toString());return o.host=s,o.pathname=t.replace(w,((e,t)=>{const s=a.groups?.[t];if(null==s)throw new Error('Misconfigured route.');return s})),o.pathname+='/'===o.pathname?e.pathname.slice(1):e.pathname,!o.hostname.includes('discordsays.com')&&!o.hostname.includes('discordsez.com')||o.pathname.startsWith(S)||(o.pathname=S+o.pathname),o.pathname=o.pathname.replace(n.pathname,''),e.pathname.endsWith('/')&&!o.pathname.endsWith('/')&&(o.pathname+='/'),o}class E{#e;#r;#a;#o=new Set(["undefined"!=typeof window?window.location.origin:"","https://discord.com","https://discordapp.com","https://ptb.discord.com","https://ptb.discordapp.com","https://canary.discord.com","https://canary.discordapp.com","https://staging.discord.co","http://localhost:3333","https://pax.discord.com","null"]);constructor(e,t){this.#e=e,this.#r=this.#c(),this.#a=t,this.receive=this.receive.bind(this),this.authentication=this.authentication.bind(this)}parseMajorMobileVersion(e){if(e&&e.includes("."))try{return parseInt(e.split(".")[0])}catch{return-1}return-1}async receive(s){if(!this.#o.has(s.origin))return;const i=s.data,c=i?.[0];switch(c){case t.Handshake:case t.Hello:case t.Close:break;case t.Frame:{if(this.#e.stateCode==e.Stable){await this.#e.appSenderPromise;const e={hirpc_state:this.#e.stateCode,rpc_message:i};return void this.#e.appSender(this.serializePayload(e))}const t=window.dso_build_variables,s=i?.[1],c=s?.data,h=s?.evt,d=s?.cmd;if(d==a.DISPATCH&&h==o.READY){sessionStorage.setItem("dso_connected","true");const e=this.#e.getMultiEvent();e.ready=this.serializePayload(c),this.#e.getMultiEvent=()=>e,t.DISABLE_CONSOLE_LOG_OVERRIDE||this.#h();try{const e=t.MAPPINGS,s=t.PATCH_URL_MAPPINGS_CONFIG;e.length>0&&(t.DISABLE_INFO_LOGS||n(`Patching url mappings... (${e.length})`),this.#a.patchUrlMappings(e,s))}catch(e){r(`Something went wrong patching the URL mappings: ${e}`)}t.DISABLE_INFO_LOGS||n("Connected to RPC!"),this.#e.dispatchReady()}else if(d==a.AUTHORIZE&&h!=o.ERROR){const e=this.#e.getMultiEvent();e.authorize=this.serializePayload(c),this.#e.getMultiEvent=()=>e}else if(d==a.AUTHENTICATE){sessionStorage.setItem("dso_authenticated","true");const e=this.#e.getMultiEvent();e.authenticate=this.serializePayload(c),this.#e.getMultiEvent=()=>e,this.#e.dispatchAuth()}}}}async authentication(s){if(!this.#o.has(s.origin))return;const i=window.dso_build_variables,c=s.data,h=c?.[0],d=c?.[1],l=d?.data,p=d?.evt,u=d?.cmd;if(h==t.Frame)switch(u){case a.AUTHORIZE:{if(p==o.ERROR)return this.#e.stateCode=e.Errored,this.#e.errorMessage="User unauthorized scopes",window.removeEventListener("message",this.receive),window.removeEventListener("message",this.authentication),void this.send(t.Close,{code:1e3,message:"User unauthorized scopes",nonce:this.getNonce()});i.DISABLE_INFO_LOGS||n("Authorized!");const s=i.TOKEN_REQUEST_PATH,c=i.SERVER_REQUEST;let h={code:l.code};if(""!=c){const e=JSON.parse(c);delete e.code,h={code:l.code,...e}}const d=await fetch(`/.proxy${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:this.serializePayload(h)}),u=await d.json();if(!u.token){const t="The server JSON response didn't include a 'token' field";return r(t),this.#e.stateCode=e.Errored,void(this.#e.errorMessage=t)}const f=this.#e.getMultiEvent();f.response=this.serializePayload(u),this.#e.getMultiEvent=()=>f,this.send(t.Frame,{cmd:a.AUTHENTICATE,nonce:this.getNonce(),args:{access_token:u.token}});break}case a.AUTHENTICATE:window.removeEventListener("message",this.authentication),i.DISABLE_INFO_LOGS||n("Authenticated!")}}send(e,t){const{source:s,sourceOrigin:i}=this.#r;s.postMessage([e,t],i)}getNonce(){const e=new Array(36);for(let t=0;t<36;t++)e[t]=Math.floor(16*Math.random());return e[14]=4,e[19]=e[19]&=-5,e[19]=e[19]|=8,e[8]=e[13]=e[18]=e[23]="-",e.map((e=>e.toString(16))).join("")}serializePayload(e){return JSON.stringify(e,((e,t)=>"bigint"==typeof t?t.toString():t))}#c(){if("undefined"==typeof window)return{source:null,sourceOrigin:"*"};let e,t;if(window.parent!=window.parent.parent){const s=window.parent.parent,i=window.parent;try{e=s.opener??s}catch{e=s}t=i.document.referrer?i.document.referrer:"*"}else{const s=window.parent,i=window;try{e=s.opener??s}catch(t){e=s}t=i.document.referrer?i.document.referrer:"*"}return{source:e,sourceOrigin:t}}#h(){const e=(e,s)=>{this.send(t.Frame,{cmd:"CAPTURE_LOG",nonce:this.getNonce(),args:{level:e,message:s}})};["log","warn","debug","info","error"].forEach((t=>{const s=console[t],i=console;s&&(console[t]=function(){const n=[].slice.call(arguments),r=""+n.join(" ");e(t,r),s.apply(i,n)})}))}}class P{#e;#d;#a;#l;constructor(){if(this.#e=new u,this.#d=new p(this.#e),this.#a=new f,this.#l=new E(this.#e,this.#a),this.#e.readyPromise=new Promise((e=>{this.#e.dispatchReady=e})),this.#e.authPromise=new Promise((e=>{this.#e.dispatchAuth=e})),this.#e.appSenderPromise=new Promise((e=>{this.#e.dispatchAppSender=e})),"undefined"!=typeof window){if(window.dso_hirpc instanceof P)return window.dso_hirpc;const e=this.getQueryObject(),t=sessionStorage.getItem("dso_instance_id");e.instance_id!=t&&(sessionStorage.removeItem("dso_connected"),sessionStorage.removeItem("dso_authenticated"),sessionStorage.setItem("dso_instance_id",e.instance_id)),Object.defineProperty(window,"dso_hirpc",{value:this,writable:!1,configurable:!1}),Object.freeze(window.dso_hirpc);this.getBuildVariables().DISABLE_INFO_LOGS||this.#p(),window.addEventListener("message",this.#l.receive)}}async load(t=1){if(this.#e.loaded)throw new Error("hiRPC Module already loaded");if("undefined"==typeof window)throw this.#e.stateCode=e.Unfunctional,new Error("Cannot load hiRPC Module outside of a web environment");return this.#e.loaded=!0,this.#e.maxAccessCount=t,new Promise((async(t,s)=>{if("true"==sessionStorage.getItem("dso_outside_discord"))return this.#e.stateCode=e.OutsideDiscord,void t();const i=this.getQueryObject();if(!i.frame_id||!i.instance_id||!i.platform)return this.#e.stateCode=e.OutsideDiscord,void t();const n=sessionStorage.getItem("dso_connected");if("false"==n||null==n)await this.#u();else{if("true"!=n)return void s(new Error(`Invalid session storage item: { dso_connected: ${n} }`));this.#e.dispatchReady()}const r=sessionStorage.getItem("dso_authenticated");if("false"==r||null==r)await this.#f();else{if("true"!=r)return void s(new Error(`Invalid session storage item: { dso_authenticated: ${r} }`));this.#e.dispatchAuth()}this.#e.stateCode=e.Stable,t()}))}async#u(){const e=this.getQueryObject(),s=this.getBuildVariables(),i=s.CLIENT_ID,r=s.DISABLE_INFO_LOGS,a=this.#l.parseMajorMobileVersion(e.mobile_app_version),o={v:1,encoding:"json",client_id:i,frame_id:e.frame_id};(e.platform===c.DESKTOP||a>=250)&&(o.sdk_version=l),r||n("Connecting..."),this.#l.send(t.Handshake,o),await this.#e.readyPromise}async#f(){window.addEventListener("message",this.#l.authentication);const e=this.getBuildVariables();this.#l.send(t.Frame,{cmd:a.AUTHORIZE,nonce:this.getNonce(),args:{client_id:e.CLIENT_ID,scope:e.OAUTH_SCOPES,response_type:"code",prompt:"none",state:""}}),await this.#e.authPromise}#p(){n(`hiRPC! version ${h}`)}getBuildVariables(){if("object"==typeof window.dso_build_variables)return window.dso_build_variables;if("object"==typeof window.Dissonity.BuildVariables)return Object.defineProperty(window,"dso_build_variables",{value:new window.Dissonity.BuildVariables.default,writable:!1,configurable:!1}),Object.freeze(window.dso_build_variables),window.dso_build_variables;{const t="Unable to access build variables. Import them through /dissonity_build_variables.js";throw r(t),this.#e.stateCode=e.Errored,this.#e.errorMessage=t,new Error(t)}}patchUrlMappings(e,t,s){this.#d.verifyHash(e)&&this.#a.patchUrlMappings(t,s)}formatPrice(e,t,s){if(this.#d.verifyHash(e))return this.#a.formatPrice(t,s)}getQueryObject(){const e=window.location.search.includes("?")?window.location.search.replace("?","").split("&"):window.parent.location.search.replace("?","").split("&"),t={};if(0!=e.length)for(let s=0;s{this.#e.dispatchAppSender=e})),this.#e.appSender=null)}}(self.Dissonity=self.Dissonity||{}).HiRpc=i})(); \ No newline at end of file diff --git a/unity/Editor/Assets/Template/Dissonity/Bridge/version.d.ts.txt b/unity/Editor/Assets/Template/Dissonity/Bridge/version.d.ts.txt index 2a51c43..dd09c99 100644 --- a/unity/Editor/Assets/Template/Dissonity/Bridge/version.d.ts.txt +++ b/unity/Editor/Assets/Template/Dissonity/Bridge/version.d.ts.txt @@ -1 +1 @@ -export declare const version = "0.5.2"; \ No newline at end of file +export declare const version = "0.6.0"; \ No newline at end of file diff --git a/unity/Editor/Assets/Template/Dissonity/Bridge/version.js.txt b/unity/Editor/Assets/Template/Dissonity/Bridge/version.js.txt index 92231f9..0e487db 100644 --- a/unity/Editor/Assets/Template/Dissonity/Bridge/version.js.txt +++ b/unity/Editor/Assets/Template/Dissonity/Bridge/version.js.txt @@ -1 +1 @@ -export const version = "0.5.2"; \ No newline at end of file +export const version = "0.6.0"; \ No newline at end of file diff --git a/unity/Editor/Assets/Template/Dissonity/app_loader.js.txt b/unity/Editor/Assets/Template/Dissonity/app_loader.js.txt index b48edd8..edb813b 100644 --- a/unity/Editor/Assets/Template/Dissonity/app_loader.js.txt +++ b/unity/Editor/Assets/Template/Dissonity/app_loader.js.txt @@ -17,10 +17,8 @@ let baseUrl = `${window.location.protocol}//${window.location.host}${getPath()}` if (!baseUrl.endsWith("/")) baseUrl += "/"; let outsideDiscord = false; -let proxyPrefixAdded = false; let needsProxyPrefix = false; let loaderPath = "Build/{{{ LOADER_FILENAME }}}"; -const versionCheckPath = baseUrl + ".proxy/version.json"; const proxyBridgeImport = "dso_proxy_bridge/"; const normalBridgeImport = "dso_bridge/"; const hirpcFileName = "dissonity_hirpc.js"; @@ -29,18 +27,21 @@ const MOBILE = "mobile"; let initialWidth = window.innerWidth; let initialHeight = window.innerHeight; async function initialize() { - async function fileExists(url) { - const response = await fetch(url, { method: "HEAD" }); - return response.ok; - } async function updatePaths() { - let { pathname } = window.location; - const pathSegments = pathname.split("/"); - pathSegments.shift(); - proxyPrefixAdded = pathSegments[0] == ".proxy"; - const prefixData = sessionStorage.getItem("dso_needs_prefix"); - needsProxyPrefix = !proxyPrefixAdded && prefixData != "false" && (prefixData == "true" || await fileExists(versionCheckPath)); - outsideDiscord = !proxyPrefixAdded && !needsProxyPrefix; + if (window.location.hostname.endsWith(".discordsays.com")) { + sessionStorage.setItem("dso_outside_discord", "false"); + } + else { + outsideDiscord = true; + sessionStorage.setItem("dso_outside_discord", "true"); + } + if (outsideDiscord || window.location.pathname.startsWith("/.proxy")) { + sessionStorage.setItem("dso_needs_prefix", "false"); + } + else { + needsProxyPrefix = true; + sessionStorage.setItem("dso_needs_prefix", "true"); + } if (needsProxyPrefix) { loaderPath = ".proxy/" + loaderPath; } @@ -56,7 +57,7 @@ async function initialize() { } async function handleHiRpc() { const isNested = window.parent != window.parent.parent; - if (isNested || typeof window.parent?.dso_hirpc == "object") { + if (isNested && typeof window.parent?.dso_hirpc == "object") { Object.defineProperty(window, "dso_hirpc", { value: window.parent.dso_hirpc, writable: false, @@ -85,7 +86,7 @@ async function handleHiRpc() { } async function load() { const hiRpc = new window.Dissonity.HiRpc.default(); - await initialize(hiRpc, false); + await initialize(hiRpc, false || hiRpc.getBuildVariables().LAZY_HIRPC_LOAD); resolve(hiRpc); } }); diff --git a/unity/Editor/BuildPosprocessor.cs b/unity/Editor/BuildPosprocessor.cs index f16ad53..8ac96fd 100644 --- a/unity/Editor/BuildPosprocessor.cs +++ b/unity/Editor/BuildPosprocessor.cs @@ -87,6 +87,13 @@ public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProj substring = fileContent.Substring(index, endIndex - index); fileContent = fileContent.Replace(substring, $"[[[ DISABLE_INFO_LOGS ]]] {data.DisableDissonityInfoLogs}"); + // Lazy hiRPC load + index = fileContent.IndexOf("[[[ LAZY_HIRPC_LOAD ]]]"); + CheckIndex(index); + endIndex = fileContent.IndexOf(VariableSeparator, index); + substring = fileContent.Substring(index, endIndex - index); + fileContent = fileContent.Replace(substring, $"[[[ LAZY_HIRPC_LOAD ]]] {data.LazyHiRpcLoad}"); + // Mappings index = fileContent.IndexOf("[[[ MAPPINGS ]]]"); CheckIndex(index); diff --git a/unity/Editor/Dialogs/UpdateDialog.cs b/unity/Editor/Dialogs/UpdateDialog.cs index fb3adeb..a3a8cd3 100644 --- a/unity/Editor/Dialogs/UpdateDialog.cs +++ b/unity/Editor/Dialogs/UpdateDialog.cs @@ -44,9 +44,9 @@ private void OnGUI() GUILayout.Space(10); GUILayout.BeginVertical(); - GUILayout.Label("v2.0.1 Overview", headerStyle); + GUILayout.Label("v2.1.0 Overview", headerStyle); - GUILayout.Label("Bug fixes", centerStyle); + GUILayout.Label("Compatibility improvements, relationship features and bug fixes", centerStyle); GUILayout.Space(15); @@ -54,7 +54,11 @@ private void OnGUI() GUILayout.Space(10); - GUILayout.Label("- Added link.xml file to the Dissonity folder."); + GUILayout.Label("- LazyHiRpcLoad option in the Advanced Configuration"); + GUILayout.Label("- Api.Commands.GetRelationships (requires Discord approval)"); + GUILayout.Label("- Api.Subscribe.RelationshipUpdate (requires Discord approval)"); + GUILayout.Label("- Models.Relationship"); + GUILayout.Label("- Relationships section in the Discord Mock"); GUILayout.Space(15); @@ -62,7 +66,7 @@ private void OnGUI() GUILayout.Space(10); - GUILayout.Label("- Api.Commands.ShareLink returns a complete mock response."); + GUILayout.Label("- Minor performance improvements related to the hiRPC Interface"); GUILayout.Space(15); @@ -70,13 +74,13 @@ private void OnGUI() GUILayout.Space(10); - GUILayout.Label("- Api.GuildId is now nullable as it should be."); + GUILayout.Label("- The game doesn't turn into a black screen when loading the index.html"); GUILayout.Space(10); - //GUILayout.Label("And more!", italicStyle); + GUILayout.Label("And more!", italicStyle); - //GUILayout.Space(20); + GUILayout.Space(20); if (GUILayout.Button("Got it!", GUILayout.Height(25))) { @@ -87,6 +91,11 @@ private void OnGUI() GUILayout.Label("Links", subHeaderStyle); + if (EditorGUILayout.LinkButton("Full changelog")) + { + Application.OpenURL("https://github.com/Furnyr/Dissonity/releases/tag/v2.1.0"); + } + if (EditorGUILayout.LinkButton("Documentation")) { Application.OpenURL("https://dissonity.dev"); diff --git a/unity/Editor/DiscordMockEditor.cs b/unity/Editor/DiscordMockEditor.cs index 656e906..558a752 100644 --- a/unity/Editor/DiscordMockEditor.cs +++ b/unity/Editor/DiscordMockEditor.cs @@ -16,7 +16,7 @@ internal class DiscordMockEditor : UnityEditor.Editor private bool showIap = false; private bool showCurrentPlayerEvents = false; private bool showOtherPlayers = false; - //private bool showRelationships = false; + private bool showRelationships = false; private bool showChannels = false; private bool showSkus = false; private bool showEntitlements = false; @@ -33,7 +33,7 @@ internal class DiscordMockEditor : UnityEditor.Editor // True when the clear menus are open private bool clearingPlayers = false; - //private bool clearingRelationships = false; + private bool clearingRelationships = false; private bool clearingChannels = false; private bool clearingSkus = false; private bool clearingEntitlements = false; @@ -41,14 +41,17 @@ internal class DiscordMockEditor : UnityEditor.Editor // Handles the "other players" event foldouts private List showOtherPlayerEvents = new(); + // Handles the "relationships" event foldouts + private List showRelationshipEvents = new(); + public override void OnInspectorGUI() { serializedObject.Update(); - DiscordMock mock = (DiscordMock) target; + DiscordMock mock = (DiscordMock)target; - GUIStyle leftButtonStyle; + GUIStyle leftButtonStyle; SetButtonStyles(out leftButtonStyle); @@ -96,7 +99,7 @@ public override void OnInspectorGUI() if (showCurrentPlayerEvents) { StartSpace(40); - DrawDispatchButtons(leftButtonStyle, mock); + DrawPlayerDispatchButtons(leftButtonStyle, mock); EndSpace(); } @@ -119,8 +122,8 @@ public override void OnInspectorGUI() var otherPlayer = otherPlayersProperty.GetArrayElementAtIndex(i); var participant = otherPlayer.FindPropertyRelative(nameof(MockPlayer.Participant)); string name = participant.FindPropertyRelative(nameof(MockParticipant.GlobalName)).stringValue; - - EditorGUILayout.PropertyField(otherPlayer, new GUIContent (name), false); + + EditorGUILayout.PropertyField(otherPlayer, new GUIContent(name), false); if (otherPlayer.isExpanded) { @@ -149,7 +152,7 @@ public override void OnInspectorGUI() } } } - + EditorGUI.indentLevel++; DrawChildrenRecursively(otherPlayer, new string[] { nameof(MockPlayer.GuildMemberRpc) }); @@ -159,14 +162,14 @@ public override void OnInspectorGUI() if (showOtherPlayerEvents[i]) { StartSpace(60); - DrawDispatchButtons(leftButtonStyle, mock, i); + DrawPlayerDispatchButtons(leftButtonStyle, mock, i); EndSpace(); } - + EditorGUI.indentLevel--; } } - + EditorGUI.indentLevel--; // Draw add player button @@ -181,13 +184,13 @@ public override void OnInspectorGUI() player.GuildMemberRpc.UserId = id; // Unique username - player.Participant.Username += $"_{mock._otherPlayers.Count+2}"; + player.Participant.Username += $"_{mock._otherPlayers.Count + 2}"; // Unique global name - player.Participant.GlobalName += $" {mock._otherPlayers.Count+2}"; + player.Participant.GlobalName += $" {mock._otherPlayers.Count + 2}"; // Unique nickname - string nickname = player.Participant.Nickname += $" {mock._otherPlayers.Count+2}"; + string nickname = player.Participant.Nickname += $" {mock._otherPlayers.Count + 2}"; player.GuildMemberRpc.Nickname = nickname; mock._otherPlayers.Add(player); @@ -231,7 +234,6 @@ public override void OnInspectorGUI() //# RELATIONSHIPS - - - - - - /* var relationshipsProperty = serializedObject.FindProperty(nameof(DiscordMock._relationships)); showRelationships = EditorGUILayout.Foldout(showRelationships, "Relationships"); @@ -246,18 +248,54 @@ public override void OnInspectorGUI() var relationship = relationshipsProperty.GetArrayElementAtIndex(i); var user = relationship.FindPropertyRelative(nameof(MockRelationship.User)); string name = user.FindPropertyRelative(nameof(MockUser.GlobalName)).stringValue; - - EditorGUILayout.PropertyField(relationship, new GUIContent (name), false); + + EditorGUILayout.PropertyField(relationship, new GUIContent(name), false); if (relationship.isExpanded) { + //? Handle difference in relationships and tracked relationships + if (showRelationshipEvents.Count != mock._relationships.Count) + { + //? Mock has more + if (showRelationshipEvents.Count < mock._relationships.Count) + { + int newRelationships = mock._relationships.Count - showRelationshipEvents.Count; + + for (int y = 0; y < newRelationships; y++) + { + showRelationshipEvents.Add(false); + } + } + + //? A relationship was deleted, regen + else + { + showRelationshipEvents.Clear(); + + foreach (var _ in mock._relationships) + { + showRelationshipEvents.Add(false); + } + } + } + EditorGUI.indentLevel++; DrawChildrenRecursively(relationship, new string[] { }); + showRelationshipEvents[i] = EditorGUILayout.Foldout(showRelationshipEvents[i], "Dispatch Events"); + + if (showRelationshipEvents[i]) + { + StartSpace(60); + DrawRelationshipDispatchButtons(leftButtonStyle, mock, i); + EndSpace(); + } + EditorGUI.indentLevel--; } } + EditorGUI.indentLevel--; // Draw add relationship button @@ -271,10 +309,10 @@ public override void OnInspectorGUI() user.Id = Utils.GetMockSnowflake(); // Unique username - user.Username += $"_{mock._relationships.Count+1}"; + user.Username += $"_{mock._relationships.Count + 1}"; // Unique global name - user.GlobalName += $" {mock._relationships.Count+1}"; + user.GlobalName += $" {mock._relationships.Count + 1}"; // Unique nickname @@ -311,6 +349,7 @@ public override void OnInspectorGUI() { clearingRelationships = false; relationshipsProperty.ClearArray(); + showRelationshipEvents.Clear(); } ResetButtonTint(); @@ -320,7 +359,6 @@ public override void OnInspectorGUI() } else if (clearingRelationships) clearingRelationships = false; - */ //# CHANNELS - - - - - var channelsProperty = serializedObject.FindProperty(nameof(DiscordMock._channels)); @@ -336,8 +374,8 @@ public override void OnInspectorGUI() // Draw the array element var channel = channelsProperty.GetArrayElementAtIndex(i); string channelName = channel.FindPropertyRelative(nameof(MockChannel.Name)).stringValue; - - EditorGUILayout.PropertyField(channel, new GUIContent (channelName), false); + + EditorGUILayout.PropertyField(channel, new GUIContent(channelName), false); if (channel.isExpanded) { @@ -348,7 +386,7 @@ public override void OnInspectorGUI() EditorGUI.indentLevel--; } } - + EditorGUI.indentLevel--; // Draw add channel button @@ -426,7 +464,7 @@ public override void OnInspectorGUI() } if (!Api.Configuration.DisableDissonityInfoLogs) Utils.DissonityLog("Dispatching mock Activity Instance Participants Update"); - + mock.ActivityInstanceParticipantsUpdate(); } @@ -598,7 +636,7 @@ public override void OnInspectorGUI() Debug.Log("[Dissonity Editor] You can only dispatch events during runtime!"); return; } - + if (!Api.Configuration.DisableDissonityInfoLogs) Utils.DissonityLog("Dispatching mock Thermal State Update"); mock.ThermalStateUpdate(); @@ -624,7 +662,7 @@ public override void OnInspectorGUI() EditorGUI.indentLevel--; } - + EndSpace(); } @@ -649,8 +687,8 @@ public override void OnInspectorGUI() // Draw the array element var sku = skusProperty.GetArrayElementAtIndex(i); string skuName = sku.FindPropertyRelative(nameof(MockSku.Name)).stringValue; - - EditorGUILayout.PropertyField(sku, new GUIContent (skuName), false); + + EditorGUILayout.PropertyField(sku, new GUIContent(skuName), false); if (sku.isExpanded) { @@ -664,7 +702,7 @@ public override void OnInspectorGUI() EditorGUI.indentLevel--; } } - + EditorGUI.indentLevel--; // Draw add sku button @@ -736,14 +774,14 @@ public override void OnInspectorGUI() var entitlement = entitlementsProperty.GetArrayElementAtIndex(i); string name = entitlement.FindPropertyRelative(nameof(MockEntitlement._mock_name)).stringValue; long id = entitlement.FindPropertyRelative(nameof(MockEntitlement.Id)).longValue; - - EditorGUILayout.PropertyField(entitlement, new GUIContent (name), false); + + EditorGUILayout.PropertyField(entitlement, new GUIContent(name), false); if (entitlement.isExpanded) { EditorGUI.indentLevel++; - DrawChildrenRecursively(entitlement, null, new Dictionary{{ nameof(MockEntitlement._mock_name), "Entitlements don't have names, this only changes the visual name in the mock." }}); + DrawChildrenRecursively(entitlement, null, new Dictionary { { nameof(MockEntitlement._mock_name), "Entitlements don't have names, this only changes the visual name in the mock." } }); StartSpace(40); @@ -757,7 +795,7 @@ public override void OnInspectorGUI() } if (!Api.Configuration.DisableDissonityInfoLogs) Utils.DissonityLog("Dispatching mock Entitlement Create"); - + mock.EntitlementCreate(id); } @@ -768,7 +806,7 @@ public override void OnInspectorGUI() EditorGUI.indentLevel--; } } - + EditorGUI.indentLevel--; // Draw add sku button @@ -826,12 +864,12 @@ public override void OnInspectorGUI() EditorGUI.indentLevel--; } - + serializedObject.ApplyModifiedProperties(); } - private void DrawDispatchButtons(GUIStyle style, DiscordMock mock, int playerIndex = -1) + private void DrawPlayerDispatchButtons(GUIStyle style, DiscordMock mock, int playerIndex = -1) { // Shorcut bool isPlaying = UnityEngine.Application.isPlaying; @@ -910,5 +948,25 @@ private void DrawDispatchButtons(GUIStyle style, DiscordMock mock, int playerInd mock.SpeakingStop(playerIndex); } } + + private void DrawRelationshipDispatchButtons(GUIStyle style, DiscordMock mock, int relationshipIndex) + { + // Shorcut + bool isPlaying = UnityEngine.Application.isPlaying; + + if (GUILayout.Button("Relationship Update", style)) + { + //? Not in runtime + if (!isPlaying) + { + Debug.Log("[Dissonity Editor] You can only dispatch events during runtime!"); + return; + } + + if (!Api.Configuration.DisableDissonityInfoLogs) Utils.DissonityLog("Dispatching mock Relationship Update"); + + mock.RelationshipUpdate(relationshipIndex); + } + } } } \ No newline at end of file diff --git a/unity/Plugins/HiRpcPlugin.jslib b/unity/Plugins/HiRpcPlugin.jslib index 0f1ade6..8b18a23 100644 --- a/unity/Plugins/HiRpcPlugin.jslib +++ b/unity/Plugins/HiRpcPlugin.jslib @@ -8,9 +8,21 @@ mergeInto(LibraryManager.library, { DsoOpenDownwardFlow: function () { const hiRpc = window.dso_hirpc; - hiRpc.openDownwardFlow((stringifiedData) => { - SendMessage("_DissonityBridge", "_HiRpcInput", stringifiedData); - }); + if (hiRpc.getBuildVariables().LAZY_HIRPC_LOAD) { + hiRpc.load(0) + .then(openFlow) + .catch(err => { + console.log(err); + }); + } + else { + openFlow(); + } + function openFlow() { + hiRpc.openDownwardFlow((stringifiedData) => { + SendMessage("_DissonityBridge", "_HiRpcInput", stringifiedData); + }); + } }, DsoEmptyRequest: function (stringifiedMessage) { const { nonce, app_hash } = JSON.parse(UTF8ToString(stringifiedMessage)); @@ -60,9 +72,9 @@ mergeInto(LibraryManager.library, { hiRpc.sendToApp(app_hash, "dissonity", payload); }, DsoSendToRpc: function (stringifiedMessage) { - const { data, app_hash } = JSON.parse(UTF8ToString(stringifiedMessage)); + const { data } = JSON.parse(UTF8ToString(stringifiedMessage)); const hiRpc = window.dso_hirpc; - hiRpc.sendToRpc(app_hash, data[0], data[1]); + hiRpc.sendToRpc(data[0], data[1]); }, DsoExpandCanvas: function () { if (typeof window.dso_expand_canvas == "undefined") diff --git a/unity/README.md b/unity/README.md index 7906cec..5a5bd9a 100644 --- a/unity/README.md +++ b/unity/README.md @@ -60,6 +60,8 @@ public class MyScript : MonoBehaviour ## Installation +> You can also install Dissonity via [OpenUPM](https://openupm.com/packages/com.furnyr.dissonity/) or with each `.unitypackage` file found in the [Releases](https://github.com/Furnyr/Dissonity/releases) tab. + 1. Create a new Unity project (Unity 2021.3 or later, Unity 6 recommended) 2. Open the package manager and install the package from this git URL: `https://github.com/Furnyr/Dissonity.git?path=/unity#v2` 3. Use the pop-up dialog to select a configuration file @@ -71,7 +73,7 @@ Dissonity is now installed! But you still need to configure a few things: ## Configuration 1. Open the configuration file in Assets/Dissonity/DissonityConfiguration.cs -2. Set your app id in `.ClientId` (find it [here](https://discord.com/developers/applications)) +2. Set your app id in `.ClientId` (find it or create a Discord app [here](https://discord.com/developers/applications)) Up and running! If you want to test your activity within Unity: @@ -89,6 +91,9 @@ Dissonity helps in the process to make the game, but you will still need to host If you're not sure how to continue, read the documentation. +> [!TIP] +> If you are also using a framework that provides Discord functionality, you should read [Third-party support](http://dissonity.dev/guides/v2/third-party-support). + ## Documentation diff --git a/unity/Runtime/Commands/DiscordCommandType.cs b/unity/Runtime/Commands/DiscordCommandType.cs index d9718b9..b10bb5f 100644 --- a/unity/Runtime/Commands/DiscordCommandType.cs +++ b/unity/Runtime/Commands/DiscordCommandType.cs @@ -27,5 +27,6 @@ internal static class DiscordCommandType public const string ShareLink = "SHARE_LINK"; public const string GetRelationships = "GET_RELATIONSHIPS"; public const string GetUser = "GET_USER"; + public const string InviteUserEmbedded = "INVITE_USER_EMBEDDED"; } } diff --git a/unity/Runtime/Commands/InviteUserEmbedded.cs b/unity/Runtime/Commands/InviteUserEmbedded.cs new file mode 100644 index 0000000..5727170 --- /dev/null +++ b/unity/Runtime/Commands/InviteUserEmbedded.cs @@ -0,0 +1,25 @@ +using System; +using Newtonsoft.Json; + +namespace Dissonity.Commands +{ + [Serializable] + internal class InviteUserEmbedded : FrameCommand + { + #nullable enable annotations + + internal override string Command => DiscordCommandType.InviteUserEmbedded; + + [JsonProperty("user_id")] + public string UserId { get; set; } + + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string? Content { get; set; } + + public InviteUserEmbedded(string userId, string? content) + { + UserId = userId; + Content = content; + } + } +} \ No newline at end of file diff --git a/unity/Runtime/Commands/InviteUserEmbedded.cs.meta b/unity/Runtime/Commands/InviteUserEmbedded.cs.meta new file mode 100644 index 0000000..4f34087 --- /dev/null +++ b/unity/Runtime/Commands/InviteUserEmbedded.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 12d415bc34f32d442b3a8a235f9cdddf \ No newline at end of file diff --git a/unity/Runtime/DiscordMock.cs b/unity/Runtime/DiscordMock.cs index f6780c3..587cb09 100644 --- a/unity/Runtime/DiscordMock.cs +++ b/unity/Runtime/DiscordMock.cs @@ -22,7 +22,7 @@ public class DiscordMock : MonoBehaviour public string _accessToken = "mock-access-token"; public MockPlayer _currentPlayer = new(); public List _otherPlayers = new(); - internal List _relationships = new(); + public List _relationships = new(); public List _channels = new(); // General events @@ -46,13 +46,13 @@ void Awake() return; } - if (Singleton != null && Singleton != this) + if (Singleton != null && Singleton != this) { - Destroy(gameObject); + Destroy(gameObject); } - else + else { - Singleton = this; + Singleton = this; } DontDestroyOnLoad(gameObject); @@ -208,7 +208,7 @@ public void CurrentGuildMemberUpdate() data.Nickname = _currentPlayer.Participant.Nickname; data.UserId = _currentPlayer.Participant.Id; - Api.bridge!.MockDiscordDispatch(DiscordEventType.CurrentGuildMemberUpdate, data); + Api.bridge!.MockDiscordDispatch(DiscordEventType.CurrentGuildMemberUpdate, data); } @@ -258,7 +258,7 @@ public void SpeakingStart(int playerIndex = -1) ChannelId = _query.ChannelId, UserId = userId }; - + Api.bridge!.MockDiscordDispatch(DiscordEventType.SpeakingStart, data); } @@ -281,10 +281,10 @@ public void SpeakingStop(int playerIndex = -1) ChannelId = _query.ChannelId, UserId = userId }; - + Api.bridge!.MockDiscordDispatch(DiscordEventType.SpeakingStop, data); } - + /// /// Dispatch a mock Entitlement Create. @@ -306,5 +306,20 @@ public void EntitlementCreate(long mockEntitlementId) Api.bridge!.MockDiscordDispatch(DiscordEventType.EntitlementCreate, data); } + + /// + /// Dispatch a mock Relationship Update. + /// + public void RelationshipUpdate(int relationshipIndex = -1) + { + //\ Dispatch + Relationship data; + + //? Test + if (relationshipIndex == -1) data = new MockRelationship().ToRelationship(); + else data = _relationships[relationshipIndex].ToRelationship(); + + Api.bridge!.MockDiscordDispatch(DiscordEventType.RelationshipUpdate, data); + } } } \ No newline at end of file diff --git a/unity/Runtime/DissonityApi.cs b/unity/Runtime/DissonityApi.cs index 8545307..b24a64d 100644 --- a/unity/Runtime/DissonityApi.cs +++ b/unity/Runtime/DissonityApi.cs @@ -482,7 +482,7 @@ public static async Task EncourageHardwareAcc var response = await SendCommand(new()); - return response.Data; + return response.Data; } @@ -512,7 +512,7 @@ public static async Task EncourageHardwareAcc if (!_configuration!.OauthScopes.Contains(OauthScope.Guilds) || !_configuration!.OauthScopes.Contains(OauthScope.DmChannelsRead)) { if (!_configuration!.DisableDissonityInfoLogs) Utils.DissonityLogWarning("Channel is DM and oauth scopes don't include 'guilds' and 'dm_channels.read'. Can't get the channel"); - + throw new CommandException("Invalid oauth scopes inside mock", (int) RpcErrorCode.InvalidPermissions); } } @@ -558,7 +558,7 @@ public static async Task GetChannelPermissions() if (!_configuration!.OauthScopes.Contains(OauthScope.GuildsMembersRead)) { if (!_configuration!.DisableDissonityInfoLogs) Utils.DissonityLogWarning("Oauth scopes don't include 'guilds.members.read'. Can't get channel permissions"); - + throw new CommandException("Invalid oauth scopes inside mock", (int) RpcErrorCode.InvalidPermissions); } @@ -664,7 +664,7 @@ public static async Task StartPurchase(long skuId) if (response.Data == null) return new Entitlement[] {}; - return response.Data; + return response.Data; } @@ -741,7 +741,8 @@ public static async Task OpenInviteDialog() if (_mock) { - if (!_configuration!.DisableDissonityInfoLogs) { + if (!_configuration!.DisableDissonityInfoLogs) + { Utils.DissonityLog("Invite dialog sent"); } @@ -774,7 +775,7 @@ public static async Task OpenShareMomentDialog(string mediaUrl) if (Platform == Models.Platform.Desktop) Utils.DissonityLog($"Share moment dialog with ({mediaUrl}) sent"); else Utils.DissonityLogWarning("Platform is mobile, not possible to open a share moment dialog"); } - + return; } @@ -796,14 +797,14 @@ public static async Task OpenShareMomentDialog(string mediaUrl) public static async Task SetActivity(ActivityBuilder activity) { if (!_ready) throw new InvalidOperationException("Tried to use a command without being ready"); - + if (_mock) { //? Invalid scopes if (!_configuration!.OauthScopes.Contains(OauthScope.RpcActivitiesWrite)) { if (!_configuration!.DisableDissonityInfoLogs) Utils.DissonityLogWarning("Oauth scopes don't include 'rpc.activities.write'. Can't set the activity"); - + throw new CommandException("Invalid oauth scopes inside mock", (int) RpcErrorCode.InvalidPermissions); } @@ -877,7 +878,7 @@ public static async Task SetOrientationLockState(OrientationLockStateType lockSt if (Platform == Models.Platform.Mobile) Utils.DissonityLog($"Set orientation lock state to ({lockState})"); else Utils.DissonityLogWarning("Platform is desktop, not possible to set orientation lock state"); } - + return; } @@ -906,8 +907,8 @@ public static async Task UserSettingsGetLocale() if (!_configuration!.OauthScopes.Contains(OauthScope.Identify)) { if (!_configuration!.DisableDissonityInfoLogs) Utils.DissonityLogWarning("Oauth scopes don't include 'identify'. Can't get user locale"); - - throw new CommandException("Invalid oauth scopes inside mock", (int) RpcErrorCode.InvalidPermissions); + + throw new CommandException("Invalid oauth scopes inside mock", (int)RpcErrorCode.InvalidPermissions); } var mockResponse = await MockSendCommand(); @@ -975,7 +976,7 @@ public static async Task GetInstanceConnectedParticipants() return response.Data.Participants; } - + /// /// Presents a modal for the user to share a link to your activity with custom query params.

/// No scopes required.
@@ -1003,18 +1004,32 @@ public static async Task ShareLink(string message, string? custom return response.Data; } - //todo This is now documented and can be released in the next minor update. /// - /// Available in the official SDK but not documented in https://discord.com/developers/docs/developer-tools/embedded-app-sdk + /// Returns the current user's relationships.

+ /// Scopes required: relationships.read

+ /// relationships.read requires approval from Discord.
+ /// ----------------------
+ /// ✅ | Web
+ /// ✅ | iOS
+ /// ✅ | Android
+ /// ----------------------
///
/// /// - private static async Task GetRelationships() + public static async Task GetRelationships() { if (!_ready) throw new InvalidOperationException("Tried to use a command without being ready"); if (_mock) { + //? Invalid scopes + if (!_configuration!.OauthScopes.Contains(OauthScope.RelationshipsRead)) + { + if (!_configuration!.DisableDissonityInfoLogs) Utils.DissonityLogWarning("Cannot get relationships without the oauth scope 'relationships.read'"); + + throw new CommandException("Invalid oauth scopes inside mock", (int)RpcErrorCode.InvalidPermissions); + } + var mockResponse = await MockSendCommand(); return mockResponse.Data.Relationships; @@ -1024,10 +1039,12 @@ private static async Task GetRelationships() return response.Data.Relationships; } - - //todo Not documented + + //todo Not documented but well-known functionality /// - /// Available in the official SDK but not documented in https://discord.com/developers/docs/developer-tools/embedded-app-sdk + /// Returns a user.

+ /// Available in the official SDK but not documented in https://discord.com/developers/docs/developer-tools/embedded-app-sdk

+ /// Consider contributing. ///
/// /// @@ -1046,6 +1063,31 @@ private static async Task GetRelationships() return response.Data; } + + //todo Not documented + /// + /// Invite a user.

+ /// Available in the official SDK but not documented in https://discord.com/developers/docs/developer-tools/embedded-app-sdk

+ /// Consider contributing. + ///
+ /// + /// + private static async Task InviteUserEmbedded(long userId, string? content = null) + { + if (!_ready) throw new InvalidOperationException("Tried to use a command without being ready"); + + if (_mock) + { + if (!_configuration!.DisableDissonityInfoLogs) + { + Utils.DissonityLog("Embedded invitation sent"); + } + + return; + } + + await SendCommand(new(userId.ToString(), content)); + } } //# PROXY - - - - - @@ -1708,6 +1750,33 @@ public static async Task VoiceStateUpdate(long channelId, A return reference; } + + /// + /// Received when a relationship of the current user is updated.

+ /// Scopes required: relationships.read

+ /// relationships.read requires approval from Discord.
+ ///
+ /// + /// + public static async Task RelationshipUpdate(Action listener) + { + if (!_ready) throw new InvalidOperationException("Tried to subscribe without being ready"); + + if (_mock) + { + //? Invalid scopes + if (!_configuration!.OauthScopes.Contains(OauthScope.RelationshipsRead)) + { + if (!_configuration!.DisableDissonityInfoLogs) Utils.DissonityLogWarning("Cannot subscribe to Relationship Update without the oauth scope 'relationships.read'"); + + throw new CommandException("Invalid oauth scopes inside mock", (int) RpcErrorCode.InvalidPermissions); + } + } + + var reference = await SubscribeCommandFactory(listener); + + return reference; + } // Method used to simplify the subscription methods internal static async Task SubscribeCommandFactory(Action listener, object? args = null, bool isInternal = false, bool once = false) where TEvent : DiscordEvent @@ -2162,6 +2231,7 @@ async void ListenToPayload() // After opening the downward flow, hiRPC will send the first payload (dissonity channel handshake) once ready. // From then, the JS and C# layer can interact. + // This loads the hiRPC module if LAZY_HIRPC_LOAD is set to true. DsoOpenDownwardFlow(); return tcs.Task; @@ -2649,8 +2719,7 @@ private static void SendToBridge(TCommand command) where TC #if !UNITY_EDITOR BridgeMessage message = new() { - Data = new object[2] { command.Opcode, payload }, - AppHash = _appHash! + Data = new object[2] { command.Opcode, payload } }; //\ Send data to RPC @@ -2935,7 +3004,20 @@ private static Task MockSendCommand(object? arg = null) wh response.Data = mock._currentPlayer.Participant.ToUser(); } - else if (!_configuration!.DisableDissonityInfoLogs) Utils.DissonityLogWarning("You can get mock user data by calling Api.Commands.GetUser with a mock user id"); + else + { + MockRelationship? mockRelationship = mock._relationships.Find(c => c.User.Id == (long)arg!); + + if (mockRelationship != null) + { + response.Data = mockRelationship.User.ToUser(); + } + + else if (!_configuration!.DisableDissonityInfoLogs) + { + Utils.DissonityLogWarning("You can get mock user data by calling Api.Commands.GetUser with a mock user id"); + } + } ((TaskCompletionSource) (object) tcs).TrySetResult(response); } diff --git a/unity/Runtime/DissonityConfigAttribute.cs b/unity/Runtime/DissonityConfigAttribute.cs index 1c84876..4d01303 100644 --- a/unity/Runtime/DissonityConfigAttribute.cs +++ b/unity/Runtime/DissonityConfigAttribute.cs @@ -55,6 +55,7 @@ public static I_UserData GetUserConfig() Type requestType = ((ISdkConfiguration) instance).GetRequestType(); Type responseType = ((ISdkConfiguration) instance).GetResponseType(); bool disableDissonityInfoLogs = ((ISdkConfiguration) instance).DisableDissonityInfoLogs; + bool lazyHiRpcLoad = ((ISdkConfiguration) instance).LazyHiRpcLoad; MappingBuilder[] mappings = ((ISdkConfiguration) instance).Mappings; PatchUrlMappingsConfigBuilder patchConfig = ((ISdkConfiguration) instance).PatchUrlMappingsConfig; ScreenResolution desktopResolution = ((ISdkConfiguration) instance).DesktopResolution; @@ -76,6 +77,7 @@ public static I_UserData GetUserConfig() ServerTokenRequest = requestType, ServerTokenResponse = responseType, DisableDissonityInfoLogs = disableDissonityInfoLogs, + LazyHiRpcLoad = lazyHiRpcLoad, Mappings = mappings, PatchUrlMappingsConfig = patchConfig, DesktopResolution = desktopResolution, diff --git a/unity/Runtime/Events/DiscordEventType.cs b/unity/Runtime/Events/DiscordEventType.cs index e0e44b3..0f90fbb 100644 --- a/unity/Runtime/Events/DiscordEventType.cs +++ b/unity/Runtime/Events/DiscordEventType.cs @@ -15,5 +15,6 @@ public static class DiscordEventType public const string EntitlementCreate = "ENTITLEMENT_CREATE"; public const string ThermalStateUpdate = "THERMAL_STATE_UPDATE"; public const string ActivityInstanceParticipantsUpdate = "ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE"; + public const string RelationshipUpdate = "RELATIONSHIP_UPDATE"; } } \ No newline at end of file diff --git a/unity/Runtime/Events/EventUtility.cs b/unity/Runtime/Events/EventUtility.cs index 91fb6e1..2421ffe 100644 --- a/unity/Runtime/Events/EventUtility.cs +++ b/unity/Runtime/Events/EventUtility.cs @@ -23,6 +23,7 @@ internal static class EventUtility { DiscordEventType.OrientationUpdate, typeof(OrientationUpdate) }, { DiscordEventType.ThermalStateUpdate, typeof(ThermalStateUpdate) }, { DiscordEventType.EntitlementCreate, typeof(EntitlementCreate) }, + { DiscordEventType.RelationshipUpdate, typeof(RelationshipUpdate) }, }; // Don't confuse "event data" with "event return data". @@ -30,7 +31,7 @@ internal static class EventUtility // while event return data is the useful data returned to the user. // E.g.: ActivityInstanceParticipantsUpdateData vs Participant[] - internal static Dictionary EventDataMap = new () + internal static Dictionary EventDataMap = new() { { DiscordEventType.Ready, typeof(ReadyEventData) }, { DiscordEventType.Error, typeof(ErrorEventData) }, @@ -44,9 +45,10 @@ internal static class EventUtility { DiscordEventType.OrientationUpdate, typeof(OrientationUpdateData) }, { DiscordEventType.ThermalStateUpdate, typeof(ThermalStateUpdateData) }, { DiscordEventType.EntitlementCreate, typeof(EntitlementCreateData) }, + { DiscordEventType.RelationshipUpdate, typeof(Relationship) }, }; - internal static Dictionary EventReturnDataMap = new () + internal static Dictionary EventReturnDataMap = new() { { DiscordEventType.Ready, typeof(ReadyEventData) }, { DiscordEventType.Error, typeof(ErrorEventData) }, @@ -60,6 +62,7 @@ internal static class EventUtility { DiscordEventType.OrientationUpdate, typeof(OrientationType) }, { DiscordEventType.ThermalStateUpdate, typeof(ThermalStateType) }, { DiscordEventType.EntitlementCreate, typeof(Entitlement) }, + { DiscordEventType.RelationshipUpdate, typeof(Relationship) }, }; internal static Type GetTypeFromString(string eventString) diff --git a/unity/Runtime/Events/RelationshipUpdate.cs b/unity/Runtime/Events/RelationshipUpdate.cs new file mode 100644 index 0000000..d762a16 --- /dev/null +++ b/unity/Runtime/Events/RelationshipUpdate.cs @@ -0,0 +1,13 @@ +using System; +using Dissonity.Models; +using Newtonsoft.Json; + +namespace Dissonity.Events +{ + [Serializable] + internal class RelationshipUpdate : DiscordEvent + { + [JsonProperty("data")] + new public Relationship Data { get; set; } + } +} \ No newline at end of file diff --git a/unity/Runtime/Events/RelationshipUpdate.cs.meta b/unity/Runtime/Events/RelationshipUpdate.cs.meta new file mode 100644 index 0000000..74ce24d --- /dev/null +++ b/unity/Runtime/Events/RelationshipUpdate.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 380fded47810e284ea414bfd52c6239a \ No newline at end of file diff --git a/unity/Runtime/Models/Mock/MockRelationship.cs b/unity/Runtime/Models/Mock/MockRelationship.cs index 7578081..7a82def 100644 --- a/unity/Runtime/Models/Mock/MockRelationship.cs +++ b/unity/Runtime/Models/Mock/MockRelationship.cs @@ -3,7 +3,7 @@ namespace Dissonity.Models.Mock { [Serializable] - internal class MockRelationship : Relationship + public class MockRelationship : Relationship { new public RelationshipType Type = RelationshipType.Friend; diff --git a/unity/Runtime/Models/Relationship.cs b/unity/Runtime/Models/Relationship.cs index 05c13d9..97fc049 100644 --- a/unity/Runtime/Models/Relationship.cs +++ b/unity/Runtime/Models/Relationship.cs @@ -4,7 +4,7 @@ namespace Dissonity.Models { [Serializable] - internal class Relationship + public class Relationship { #nullable enable annotations diff --git a/unity/Runtime/SdkConfiguration.cs b/unity/Runtime/SdkConfiguration.cs index d1b751c..5cfdbb8 100644 --- a/unity/Runtime/SdkConfiguration.cs +++ b/unity/Runtime/SdkConfiguration.cs @@ -14,6 +14,7 @@ public interface ISdkConfiguration // Optional bool DisableConsoleLogOverride { get; } bool DisableDissonityInfoLogs { get; } + bool LazyHiRpcLoad { get; } MappingBuilder[] Mappings { get; } PatchUrlMappingsConfigBuilder PatchUrlMappingsConfig { get; } bool SynchronizeUser { get; } @@ -56,6 +57,12 @@ public abstract class SdkConfiguration : ISdkConfiguration ///
public virtual bool DisableConsoleLogOverride { get; } = true; + /// + /// Load the hiRPC module on Api.Initialize, rather than before loading the game.

+ /// Defaults to false. + ///
+ public virtual bool LazyHiRpcLoad { get; } = false; + /// /// Disable information logs.

/// It's recommended to keep logs enabled during testing. @@ -137,6 +144,7 @@ public class I_UserData : ISdkConfiguration // Optional public bool DisableConsoleLogOverride { get; set; } + public bool LazyHiRpcLoad { get; set; } public bool DisableDissonityInfoLogs { get; set; } public bool SynchronizeUser { get; set; } public bool SynchronizeGuildMemberRpc { get; set; } diff --git a/unity/Tests/Runtime/DoubleInitialize.cs b/unity/Tests/Runtime/DoubleInitialize.cs index d7fa0cf..158ff09 100644 --- a/unity/Tests/Runtime/DoubleInitialize.cs +++ b/unity/Tests/Runtime/DoubleInitialize.cs @@ -15,7 +15,7 @@ public IEnumerator DoubleInitializeTest() { RawOverrideConfiguration(new Dissonity.I_UserData { DisableDissonityInfoLogs = true, - OauthScopes = new string[] { OauthScope.Identify, OauthScope.RpcActivitiesWrite, OauthScope.RpcVoiceRead, OauthScope.Guilds, OauthScope.GuildsMembersRead } + OauthScopes = new string[] { OauthScope.Identify, OauthScope.RpcActivitiesWrite, OauthScope.RpcVoiceRead, OauthScope.Guilds, OauthScope.GuildsMembersRead, OauthScope.RelationshipsRead } }); Task task = Initialize(); diff --git a/unity/Tests/Runtime/RpcCommands.cs b/unity/Tests/Runtime/RpcCommands.cs index 6b07442..b224ca1 100644 --- a/unity/Tests/Runtime/RpcCommands.cs +++ b/unity/Tests/Runtime/RpcCommands.cs @@ -14,54 +14,57 @@ public IEnumerator RpcCommandsTest() yield return new WaitUntil(() => captureLogTask.IsCompleted); var hwaTask = Commands.EncourageHardwareAcceleration(); - yield return new WaitUntil(() => hwaTask.IsCompleted); + yield return new WaitUntil(() => hwaTask.IsCompleted); var getChannelTask = Commands.GetChannel(0); - yield return new WaitUntil(() => getChannelTask.IsCompleted); + yield return new WaitUntil(() => getChannelTask.IsCompleted); var getChannelPermsTask = Commands.GetChannelPermissions(); - yield return new WaitUntil(() => getChannelPermsTask.IsCompleted); + yield return new WaitUntil(() => getChannelPermsTask.IsCompleted); var getEntTask = Commands.GetEntitlements(); - yield return new WaitUntil(() => getEntTask.IsCompleted); + yield return new WaitUntil(() => getEntTask.IsCompleted); var getParticipantsTask = Commands.GetInstanceConnectedParticipants(); - yield return new WaitUntil(() => getParticipantsTask.IsCompleted); + yield return new WaitUntil(() => getParticipantsTask.IsCompleted); var getPlatformBhTask = Commands.GetPlatformBehaviors(); - yield return new WaitUntil(() => getPlatformBhTask.IsCompleted); + yield return new WaitUntil(() => getPlatformBhTask.IsCompleted); var getSkusTask = Commands.GetSkus(); - yield return new WaitUntil(() => getSkusTask.IsCompleted); + yield return new WaitUntil(() => getSkusTask.IsCompleted); var initImgUpTask = Commands.InitiateImageUpload(); - yield return new WaitUntil(() => initImgUpTask.IsCompleted); + yield return new WaitUntil(() => initImgUpTask.IsCompleted); var openLinkTask = Commands.OpenExternalLink(""); - yield return new WaitUntil(() => openLinkTask.IsCompleted); + yield return new WaitUntil(() => openLinkTask.IsCompleted); var openInviteTask = Commands.OpenInviteDialog(); - yield return new WaitUntil(() => openInviteTask.IsCompleted); + yield return new WaitUntil(() => openInviteTask.IsCompleted); var openShareMomentTask = Commands.OpenShareMomentDialog(""); - yield return new WaitUntil(() => openShareMomentTask.IsCompleted); + yield return new WaitUntil(() => openShareMomentTask.IsCompleted); var setActTask = Commands.SetActivity(new ActivityBuilder()); - yield return new WaitUntil(() => setActTask.IsCompleted); + yield return new WaitUntil(() => setActTask.IsCompleted); var setConfigTask = Commands.SetConfig(true); - yield return new WaitUntil(() => setConfigTask.IsCompleted); + yield return new WaitUntil(() => setConfigTask.IsCompleted); var setOriLockStateTask = Commands.SetOrientationLockState(OrientationLockStateType.Landscape); - yield return new WaitUntil(() => setOriLockStateTask.IsCompleted); + yield return new WaitUntil(() => setOriLockStateTask.IsCompleted); var shareLinkTask = Commands.ShareLink(""); - yield return new WaitUntil(() => shareLinkTask.IsCompleted); + yield return new WaitUntil(() => shareLinkTask.IsCompleted); var startPurchaseTask = Commands.StartPurchase(0); - yield return new WaitUntil(() => startPurchaseTask.IsCompleted); + yield return new WaitUntil(() => startPurchaseTask.IsCompleted); var getLocaleTask = Commands.UserSettingsGetLocale(); - yield return new WaitUntil(() => getLocaleTask.IsCompleted); + yield return new WaitUntil(() => getLocaleTask.IsCompleted); + + var getRelationshipsTask = Commands.GetRelationships(); + yield return new WaitUntil(() => getRelationshipsTask.IsCompleted); } } diff --git a/unity/Tests/Runtime/RpcEvents.cs b/unity/Tests/Runtime/RpcEvents.cs index 640dce5..09711de 100644 --- a/unity/Tests/Runtime/RpcEvents.cs +++ b/unity/Tests/Runtime/RpcEvents.cs @@ -14,7 +14,8 @@ public IEnumerator RpcEventsTest() // ActivityInstanceParticipantsUpdate TaskCompletionSource ActivityInstanceParticipantsUpdateTask = new(); - _ = Subscribe.ActivityInstanceParticipantsUpdate((d) => { + _ = Subscribe.ActivityInstanceParticipantsUpdate((d) => + { ActivityInstanceParticipantsUpdateTask.SetResult(true); }); mock.ActivityInstanceParticipantsUpdate(); @@ -23,7 +24,8 @@ public IEnumerator RpcEventsTest() // ActivityLayoutModeUpdate TaskCompletionSource ActivityLayoutModeUpdateTask = new(); - _ = Subscribe.ActivityLayoutModeUpdate((d) => { + _ = Subscribe.ActivityLayoutModeUpdate((d) => + { ActivityLayoutModeUpdateTask.SetResult(true); }); mock.ActivityLayoutModeUpdate(); @@ -32,7 +34,8 @@ public IEnumerator RpcEventsTest() // CurrentGuildMemberUpdate TaskCompletionSource CurrentGuildMemberUpdateTask = new(); - _ = Subscribe.CurrentGuildMemberUpdate(0, (d) => { + _ = Subscribe.CurrentGuildMemberUpdate(0, (d) => + { CurrentGuildMemberUpdateTask.SetResult(true); }); mock.CurrentGuildMemberUpdate(); @@ -41,7 +44,8 @@ public IEnumerator RpcEventsTest() // CurrentUserUpdate TaskCompletionSource CurrentUserUpdateTask = new(); - _ = Subscribe.CurrentUserUpdate((d) => { + _ = Subscribe.CurrentUserUpdate((d) => + { CurrentUserUpdateTask.SetResult(true); }); mock.CurrentUserUpdate(); @@ -50,10 +54,12 @@ public IEnumerator RpcEventsTest() // EntitlementCreate TaskCompletionSource EntitlementCreateTask = new(); - _ = Subscribe.EntitlementCreate((d) => { + _ = Subscribe.EntitlementCreate((d) => + { EntitlementCreateTask.SetResult(true); }); - mock._entitlements.Add(new() { + mock._entitlements.Add(new() + { Id = 0 }); mock.EntitlementCreate(0); @@ -62,7 +68,8 @@ public IEnumerator RpcEventsTest() // OrientationUpdate TaskCompletionSource OrientationUpdateTask = new(); - _ = Subscribe.OrientationUpdate((d) => { + _ = Subscribe.OrientationUpdate((d) => + { OrientationUpdateTask.SetResult(true); }); mock.OrientationUpdate(); @@ -71,7 +78,8 @@ public IEnumerator RpcEventsTest() // SpeakingStart TaskCompletionSource SpeakingStartTask = new(); - _ = Subscribe.SpeakingStart(0, (d) => { + _ = Subscribe.SpeakingStart(0, (d) => + { SpeakingStartTask.SetResult(true); }); mock.SpeakingStart(); @@ -80,7 +88,8 @@ public IEnumerator RpcEventsTest() // SpeakingStop TaskCompletionSource SpeakingStopTask = new(); - _ = Subscribe.SpeakingStop(0, (d) => { + _ = Subscribe.SpeakingStop(0, (d) => + { SpeakingStopTask.SetResult(true); }); mock.SpeakingStop(); @@ -89,18 +98,31 @@ public IEnumerator RpcEventsTest() // ThermalStateUpdate TaskCompletionSource ThermalStateUpdateTask = new(); - _ = Subscribe.ThermalStateUpdate((d) => { + _ = Subscribe.ThermalStateUpdate((d) => + { ThermalStateUpdateTask.SetResult(true); }); mock.ThermalStateUpdate(); yield return new WaitUntil(() => ThermalStateUpdateTask.Task.IsCompleted); + // VoiceStateUpdate TaskCompletionSource VoiceStateUpdateTask = new(); - _ = Subscribe.VoiceStateUpdate(0, (d) => { + _ = Subscribe.VoiceStateUpdate(0, (d) => + { VoiceStateUpdateTask.SetResult(true); }); mock.VoiceStateUpdate(); yield return new WaitUntil(() => VoiceStateUpdateTask.Task.IsCompleted); + + + // RelationshipUpdate + TaskCompletionSource RelationshipUpdateTask = new(); + _ = Subscribe.RelationshipUpdate((d) => + { + RelationshipUpdateTask.SetResult(true); + }); + mock.RelationshipUpdate(); + yield return new WaitUntil(() => RelationshipUpdateTask.Task.IsCompleted); } } diff --git a/unity/package.json b/unity/package.json index 03a4a7b..0c73763 100644 --- a/unity/package.json +++ b/unity/package.json @@ -1,6 +1,6 @@ { "name": "com.furnyr.dissonity", - "version": "2.0.1", + "version": "2.1.0", "unity": "2021.3", "displayName": "Dissonity", "description": "Interact with the Discord client inside a Unity activity.", diff --git a/website/src/main.tsx b/website/src/main.tsx index a23d938..7002e5f 100644 --- a/website/src/main.tsx +++ b/website/src/main.tsx @@ -26,6 +26,7 @@ import GettingStarted from "./routes/guides/getting-started.tsx"; import HowDoesItWork from "./routes/guides/how-does-it-work.tsx"; import WhyDissonity from "./routes/guides/why-dissonity.tsx"; import MigrationV2 from "./routes/guides/migration-v2.tsx"; +import ThirdPartySupport from "./routes/guides/third-party-support.tsx"; import DocsIndex from "./routes/docs/docsIndex.tsx"; import Performance from "./routes/docs/development/performance.tsx"; @@ -115,9 +116,6 @@ const router = createBrowserRouter([ { path: "/docs/v2/api/utils", element: - },{ - path: "/docs/v2/api/configuration", - element: }, { path: "/docs/v2/api/exceptions", @@ -197,6 +195,10 @@ const router = createBrowserRouter([ path: "/guides/v2/migration-v2", element: }, + { + path: "/guides/v2/third-party-support", + element: + } ] }, { diff --git a/website/src/routes/docs/api/configuration.tsx b/website/src/routes/docs/api/configuration.tsx index 4e9bed3..84fd1a4 100644 --- a/website/src/routes/docs/api/configuration.tsx +++ b/website/src/routes/docs/api/configuration.tsx @@ -161,6 +161,12 @@ public override PatchUrlMappingsConfigBuilder PatchUrlMappingsConfig => new() Example URL mappings configuration. +

Lazy hiRPC Load

+ +

+ By default, Dissonity begins authentication before loading the Unity build. If you need to check whether authentication is necessary when calling Initialize rather than before running the C# code, set LazyHiRpcLoad to true. +

+

External links

    diff --git a/website/src/routes/docs/internals/hirpc.tsx b/website/src/routes/docs/internals/hirpc.tsx index 80a0835..5d65e0a 100644 --- a/website/src/routes/docs/internals/hirpc.tsx +++ b/website/src/routes/docs/internals/hirpc.tsx @@ -290,6 +290,13 @@ end`}/> {` +