├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE-2.0 ├── LICENSE-MIT ├── README.md ├── discord_game_sdk ├── Cargo.toml ├── README.md ├── README.tpl └── src │ ├── action.rs │ ├── activity.rs │ ├── activity_kind.rs │ ├── aliases.rs │ ├── cast.rs │ ├── comparison.rs │ ├── create_flags.rs │ ├── discord.rs │ ├── distance.rs │ ├── entitlement.rs │ ├── entitlement_kind.rs │ ├── error.rs │ ├── event_handler.rs │ ├── events.rs │ ├── fetch_kind.rs │ ├── file_stat.rs │ ├── image.rs │ ├── image_handle.rs │ ├── image_kind.rs │ ├── input_mode.rs │ ├── input_mode_kind.rs │ ├── iter.rs │ ├── lib.rs │ ├── lobby.rs │ ├── lobby_kind.rs │ ├── lobby_member_transaction.rs │ ├── lobby_transaction.rs │ ├── methods │ ├── achievements.rs │ ├── activities.rs │ ├── applications.rs │ ├── callback.rs │ ├── core.rs │ ├── images.rs │ ├── lobbies.rs │ ├── networking.rs │ ├── overlay.rs │ ├── relationships.rs │ ├── storage.rs │ ├── store.rs │ ├── users.rs │ └── voice.rs │ ├── mock │ ├── ffi.rs │ └── mod.rs │ ├── oauth2_token.rs │ ├── premium_kind.rs │ ├── presence.rs │ ├── relationship.rs │ ├── relationship_kind.rs │ ├── reliability.rs │ ├── request_reply.rs │ ├── search_query.rs │ ├── sku.rs │ ├── sku_kind.rs │ ├── status.rs │ ├── to_result.rs │ ├── user.rs │ ├── user_achievement.rs │ ├── user_flags.rs │ └── utils.rs └── discord_game_sdk_sys ├── Cargo.toml ├── README.md ├── README.tpl ├── build.rs ├── discord_game_sdk.h └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.rs.bk 2 | .envrc 3 | /target 4 | Cargo.lock 5 | discord_game_sdk.dll 6 | sdk/ 7 | shell.nix 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "discord_game_sdk", 4 | "discord_game_sdk_sys", 5 | ] 6 | -------------------------------------------------------------------------------- /LICENSE-APACHE-2.0: -------------------------------------------------------------------------------- 1 | Version 2.0, January 2004 2 | http://www.apache.org/licenses/ 3 | 4 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 5 | 6 | 1. Definitions. 7 | 8 | "License" shall mean the terms and conditions for use, reproduction, 9 | and distribution as defined by Sections 1 through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by 12 | the copyright owner that is granting the License. 13 | 14 | "Legal Entity" shall mean the union of the acting entity and all 15 | other entities that control, are controlled by, or are under common 16 | control with that entity. For the purposes of this definition, 17 | "control" means (i) the power, direct or indirect, to cause the 18 | direction or management of such entity, whether by contract or 19 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity 23 | exercising permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, 26 | including but not limited to software source code, documentation 27 | source, and configuration files. 28 | 29 | "Object" form shall mean any form resulting from mechanical 30 | transformation or translation of a Source form, including but 31 | not limited to compiled object code, generated documentation, 32 | and conversions to other media types. 33 | 34 | "Work" shall mean the work of authorship, whether in Source or 35 | Object form, made available under the License, as indicated by a 36 | copyright notice that is included in or attached to the work 37 | (an example is provided in the Appendix below). 38 | 39 | "Derivative Works" shall mean any work, whether in Source or Object 40 | form, that is based on (or derived from) the Work and for which the 41 | editorial revisions, annotations, elaborations, or other modifications 42 | represent, as a whole, an original work of authorship. For the purposes 43 | of this License, Derivative Works shall not include works that remain 44 | separable from, or merely link (or bind by name) to the interfaces of, 45 | the Work and Derivative Works thereof. 46 | 47 | "Contribution" shall mean any work of authorship, including 48 | the original version of the Work and any modifications or additions 49 | to that Work or Derivative Works thereof, that is intentionally 50 | submitted to Licensor for inclusion in the Work by the copyright owner 51 | or by an individual or Legal Entity authorized to submit on behalf of 52 | the copyright owner. For the purposes of this definition, "submitted" 53 | means any form of electronic, verbal, or written communication sent 54 | to the Licensor or its representatives, including but not limited to 55 | communication on electronic mailing lists, source code control systems, 56 | and issue tracking systems that are managed by, or on behalf of, the 57 | Licensor for the purpose of discussing and improving the Work, but 58 | excluding communication that is conspicuously marked or otherwise 59 | designated in writing by the copyright owner as "Not a Contribution." 60 | 61 | "Contributor" shall mean Licensor and any individual or Legal Entity 62 | on behalf of whom a Contribution has been received by Licensor and 63 | subsequently incorporated within the Work. 64 | 65 | 2. Grant of Copyright License. Subject to the terms and conditions of 66 | this License, each Contributor hereby grants to You a perpetual, 67 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 68 | copyright license to reproduce, prepare Derivative Works of, 69 | publicly display, publicly perform, sublicense, and distribute the 70 | Work and such Derivative Works in Source or Object form. 71 | 72 | 3. Grant of Patent License. Subject to the terms and conditions of 73 | this License, each Contributor hereby grants to You a perpetual, 74 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 75 | (except as stated in this section) patent license to make, have made, 76 | use, offer to sell, sell, import, and otherwise transfer the Work, 77 | where such license applies only to those patent claims licensable 78 | by such Contributor that are necessarily infringed by their 79 | Contribution(s) alone or by combination of their Contribution(s) 80 | with the Work to which such Contribution(s) was submitted. If You 81 | institute patent litigation against any entity (including a 82 | cross-claim or counterclaim in a lawsuit) alleging that the Work 83 | or a Contribution incorporated within the Work constitutes direct 84 | or contributory patent infringement, then any patent licenses 85 | granted to You under this License for that Work shall terminate 86 | as of the date such litigation is filed. 87 | 88 | 4. Redistribution. You may reproduce and distribute copies of the 89 | Work or Derivative Works thereof in any medium, with or without 90 | modifications, and in Source or Object form, provided that You 91 | meet the following conditions: 92 | 93 | (a) You must give any other recipients of the Work or 94 | Derivative Works a copy of this License; and 95 | 96 | (b) You must cause any modified files to carry prominent notices 97 | stating that You changed the files; and 98 | 99 | (c) You must retain, in the Source form of any Derivative Works 100 | that You distribute, all copyright, patent, trademark, and 101 | attribution notices from the Source form of the Work, 102 | excluding those notices that do not pertain to any part of 103 | the Derivative Works; and 104 | 105 | (d) If the Work includes a "NOTICE" text file as part of its 106 | distribution, then any Derivative Works that You distribute must 107 | include a readable copy of the attribution notices contained 108 | within such NOTICE file, excluding those notices that do not 109 | pertain to any part of the Derivative Works, in at least one 110 | of the following places: within a NOTICE text file distributed 111 | as part of the Derivative Works; within the Source form or 112 | documentation, if provided along with the Derivative Works; or, 113 | within a display generated by the Derivative Works, if and 114 | wherever such third-party notices normally appear. The contents 115 | of the NOTICE file are for informational purposes only and 116 | do not modify the License. You may add Your own attribution 117 | notices within Derivative Works that You distribute, alongside 118 | or as an addendum to the NOTICE text from the Work, provided 119 | that such additional attribution notices cannot be construed 120 | as modifying the License. 121 | 122 | You may add Your own copyright statement to Your modifications and 123 | may provide additional or different license terms and conditions 124 | for use, reproduction, or distribution of Your modifications, or 125 | for any such Derivative Works as a whole, provided Your use, 126 | reproduction, and distribution of the Work otherwise complies with 127 | the conditions stated in this License. 128 | 129 | 5. Submission of Contributions. Unless You explicitly state otherwise, 130 | any Contribution intentionally submitted for inclusion in the Work 131 | by You to the Licensor shall be under the terms and conditions of 132 | this License, without any additional terms or conditions. 133 | Notwithstanding the above, nothing herein shall supersede or modify 134 | the terms of any separate license agreement you may have executed 135 | with Licensor regarding such Contributions. 136 | 137 | 6. Trademarks. This License does not grant permission to use the trade 138 | names, trademarks, service marks, or product names of the Licensor, 139 | except as required for reasonable and customary use in describing the 140 | origin of the Work and reproducing the content of the NOTICE file. 141 | 142 | 7. Disclaimer of Warranty. Unless required by applicable law or 143 | agreed to in writing, Licensor provides the Work (and each 144 | Contributor provides its Contributions) on an "AS IS" BASIS, 145 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 146 | implied, including, without limitation, any warranties or conditions 147 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 148 | PARTICULAR PURPOSE. You are solely responsible for determining the 149 | appropriateness of using or redistributing the Work and assume any 150 | risks associated with Your exercise of permissions under this License. 151 | 152 | 8. Limitation of Liability. In no event and under no legal theory, 153 | whether in tort (including negligence), contract, or otherwise, 154 | unless required by applicable law (such as deliberate and grossly 155 | negligent acts) or agreed to in writing, shall any Contributor be 156 | liable to You for damages, including any direct, indirect, special, 157 | incidental, or consequential damages of any character arising as a 158 | result of this License or out of the use or inability to use the 159 | Work (including but not limited to damages for loss of goodwill, 160 | work stoppage, computer failure or malfunction, or any and all 161 | other commercial damages or losses), even if such Contributor 162 | has been advised of the possibility of such damages. 163 | 164 | 9. Accepting Warranty or Additional Liability. While redistributing 165 | the Work or Derivative Works thereof, You may choose to offer, 166 | and charge a fee for, acceptance of support, warranty, indemnity, 167 | or other liability obligations and/or rights consistent with this 168 | License. However, in accepting such obligations, You may act only 169 | on Your own behalf and on Your sole responsibility, not on behalf 170 | of any other Contributor, and only if You agree to indemnify, 171 | defend, and hold each Contributor harmless for any liability 172 | incurred by, or claims asserted against, such Contributor by reason 173 | of your accepting any such warranty or additional liability. 174 | 175 | END OF TERMS AND CONDITIONS 176 | 177 | APPENDIX: How to apply the Apache License to your work. 178 | 179 | To apply the Apache License to your work, attach the following 180 | boilerplate notice, with the fields enclosed by brackets "[]" 181 | replaced with your own identifying information. (Don't include 182 | the brackets!) The text should be enclosed in the appropriate 183 | comment syntax for the file format. We also recommend that a 184 | file or class name and description of purpose be included on the 185 | same "printed page" as the copyright notice for easier 186 | identification within third-party archives. 187 | 188 | Copyright [yyyy] [name of copyright owner] 189 | 190 | Licensed under the Apache License, Version 2.0 (the "License"); 191 | you may not use this file except in compliance with the License. 192 | You may obtain a copy of the License at 193 | 194 | http://www.apache.org/licenses/LICENSE-2.0 195 | 196 | Unless required by applicable law or agreed to in writing, software 197 | distributed under the License is distributed on an "AS IS" BASIS, 198 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 199 | See the License for the specific language governing permissions and 200 | limitations under the License. 201 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Lucas Desgouilles 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | discord_game_sdk/README.md -------------------------------------------------------------------------------- /discord_game_sdk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "discord_game_sdk" 3 | version = "1.0.1" # check src/lib.rs 4 | authors = ["ldesgoui "] 5 | edition = "2018" 6 | description = "Safe wrapper for the Discord Game SDK" 7 | license = "Apache-2.0 OR MIT" 8 | repository = "https://github.com/ldesgoui/discord_game_sdk" 9 | keywords = ["discord", "sdk", "gamedev"] 10 | categories = ["api-bindings", "game-engines"] 11 | readme = "README.md" 12 | 13 | [package.metadata.docs.rs] 14 | features = ["private-docs-rs"] 15 | no-default-features = true 16 | 17 | [dependencies] 18 | bitflags = "1.2" 19 | discord_game_sdk_sys = { path = "../discord_game_sdk_sys", version = "1.0.0" } 20 | log = "0.4" 21 | memchr = "2.2" 22 | image = { version = "0.23", default-features = false, optional = true } 23 | 24 | [dev-dependencies] 25 | pretty_env_logger = "0.4" 26 | 27 | [features] 28 | default = ["link"] 29 | link = ["discord_game_sdk_sys/link"] 30 | private-docs-rs = ["discord_game_sdk_sys/private-docs-rs"] # DO NOT RELY ON THIS 31 | -------------------------------------------------------------------------------- /discord_game_sdk/README.md: -------------------------------------------------------------------------------- 1 | # discord_game_sdk 2 | 3 | [![Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/discord_game_sdk) 4 | [![Latest Version](https://img.shields.io/crates/v/discord_game_sdk.svg)](https://crates.io/crates/discord_game_sdk) 5 | ![License](https://img.shields.io/crates/l/discord_game_sdk) 6 | [![Build Status](https://img.shields.io/github/workflow/status/ldesgoui/discord_game_sdk/Continuous%20Integration)](https://github.com/ldesgoui/discord_game_sdk/actions) 7 | 8 | This crate provides a safe interface to the [Discord Game SDK]. 9 | 10 | *This crate is not official, it is not supported by the Discord Game SDK Developers.* 11 | 12 | The [Discord Game SDK] provides features such as, but not limited to: 13 | 14 | - Activities (Rich Presence) 15 | - Users, Avatars and Relationships 16 | - Lobbies, Matchmaking and Voice communication 17 | - Faux-P2P Networking on Discord's Infrastructure 18 | - Cloud Synchronized Storage 19 | - Store Transactions 20 | - Achievements 21 | 22 | *Version requirement: Rust 1.47 and up.* 23 | 24 | *[Release Notes](https://github.com/ldesgoui/discord_game_sdk/releases)* 25 | 26 | 27 | ## Usage 28 | 29 | Add this to your `Cargo.toml`: 30 | 31 | ```toml 32 | [dependencies] 33 | discord_game_sdk = "1.0.1" 34 | ``` 35 | 36 | Read up on potential [`bindgen` requirements]. 37 | 38 | Download the [Discord Game SDK] and set the following environment variable to where you extracted it: 39 | 40 | ```sh 41 | export DISCORD_GAME_SDK_PATH=/path/to/discord_game_sdk 42 | ``` 43 | 44 | If you're also planning on using the default `link` feature, keep reading below. 45 | 46 | 47 | ## Features: 48 | 49 | #### `link` 50 | 51 | Enabled by default, delegates to `discord_game_sdk_sys/link`. 52 | 53 | Provides functional linking with the caveat that libraries are renamed and some additional 54 | set-up is required: 55 | 56 | ```sh 57 | # Linux: prepend with `lib` and add to library search path 58 | cp $DISCORD_GAME_SDK_PATH/lib/x86_64/{,lib}discord_game_sdk.so 59 | export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}$DISCORD_GAME_SDK_PATH/lib/x86_64 60 | 61 | # Mac OS: prepend with `lib` and add to library search path 62 | cp $DISCORD_GAME_SDK_PATH/lib/x86_64/{,lib}discord_game_sdk.dylib 63 | export DYLD_LIBRARY_PATH=${DYLD_LIBRARY_PATH:+${DYLD_LIBRARY_PATH}:}$DISCORD_GAME_SDK_PATH/lib/x86_64 64 | 65 | # Windows: change `dll.lib` to `lib` (won't affect library searching) 66 | cp $DISCORD_GAME_SDK_PATH/lib/x86_64/discord_game_sdk.{dll.lib,lib} 67 | cp $DISCORD_GAME_SDK_PATH/lib/x86/discord_game_sdk.{dll.lib,lib} 68 | ``` 69 | 70 | This allows for `cargo run` to function. 71 | 72 | 73 | #### [`image`](https://docs.rs/image) 74 | 75 | Optional crate. 76 | 77 | Provides a conversion from our `Image` to `image::RgbaImage`. 78 | 79 | 80 | ## Safety 81 | 82 | This crate relies on the SDK to provide correct data and behavior: 83 | 84 | - Non-null pointers to valid memory 85 | - UTF-8, NUL-terminated strings 86 | - No mutation of memory it should have no ownership of 87 | - No use of pointers after `destroy` is called 88 | 89 | Some of these are tested when compiled with `debug_assertions`. 90 | 91 | 92 | ## Legal 93 | 94 | You *MUST* acquaint yourself with and agree to the [official terms of the Discord Game SDK]. 95 | 96 | The code of the Rust crates `discord_game_sdk` and `discord_game_sdk_sys` 97 | are licensed at your option under either of: 98 | 99 | * [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) 100 | * [MIT License](https://opensource.org/licenses/MIT) 101 | 102 | Unless you explicitly state otherwise, any contribution intentionally 103 | submitted for inclusion in the work by you, as defined in the Apache-2.0 104 | license, shall be dual licensed as above, without any additional terms or 105 | conditions. 106 | 107 | 108 | [Discord Game SDK]: https://discordapp.com/developers/docs/game-sdk/sdk-starter-guide 109 | [`bindgen` requirements]: https://rust-lang.github.io/rust-bindgen/requirements.html 110 | [official terms of the Discord Game SDK]: https://discordapp.com/developers/docs/legal 111 | -------------------------------------------------------------------------------- /discord_game_sdk/README.tpl: -------------------------------------------------------------------------------- 1 | # {{crate}} 2 | 3 | [![Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/{{crate}}) 4 | [![Latest Version](https://img.shields.io/crates/v/{{crate}}.svg)](https://crates.io/crates/{{crate}}) 5 | ![License](https://img.shields.io/crates/l/{{crate}}) 6 | [![Build Status](https://img.shields.io/github/workflow/status/ldesgoui/discord_game_sdk/Continuous%20Integration)](https://github.com/ldesgoui/discord_game_sdk/actions) 7 | 8 | {{readme}} 9 | -------------------------------------------------------------------------------- /discord_game_sdk/src/action.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Activity Action 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/activities#data-models-activityactiontype-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum Action { 8 | /// Invite to join a game 9 | Join, 10 | /// Invite to spectate a game 11 | Spectate, 12 | /// Safety net for missing definitions 13 | Undefined(sys::EDiscordActivityActionType), 14 | } 15 | 16 | impl From for Action { 17 | fn from(source: sys::EDiscordActivityActionType) -> Self { 18 | match source { 19 | sys::DiscordActivityActionType_Join => Self::Join, 20 | sys::DiscordActivityActionType_Spectate => Self::Spectate, 21 | _ => Self::Undefined(source), 22 | } 23 | } 24 | } 25 | 26 | impl Into for Action { 27 | fn into(self) -> sys::EDiscordActivityActionType { 28 | match self { 29 | Self::Join => sys::DiscordActivityActionType_Join, 30 | Self::Spectate => sys::DiscordActivityActionType_Spectate, 31 | Self::Undefined(n) => n, 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /discord_game_sdk/src/activity.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | sys, 3 | utils::{charbuf_to_str, write_charbuf}, 4 | ActivityKind, ClientID, UnixTimestamp, 5 | }; 6 | use std::convert::TryInto; 7 | 8 | /// Activity (also known as Rich Presence) 9 | /// 10 | /// To enable players to join or spectate via invites and requests, some fields are required: 11 | /// - Joining 12 | /// - [`with_party_id`](#method.with_party_id) 13 | /// - [`with_party_amount`](#method.with_party_amount) 14 | /// - [`with_party_capacity`](#method.with_party_capacity) 15 | /// - [`with_join_secret`](#method.with_join_secret) 16 | /// - Spectating 17 | /// - [`with_spectate_secret`](#method.with_spectate_secret) 18 | /// 19 | /// > [Struct in official docs](https://discordapp.com/developers/docs/game-sdk/activities#data-models-activity-struct) 20 | /// 21 | /// ```rust 22 | /// # use discord_game_sdk::*; 23 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 24 | /// # let now = 0; 25 | /// discord.update_activity( 26 | /// &Activity::empty() 27 | /// .with_state("On Main Menu") 28 | /// .with_start_time(now), 29 | /// |discord, result| { 30 | /// if let Err(error) = result { 31 | /// eprintln!("failed to update activity: {}", error); 32 | /// } 33 | /// }, 34 | /// ); 35 | /// # Ok(()) } 36 | /// ``` 37 | #[derive(Clone, Eq, PartialEq)] 38 | #[repr(transparent)] 39 | pub struct Activity(pub(crate) sys::DiscordActivity); 40 | 41 | impl Activity { 42 | /// Create a new Activity with empty fields 43 | pub fn empty() -> Self { 44 | Self(sys::DiscordActivity::default()) 45 | } 46 | 47 | /// Check if an Activity is completely blank 48 | pub fn is_empty(&self) -> bool { 49 | self == &Self::empty() 50 | } 51 | 52 | /// Type of Activty 53 | pub fn kind(&self) -> ActivityKind { 54 | self.0.type_.into() 55 | } 56 | 57 | /// The unique ID of the application 58 | pub fn application_id(&self) -> ClientID { 59 | self.0.application_id 60 | } 61 | 62 | /// The name of the application 63 | pub fn name(&self) -> &str { 64 | charbuf_to_str(&self.0.name) 65 | } 66 | 67 | /// The player's current party status 68 | pub fn state(&self) -> &str { 69 | charbuf_to_str(&self.0.state) 70 | } 71 | 72 | /// What the player is currently doing 73 | pub fn details(&self) -> &str { 74 | charbuf_to_str(&self.0.details) 75 | } 76 | 77 | /// When the current activity has started, in UNIX Time 78 | pub fn start_time(&self) -> UnixTimestamp { 79 | self.0.timestamps.start 80 | } 81 | 82 | /// When the current activity will end, in UNIX Time 83 | pub fn end_time(&self) -> UnixTimestamp { 84 | self.0.timestamps.end 85 | } 86 | 87 | /// The key of an asset to display 88 | pub fn large_image_key(&self) -> &str { 89 | charbuf_to_str(&self.0.assets.large_image) 90 | } 91 | 92 | /// The tooltip displayed when hovering over the large image 93 | pub fn large_image_tooltip(&self) -> &str { 94 | charbuf_to_str(&self.0.assets.large_text) 95 | } 96 | 97 | /// The key of an asset to display 98 | pub fn small_image_key(&self) -> &str { 99 | charbuf_to_str(&self.0.assets.small_image) 100 | } 101 | 102 | /// The tooltip displayed when hovering over the small image 103 | pub fn small_image_tooltip(&self) -> &str { 104 | charbuf_to_str(&self.0.assets.small_text) 105 | } 106 | 107 | /// The unique identifier for the party 108 | pub fn party_id(&self) -> &str { 109 | charbuf_to_str(&self.0.party.id) 110 | } 111 | 112 | /// The number of players currently in the party 113 | pub fn party_amount(&self) -> u32 { 114 | // XXX: i32 should be u32 115 | self.0.party.size.current_size.try_into().unwrap() 116 | } 117 | 118 | /// The maximum capacity of the party 119 | pub fn party_capacity(&self) -> u32 { 120 | // XXX: i32 should be u32 121 | self.0.party.size.max_size.try_into().unwrap() 122 | } 123 | 124 | /// Whether this activity is an instanced context, like a match 125 | pub fn instance(&self) -> bool { 126 | self.0.instance 127 | } 128 | 129 | /// The unique hash for the given match context 130 | pub fn match_secret(&self) -> &str { 131 | charbuf_to_str(&self.0.secrets.match_) 132 | } 133 | 134 | /// The unique hash for chat invites and Ask to Join 135 | pub fn join_secret(&self) -> &str { 136 | charbuf_to_str(&self.0.secrets.join) 137 | } 138 | 139 | /// The unique hash for Spectate button 140 | pub fn spectate_secret(&self) -> &str { 141 | charbuf_to_str(&self.0.secrets.spectate) 142 | } 143 | 144 | /// The player's current party status 145 | /// 146 | /// Only the first 128 bytes will be written. 147 | pub fn with_state(&mut self, value: &str) -> &mut Self { 148 | write_charbuf(&mut self.0.state, value); 149 | self 150 | } 151 | 152 | /// What the player is currently doing 153 | /// 154 | /// Only the first 128 bytes will be written. 155 | pub fn with_details(&mut self, value: &str) -> &mut Self { 156 | write_charbuf(&mut self.0.details, value); 157 | self 158 | } 159 | 160 | /// When the current activity has started, in UNIX time 161 | pub fn with_start_time(&mut self, value: UnixTimestamp) -> &mut Self { 162 | self.0.timestamps.start = value; 163 | self 164 | } 165 | 166 | /// When the current activity will end, in UNIX time 167 | pub fn with_end_time(&mut self, value: UnixTimestamp) -> &mut Self { 168 | self.0.timestamps.end = value; 169 | self 170 | } 171 | 172 | /// The key of an asset to display 173 | /// 174 | /// Only the first 128 bytes will be written. 175 | pub fn with_large_image_key(&mut self, value: &str) -> &mut Self { 176 | write_charbuf(&mut self.0.assets.large_image, value); 177 | self 178 | } 179 | 180 | /// The tooltip displayed when hovering over the large image 181 | /// 182 | /// Only the first 128 bytes will be written. 183 | pub fn with_large_image_tooltip(&mut self, value: &str) -> &mut Self { 184 | write_charbuf(&mut self.0.assets.large_text, value); 185 | self 186 | } 187 | 188 | /// The key of an asset to display 189 | /// 190 | /// Only the first 128 bytes will be written. 191 | pub fn with_small_image_key(&mut self, value: &str) -> &mut Self { 192 | write_charbuf(&mut self.0.assets.small_image, value); 193 | self 194 | } 195 | 196 | /// The tooltip displayed when hovering over the small image 197 | /// 198 | /// Only the first 128 bytes will be written. 199 | pub fn with_small_image_tooltip(&mut self, value: &str) -> &mut Self { 200 | write_charbuf(&mut self.0.assets.small_text, value); 201 | self 202 | } 203 | 204 | /// The unique identifier for the party 205 | /// 206 | /// Only the first 128 bytes will be written. 207 | pub fn with_party_id(&mut self, value: &str) -> &mut Self { 208 | write_charbuf(&mut self.0.party.id, value); 209 | self 210 | } 211 | 212 | /// The number of players currently in the party 213 | pub fn with_party_amount(&mut self, value: u32) -> &mut Self { 214 | // XXX: i32 should be u32 215 | self.0.party.size.current_size = value.try_into().unwrap(); 216 | self 217 | } 218 | 219 | /// The maximum capacity of the party 220 | pub fn with_party_capacity(&mut self, value: u32) -> &mut Self { 221 | // XXX: i32 should be u32 222 | self.0.party.size.max_size = value.try_into().unwrap(); 223 | self 224 | } 225 | 226 | /// Whether this activity is an instanced context, like a match 227 | pub fn with_instance(&mut self, value: bool) -> &mut Self { 228 | self.0.instance = value; 229 | self 230 | } 231 | 232 | /// The unique hash for the given match context 233 | /// 234 | /// Only the first 128 bytes will be written. 235 | pub fn with_match_secret(&mut self, value: &str) -> &mut Self { 236 | write_charbuf(&mut self.0.secrets.match_, value); 237 | self 238 | } 239 | 240 | /// The unique hash for chat invites and Ask to Join 241 | /// 242 | /// Only the first 128 bytes will be written. 243 | pub fn with_join_secret(&mut self, value: &str) -> &mut Self { 244 | write_charbuf(&mut self.0.secrets.join, value); 245 | self 246 | } 247 | 248 | /// The unique hash for Spectate button 249 | /// 250 | /// Only the first 128 bytes will be written. 251 | pub fn with_spectate_secret(&mut self, value: &str) -> &mut Self { 252 | write_charbuf(&mut self.0.secrets.spectate, value); 253 | self 254 | } 255 | } 256 | 257 | impl std::fmt::Debug for Activity { 258 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 259 | fmt.debug_struct("Activity") 260 | .field("kind", &self.kind()) 261 | .field("application_id", &self.application_id()) 262 | .field("name", &self.name()) 263 | .field("state", &self.state()) 264 | .field("details", &self.details()) 265 | .field("start_time", &self.start_time()) 266 | .field("end_time", &self.end_time()) 267 | .field("large_image_key", &self.large_image_key()) 268 | .field("large_image_tooltip", &self.large_image_tooltip()) 269 | .field("small_image_key", &self.small_image_key()) 270 | .field("small_image_tooltip", &self.small_image_tooltip()) 271 | .field("party_id", &self.party_id()) 272 | .field("party_amount", &self.party_amount()) 273 | .field("party_capacity", &self.party_capacity()) 274 | .field("instance", &self.instance()) 275 | .field("match_secret", &self.match_secret()) 276 | .field("join_secret", &self.join_secret()) 277 | .field("spectate_secret", &self.spectate_secret()) 278 | .finish() 279 | } 280 | } 281 | 282 | impl std::fmt::Display for Activity { 283 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 284 | if self.is_empty() { 285 | return write!(fmt, "no activity"); 286 | } 287 | 288 | match self.kind() { 289 | ActivityKind::Listening => write!( 290 | fmt, 291 | "listening to {}: {} by {}", 292 | self.name(), 293 | self.details(), 294 | self.state() 295 | ), 296 | 297 | ActivityKind::Playing => write!(fmt, "playing {}", self.name()), 298 | 299 | ActivityKind::Streaming => { 300 | write!(fmt, "streaming on {}: {}", self.name(), self.state()) 301 | } 302 | 303 | ActivityKind::Watching => write!(fmt, "watching a live stream"), 304 | 305 | ActivityKind::Undefined(n) => write!(fmt, "undefined activity ({})", n), 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /discord_game_sdk/src/activity_kind.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Activity Type 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/activities#data-models-activitytype-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum ActivityKind { 8 | /// Listening to music (only Spotify as of Jan 2020) 9 | Listening, 10 | /// Playing a game 11 | Playing, 12 | /// Live streaming (only Twitch as of Jan 2020) 13 | Streaming, 14 | /// Watching a live stream 15 | Watching, 16 | /// Safety net for missing definitions 17 | Undefined(sys::EDiscordActivityType), 18 | } 19 | 20 | impl From for ActivityKind { 21 | fn from(source: sys::EDiscordActivityType) -> Self { 22 | match source { 23 | sys::DiscordActivityType_Listening => Self::Listening, 24 | sys::DiscordActivityType_Playing => Self::Playing, 25 | sys::DiscordActivityType_Streaming => Self::Streaming, 26 | sys::DiscordActivityType_Watching => Self::Watching, 27 | _ => Self::Undefined(source), 28 | } 29 | } 30 | } 31 | 32 | impl Into for ActivityKind { 33 | fn into(self) -> sys::EDiscordActivityType { 34 | match self { 35 | Self::Listening => sys::DiscordActivityType_Listening, 36 | Self::Playing => sys::DiscordActivityType_Playing, 37 | Self::Streaming => sys::DiscordActivityType_Streaming, 38 | Self::Watching => sys::DiscordActivityType_Watching, 39 | Self::Undefined(n) => n, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /discord_game_sdk/src/aliases.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Client ID of an Application 4 | pub type ClientID = sys::DiscordClientId; 5 | 6 | /// Unique ID of a Lobby 7 | pub type LobbyID = sys::DiscordLobbyId; 8 | 9 | /// ID of a Network Channel 10 | pub type NetworkChannelID = sys::DiscordNetworkChannelId; 11 | 12 | /// ID of a Network Peer 13 | pub type NetworkPeerID = sys::DiscordNetworkPeerId; 14 | 15 | /// Unique ID across Discord 16 | /// 17 | /// > [Snowflakes in official docs](https://discordapp.com/developers/docs/reference#snowflakes) 18 | pub type Snowflake = sys::DiscordSnowflake; 19 | 20 | /// UNIX Timestamp, number of seconds since 1 January 1970 21 | pub type UnixTimestamp = sys::DiscordTimestamp; 22 | 23 | /// ID of a User 24 | pub type UserID = sys::DiscordUserId; 25 | -------------------------------------------------------------------------------- /discord_game_sdk/src/cast.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Lobby Search Cast 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#data-models-lobbysearchcast-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum Cast { 8 | /// Cast the value as a number 9 | Number, 10 | /// Cast the value as a number 11 | String, 12 | } 13 | 14 | impl Into for Cast { 15 | fn into(self) -> sys::EDiscordLobbySearchCast { 16 | match self { 17 | Self::String => sys::DiscordLobbySearchCast_String, 18 | Self::Number => sys::DiscordLobbySearchCast_Number, 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /discord_game_sdk/src/comparison.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Lobby Search Comparison 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#data-models-lobbysearchcomparison-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum Comparison { 8 | /// Metadata must be equal to the search value 9 | Equal, 10 | /// Metadata must be greater than the search value 11 | GreaterThan, 12 | /// Metadata must be greater than or equal to the search value 13 | GreaterThanOrEqual, 14 | /// Metadata must be less than the search value 15 | LessThan, 16 | /// Metadata must be less than or equal to the search value 17 | LessThanOrEqual, 18 | /// Metadata must not be equal to the search value 19 | NotEqual, 20 | } 21 | 22 | impl Into for Comparison { 23 | fn into(self) -> sys::EDiscordLobbySearchComparison { 24 | match self { 25 | Self::Equal => sys::DiscordLobbySearchComparison_Equal, 26 | Self::GreaterThan => sys::DiscordLobbySearchComparison_GreaterThan, 27 | Self::GreaterThanOrEqual => sys::DiscordLobbySearchComparison_GreaterThanOrEqual, 28 | Self::LessThan => sys::DiscordLobbySearchComparison_LessThan, 29 | Self::LessThanOrEqual => sys::DiscordLobbySearchComparison_LessThanOrEqual, 30 | Self::NotEqual => sys::DiscordLobbySearchComparison_NotEqual, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /discord_game_sdk/src/create_flags.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Discord Creation Flags 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/discord#data-models-createflags-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum CreateFlags { 8 | /// Requires Discord to be running to play the game 9 | Default, 10 | /// Does not require Discord to be running, use this on other platforms 11 | NoRequireDiscord, 12 | } 13 | 14 | impl Default for CreateFlags { 15 | fn default() -> Self { 16 | Self::Default 17 | } 18 | } 19 | 20 | impl Into for CreateFlags { 21 | fn into(self) -> sys::EDiscordCreateFlags { 22 | match self { 23 | Self::Default => sys::DiscordCreateFlags_Default, 24 | Self::NoRequireDiscord => sys::DiscordCreateFlags_NoRequireDiscord, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /discord_game_sdk/src/discord.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, ClientID}; 2 | use std::{cell::UnsafeCell, marker::PhantomData, mem::ManuallyDrop}; 3 | 4 | /// Main interface with SDK 5 | /// 6 | /// The Discord Game SDK is not thread-safe, this struct should only be made `Send`/`Sync` with 7 | /// appropriate safety measures, and not as-is. 8 | /// 9 | /// As opposed to the general structure of the Discord Game SDK, and to help with memory and thread 10 | /// safety, the methods of the Manager "classes" are part of this struct. 11 | /// 12 | /// ### Callbacks 13 | /// 14 | /// All `callback`s will be called with `Err(TransactionAborted)` when the instance is dropped 15 | /// 16 | /// ```rust,compile_fail 17 | /// // Static test to verify callbacks exhibit proper ownership 18 | /// # use discord_game_sdk::*; 19 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> 20 | /// { 21 | /// let illegal = "hey".to_string(); 22 | /// discord.validate_or_exit(move |_, _| { 23 | /// dbg!(&illegal); // moved here, will outlive outer block 24 | /// }); 25 | /// dbg!(&illegal); // but borrowed here, illegal 26 | /// # Ok(()) 27 | /// } 28 | /// ``` 29 | /// 30 | /// ### Iterators 31 | /// 32 | /// ```rust,compile_fail 33 | /// // Static test to verify `Iterator`s depend on `Discord`'s lifetime 34 | /// # use discord_game_sdk::*; 35 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 36 | /// let mut iter = discord.iter_user_achievements(); 37 | /// drop(discord); // dropped while a live reference exists, illegal 38 | /// for achievement in iter { 39 | /// // ... 40 | /// } 41 | /// # Ok(()) } 42 | /// ``` 43 | /// 44 | /// ## Table of Contents 45 | /// 46 | /// - [Core](#core) 47 | /// - [Achievements](#achievements) 48 | /// - [Activities](#activities) 49 | /// - [Applications](#applications) 50 | /// - [Images](#images) 51 | /// - [Lobbies](#lobbies) 52 | /// - [Networking](#networking) 53 | /// - [Overlay](#overlay) 54 | /// - [Relationships](#relationships) 55 | /// - [Storage](#storage) 56 | /// - [Store](#store) 57 | /// - [Users](#users) 58 | /// - [Voice](#voice) 59 | pub struct Discord<'d, E>(pub(crate) *mut DiscordInner<'d, E>); 60 | 61 | impl Drop for Discord<'_, E> { 62 | fn drop(&mut self) { 63 | unsafe { 64 | let core = (*self.0).core; 65 | if !core.is_null() { 66 | (*core).destroy.unwrap()(core); 67 | } 68 | 69 | drop(Box::from_raw(self.0)); 70 | } 71 | } 72 | } 73 | 74 | impl<'d, E> Discord<'d, E> { 75 | /// The Client ID that was supplied during creation 76 | pub fn client_id(&self) -> ClientID { 77 | self.inner().client_id 78 | } 79 | 80 | /// The [`EventHandler`](trait.EventHandler.html) 81 | pub fn event_handler(&self) -> &Option { 82 | self.inner().event_handler() 83 | } 84 | 85 | /// The [`EventHandler`](trait.EventHandler.html) 86 | pub fn event_handler_mut(&mut self) -> &mut Option { 87 | self.inner_mut().event_handler_mut() 88 | } 89 | 90 | pub(crate) fn inner(&self) -> &DiscordInner<'d, E> { 91 | unsafe { &*self.0 } 92 | } 93 | 94 | pub(crate) fn inner_mut(&mut self) -> &mut DiscordInner<'d, E> { 95 | unsafe { &mut *self.0 } 96 | } 97 | 98 | pub(crate) fn ref_copy(&self) -> DiscordRef<'d, E> { 99 | DiscordRef(ManuallyDrop::new(Discord(self.0))) 100 | } 101 | } 102 | 103 | impl std::fmt::Debug for Discord<'_, E> { 104 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 105 | fmt.debug_tuple("Discord").field(&self.inner()).finish() 106 | } 107 | } 108 | 109 | pub(crate) struct DiscordInner<'d, E> { 110 | pub(crate) _invariant_lifetime: PhantomData<*mut &'d ()>, 111 | 112 | pub(crate) core: *mut sys::IDiscordCore, 113 | pub(crate) client_id: sys::DiscordClientId, 114 | pub(crate) event_handler: UnsafeCell>, 115 | 116 | pub(crate) achievement_events: sys::IDiscordAchievementEvents, 117 | pub(crate) activity_events: sys::IDiscordActivityEvents, 118 | pub(crate) lobby_events: sys::IDiscordLobbyEvents, 119 | pub(crate) network_events: sys::IDiscordNetworkEvents, 120 | pub(crate) overlay_events: sys::IDiscordOverlayEvents, 121 | pub(crate) relationship_events: sys::IDiscordRelationshipEvents, 122 | pub(crate) store_events: sys::IDiscordStoreEvents, 123 | pub(crate) user_events: sys::IDiscordUserEvents, 124 | pub(crate) voice_events: sys::IDiscordVoiceEvents, 125 | } 126 | 127 | impl DiscordInner<'_, E> { 128 | pub(crate) fn event_handler(&self) -> &Option { 129 | unsafe { &*self.event_handler.get() } 130 | } 131 | 132 | pub(crate) fn event_handler_mut(&mut self) -> &mut Option { 133 | unsafe { &mut *self.event_handler.get() } 134 | } 135 | } 136 | 137 | impl std::fmt::Debug for DiscordInner<'_, E> { 138 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 139 | fmt.debug_struct("DiscordInner") 140 | .field("ffi_ptr", &self.core) 141 | .field("client_id", &self.client_id) 142 | .field("event_handler", self.event_handler()) 143 | .finish() 144 | } 145 | } 146 | 147 | #[derive(Debug)] 148 | pub(crate) struct DiscordRef<'d, E>(ManuallyDrop>); 149 | 150 | impl<'d, E> std::ops::Deref for DiscordRef<'d, E> { 151 | type Target = Discord<'d, E>; 152 | 153 | fn deref(&self) -> &Discord<'d, E> { 154 | &self.0 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /discord_game_sdk/src/distance.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Lobby Search Max Distance 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#data-models-lobbysearchdistance-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum Distance { 8 | /// Within the same region 9 | Local, 10 | /// Within the same and adjacent regions 11 | Default, 12 | /// Far distances, like US to EU 13 | Extended, 14 | /// All regions 15 | Global, 16 | } 17 | 18 | impl Into for Distance { 19 | fn into(self) -> sys::EDiscordLobbySearchDistance { 20 | match self { 21 | Self::Default => sys::DiscordLobbySearchDistance_Default, 22 | Self::Extended => sys::DiscordLobbySearchDistance_Extended, 23 | Self::Global => sys::DiscordLobbySearchDistance_Global, 24 | Self::Local => sys::DiscordLobbySearchDistance_Local, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /discord_game_sdk/src/entitlement.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, EntitlementKind, Snowflake}; 2 | 3 | /// Proof that user has made a purchase 4 | /// 5 | /// This must then be consumed by your game's backend 6 | /// 7 | /// > [Struct in official docs](https://discordapp.com/developers/docs/game-sdk/store#data-models-entitlement-struct) 8 | #[derive(Clone, Eq, Hash, PartialEq)] 9 | #[repr(transparent)] 10 | pub struct Entitlement(pub(crate) sys::DiscordEntitlement); 11 | 12 | impl Entitlement { 13 | /// The unique ID of the entitlement 14 | pub fn id(&self) -> Snowflake { 15 | self.0.id 16 | } 17 | 18 | /// The kind of entitlement it is 19 | pub fn kind(&self) -> EntitlementKind { 20 | self.0.type_.into() 21 | } 22 | 23 | /// The ID of the SKU to which the user is entitled 24 | pub fn sku_id(&self) -> Snowflake { 25 | self.0.sku_id 26 | } 27 | } 28 | 29 | impl std::fmt::Debug for Entitlement { 30 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | fmt.debug_struct("Entitlement") 32 | .field("id", &self.id()) 33 | .field("kind", &self.kind()) 34 | .field("sku_id", &self.sku_id()) 35 | .finish() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /discord_game_sdk/src/entitlement_kind.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Entitlement Type 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/store#data-models-entitlementtype-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum EntitlementKind { 8 | /// Entitlement was gifted by a developer 9 | DeveloperGift, 10 | /// Entitlement was granted when the SKU was free 11 | FreePurchase, 12 | /// Entitlement was claimed by user for free as a Nitro Subscriber 13 | PremiumPurchase, 14 | /// Entitlement for a Discord Nitro subscription 15 | PremiumSubscription, 16 | /// Entitlement was purchased 17 | Purchase, 18 | /// Entitlement was purchased by a dev in application test mode 19 | TestModePurchase, 20 | /// Entitlement was gifted by another user 21 | UserGift, 22 | /// Safety net for missing definitions 23 | Undefined(sys::EDiscordEntitlementType), 24 | } 25 | 26 | impl From for EntitlementKind { 27 | fn from(source: sys::EDiscordEntitlementType) -> Self { 28 | match source { 29 | sys::DiscordEntitlementType_DeveloperGift => Self::DeveloperGift, 30 | sys::DiscordEntitlementType_FreePurchase => Self::FreePurchase, 31 | sys::DiscordEntitlementType_PremiumPurchase => Self::PremiumPurchase, 32 | sys::DiscordEntitlementType_PremiumSubscription => Self::PremiumSubscription, 33 | sys::DiscordEntitlementType_Purchase => Self::Purchase, 34 | sys::DiscordEntitlementType_TestModePurchase => Self::TestModePurchase, 35 | sys::DiscordEntitlementType_UserGift => Self::UserGift, 36 | _ => Self::Undefined(source), 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /discord_game_sdk/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | use std::fmt; 3 | 4 | /// Alias for a `Result` with the error type [`discord_game_sdk::Error`] 5 | /// 6 | /// [`discord_game_sdk::Error`]: enum.Error.html 7 | pub type Result = std::result::Result; 8 | 9 | /// Discord Error 10 | /// 11 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/discord#data-models-result-enum) 12 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 13 | pub enum Error { 14 | /// Discord isn't working 15 | ServiceUnavailable, 16 | 17 | /// The SDK version is outdated 18 | InvalidVersion, 19 | 20 | /// An internal erorr on transactional operations 21 | LockFailed, 22 | 23 | /// Internal error 24 | Internal, 25 | 26 | /// Invalid payload 27 | InvalidPayload, 28 | 29 | /// Invalid command 30 | InvalidCommand, 31 | 32 | /// Invalid permissions 33 | InvalidPermissions, 34 | 35 | /// Could not fetch 36 | NotFetched, 37 | 38 | /// Not found 39 | NotFound, 40 | 41 | /// User already has network connection open on that channel 42 | Conflict, 43 | 44 | /// Activity secrets must be unique and not match party id 45 | InvalidSecret, 46 | 47 | /// Join request for that user does not exist 48 | InvalidJoinSecret, 49 | 50 | /// Invalid Application ID in Activity payload (none should be set) 51 | NoEligibleActivity, 52 | 53 | /// Invalid invite 54 | InvalidInvite, 55 | 56 | /// Not authenticated 57 | NotAuthenticated, 58 | 59 | /// The user's bearer token is invalid 60 | InvalidAccessToken, 61 | 62 | /// Access token belongs to another application 63 | ApplicationMismatch, 64 | 65 | /// Internal error fetching image data 66 | InvalidDataUrl, 67 | 68 | /// Invalid base64 data 69 | InvalidBase64, 70 | 71 | /// Trying to access data before it was filtered 72 | NotFiltered, 73 | 74 | /// Lobby full 75 | LobbyFull, 76 | 77 | /// Invalid lobby secret 78 | InvalidLobbySecret, 79 | 80 | /// Filename is too long 81 | InvalidFilename, 82 | 83 | /// File is too big 84 | InvalidFileSize, 85 | 86 | /// Invalid entitlement 87 | InvalidEntitlement, 88 | 89 | /// Discord is not installed 90 | NotInstalled, 91 | 92 | /// Discord is not running 93 | NotRunning, 94 | 95 | /// Insufficient buffer 96 | InsufficientBuffer, 97 | 98 | /// Purchase canceled 99 | PurchaseCanceled, 100 | 101 | /// Invalid guild 102 | InvalidGuild, 103 | 104 | /// Invalid event 105 | InvalidEvent, 106 | 107 | /// Invalid channel 108 | InvalidChannel, 109 | 110 | /// Invalid origin 111 | InvalidOrigin, 112 | 113 | /// Rate limited 114 | RateLimited, 115 | 116 | /// `OAuth2` error 117 | OAuth2, 118 | 119 | /// Select channel timeout 120 | SelectChannelTimeout, 121 | 122 | /// Get guild timeout 123 | GetGuildTimeout, 124 | 125 | /// Select voice force required 126 | SelectVoiceForceRequired, 127 | 128 | /// Capture shortcut already listening 129 | CaptureShortcutAlreadyListening, 130 | 131 | /// Unauthorized for achievement 132 | UnauthorizedForAchievement, 133 | 134 | /// Invalid gift code 135 | InvalidGiftCode, 136 | 137 | /// Purchase error 138 | Purchase, 139 | 140 | /// Transaction aborted 141 | TransactionAborted, 142 | 143 | /// Safety net for missing definitions 144 | Undefined(sys::EDiscordResult), 145 | } 146 | 147 | impl fmt::Display for Error { 148 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 149 | use Error::*; 150 | 151 | let message = match self { 152 | ServiceUnavailable => "service unavailable", 153 | InvalidVersion => "invalid version", 154 | LockFailed => "lock failed", 155 | Internal => "internal error", 156 | InvalidPayload => "invalid payload", 157 | InvalidCommand => "invalid command", 158 | InvalidPermissions => "invalid permissions", 159 | NotFetched => "not fetched", 160 | NotFound => "not found", 161 | Conflict => "conflict", 162 | InvalidSecret => "invalid secret", 163 | InvalidJoinSecret => "invalid join secret", 164 | NoEligibleActivity => "no eligible activity", 165 | InvalidInvite => "invalid invite", 166 | NotAuthenticated => "not authenticated", 167 | InvalidAccessToken => "invalid access token", 168 | ApplicationMismatch => "application mismatch", 169 | InvalidDataUrl => "invalid data URL", 170 | InvalidBase64 => "invalid base-64", 171 | NotFiltered => "not filtered", 172 | LobbyFull => "lobby full", 173 | InvalidLobbySecret => "invalid lobby secret", 174 | InvalidFilename => "invalid filename", 175 | InvalidFileSize => "invalid file size", 176 | InvalidEntitlement => "invalid entitlement", 177 | NotInstalled => "not installed", 178 | NotRunning => "not running", 179 | InsufficientBuffer => "insufficient buffer", 180 | PurchaseCanceled => "purchase canceled", 181 | InvalidGuild => "invalid guild", 182 | InvalidEvent => "invalid event", 183 | InvalidChannel => "invalid channel", 184 | InvalidOrigin => "invalid origin", 185 | RateLimited => "rate limited", 186 | OAuth2 => "OAuth 2.0 error", 187 | SelectChannelTimeout => "select channel timeout", 188 | GetGuildTimeout => "get guild timeout", 189 | SelectVoiceForceRequired => "select voice force required", 190 | CaptureShortcutAlreadyListening => "capture shortcut already listening", 191 | UnauthorizedForAchievement => "unauthorized for achievement", 192 | InvalidGiftCode => "invalid gift code", 193 | Purchase => "purchase error", 194 | TransactionAborted => "transaction aborted", 195 | Undefined(n) => return write!(f, "undefined error {}", n), 196 | }; 197 | 198 | write!(f, "{}", message) 199 | } 200 | } 201 | 202 | impl std::error::Error for Error {} 203 | -------------------------------------------------------------------------------- /discord_game_sdk/src/event_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Action, Activity, Discord, Entitlement, LobbyID, NetworkChannelID, NetworkPeerID, Relationship, 3 | User, UserAchievement, UserID, 4 | }; 5 | 6 | #[allow(unused_variables)] 7 | /// Trait providing callbacks for the SDK. 8 | /// 9 | /// All methods have a default empty implementation. 10 | pub trait EventHandler: Sized { 11 | /// Fired when an User Achievement is updated 12 | /// 13 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/achievements#onuserachievementupdate) 14 | fn on_user_achievement_update( 15 | &mut self, 16 | discord: &Discord<'_, Self>, 17 | user_achievement: &UserAchievement, 18 | ) { 19 | } 20 | 21 | /// Fired when the current user accepts an invitation to join in chat or receives confirmation from Asking to Join. 22 | /// 23 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/activities#onactivityjoin) 24 | fn on_activity_join(&mut self, discord: &Discord<'_, Self>, secret: &str) {} 25 | 26 | /// Fired when the current user accepts an invitation to spectate in chat 27 | /// or clicks the Spectate button on another user's profile. 28 | /// 29 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/activities#onactivityspectate) 30 | fn on_activity_spectate(&mut self, discord: &Discord<'_, Self>, secret: &str) {} 31 | 32 | /// Fires when a user asks to join the game of the current user. 33 | /// 34 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/activities#onactivityjoinrequest) 35 | fn on_activity_join_request(&mut self, discord: &Discord<'_, Self>, user: &User) {} 36 | 37 | /// Fires when the current user receives an invitation to join or spectate. 38 | /// 39 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/activities#onactivityinvite) 40 | fn on_activity_invite( 41 | &mut self, 42 | discord: &Discord<'_, Self>, 43 | kind: Action, 44 | user: &User, 45 | activity: &Activity, 46 | ) { 47 | } 48 | 49 | /// Fires when a lobby is updated. 50 | /// 51 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#onlobbyupdate) 52 | fn on_lobby_update(&mut self, discord: &Discord<'_, Self>, lobby_id: LobbyID) {} 53 | 54 | /// Fired when a lobby is deleted. 55 | /// 56 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#onlobbydelete) 57 | fn on_lobby_delete(&mut self, discord: &Discord<'_, Self>, lobby_id: LobbyID, reason: u32) {} 58 | 59 | /// Fires when a member joins the lobby. 60 | /// 61 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#onmemberconnect) 62 | fn on_member_connect( 63 | &mut self, 64 | discord: &Discord<'_, Self>, 65 | lobby_id: LobbyID, 66 | member_id: UserID, 67 | ) { 68 | } 69 | 70 | /// Fires when data for a lobby member is updated. 71 | /// 72 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#onmemberupdate) 73 | fn on_member_update( 74 | &mut self, 75 | discord: &Discord<'_, Self>, 76 | lobby_id: LobbyID, 77 | member_id: UserID, 78 | ) { 79 | } 80 | 81 | /// Fires when a member leaves the lobby. 82 | /// 83 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#onmemberdisconnect) 84 | fn on_member_disconnect( 85 | &mut self, 86 | discord: &Discord<'_, Self>, 87 | lobby_id: LobbyID, 88 | member_id: UserID, 89 | ) { 90 | } 91 | 92 | /// Fires when a message is sent to the lobby. 93 | /// 94 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#onlobbymessage) 95 | fn on_lobby_message( 96 | &mut self, 97 | discord: &Discord<'_, Self>, 98 | lobby_id: LobbyID, 99 | member_id: UserID, 100 | data: &[u8], 101 | ) { 102 | } 103 | 104 | /// Fires when a user connected to voice starts or stops speaking. 105 | /// 106 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#onspeaking) 107 | fn on_speaking( 108 | &mut self, 109 | discord: &Discord<'_, Self>, 110 | lobby_id: LobbyID, 111 | member_id: UserID, 112 | speaking: bool, 113 | ) { 114 | } 115 | 116 | /// Fires when the user receives a message from the lobby's networking layer. 117 | /// 118 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#onnetworkmessage) 119 | fn on_lobby_network_message( 120 | &mut self, 121 | discord: &Discord<'_, Self>, 122 | lobby_id: LobbyID, 123 | member_id: UserID, 124 | channel_id: NetworkChannelID, 125 | data: &[u8], 126 | ) { 127 | } 128 | 129 | /// Fires when you receive data from another user. 130 | /// 131 | /// This callback will only fire if you already have an open channel with the user sending you data. 132 | /// 133 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/networking#onmessage) 134 | fn on_network_message( 135 | &mut self, 136 | discord: &Discord<'_, Self>, 137 | peer_id: NetworkPeerID, 138 | channel_id: NetworkChannelID, 139 | data: &[u8], 140 | ) { 141 | } 142 | 143 | /// Fires when your networking route has changed. 144 | /// 145 | /// You should broadcast this change to other users. 146 | /// 147 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/networking#onrouteupdate) 148 | fn on_network_route_update(&mut self, discord: &Discord<'_, Self>, route: &str) {} 149 | 150 | /// Fires when the overlay is opened or closed. 151 | /// 152 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/overlay#ontoggle) 153 | fn on_overlay_toggle(&mut self, discord: &Discord<'_, Self>, closed: bool) {} 154 | 155 | /// Fires at initialization when Discord<'_, Self> has cached a snapshot of all your relationships. 156 | /// 157 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/relationships#onrefresh) 158 | fn on_relationships_refresh(&mut self, discord: &Discord<'_, Self>) {} 159 | 160 | /// Fires when a relationship in the filtered list changes, like an updated presence or user attribute. 161 | /// 162 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/relationships#onrelationshipupdate) 163 | fn on_relationship_update(&mut self, discord: &Discord<'_, Self>, relationship: &Relationship) { 164 | } 165 | 166 | /// Fires when the connected user receives a new entitlement, either through purchase or through a developer grant. 167 | /// 168 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/store#onentitlementcreate) 169 | fn on_entitlement_create(&mut self, discord: &Discord<'_, Self>, entitlement: &Entitlement) {} 170 | 171 | /// Fires when the connected user loses an entitlement, either by expiration, revocation, 172 | /// or consumption in the case of consumable entitlements. 173 | /// 174 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/store#onentitlementdelete) 175 | fn on_entitlement_delete(&mut self, discord: &Discord<'_, Self>, entitlement: &Entitlement) {} 176 | 177 | /// Fires when the User struct of the currently connected user changes. 178 | /// 179 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/users#oncurrentuserupdate) 180 | fn on_current_user_update(&mut self, discord: &Discord<'_, Self>) {} 181 | 182 | /// Fires when the current user has updated their voice settings. 183 | fn on_voice_settings_update(&mut self, discord: &Discord<'_, Self>) {} 184 | } 185 | 186 | /// Empty implementation 187 | impl EventHandler for () {} 188 | -------------------------------------------------------------------------------- /discord_game_sdk/src/fetch_kind.rs: -------------------------------------------------------------------------------- 1 | /// Image Fetch Option 2 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 3 | pub enum FetchKind { 4 | /// Always download a fresh version of the image 5 | ForceRefresh, 6 | /// Use a cached version of the image if available 7 | UseCached, 8 | } 9 | 10 | impl Into for FetchKind { 11 | fn into(self) -> bool { 12 | match self { 13 | Self::ForceRefresh => true, 14 | Self::UseCached => false, 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /discord_game_sdk/src/file_stat.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, utils::charbuf_to_str, UnixTimestamp}; 2 | use std::convert::TryInto; 3 | 4 | /// File Metadata 5 | /// 6 | /// > [Struct in official docs](https://discordapp.com/developers/docs/game-sdk/storage#data-models-filestat-struct) 7 | #[derive(Clone, Eq, PartialEq)] 8 | #[repr(transparent)] 9 | pub struct FileStat(pub(crate) sys::DiscordFileStat); 10 | 11 | impl FileStat { 12 | /// The name of the file 13 | pub fn filename(&self) -> &str { 14 | charbuf_to_str(&self.0.filename) 15 | } 16 | 17 | /// The total size in bytes 18 | pub fn size(&self) -> u64 { 19 | self.0.size 20 | } 21 | 22 | /// When the file was last modified, in UNIX Time 23 | pub fn last_modified(&self) -> UnixTimestamp { 24 | // XXX: u64 should be UnixTimestamp 25 | self.0.last_modified.try_into().unwrap() 26 | } 27 | } 28 | 29 | impl std::fmt::Debug for FileStat { 30 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | fmt.debug_struct("FileStat") 32 | .field("filename", &self.filename()) 33 | .field("size", &self.size()) 34 | .field("last_modified", &self.last_modified()) 35 | .finish() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /discord_game_sdk/src/image.rs: -------------------------------------------------------------------------------- 1 | /// Image with pixel data 2 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 3 | pub struct Image { 4 | pub(crate) width: u32, 5 | pub(crate) height: u32, 6 | pub(crate) data: Vec, 7 | } 8 | 9 | impl Image { 10 | /// The width and height in pixels of the image 11 | pub fn dimensions(&self) -> (u32, u32) { 12 | (self.width, self.height) 13 | } 14 | 15 | /// The width in pixels of the image 16 | pub fn width(&self) -> u32 { 17 | self.width 18 | } 19 | 20 | /// The height in pixels of the image 21 | pub fn height(&self) -> u32 { 22 | self.height 23 | } 24 | 25 | /// Flat slice of uncompressed SRGBA image data 26 | /// 27 | /// Length is `width * height * 4` 28 | /// 29 | /// Pattern is: `RGBARGBARGBA...` 30 | pub fn data(&self) -> &[u8] { 31 | &self.data 32 | } 33 | } 34 | 35 | #[cfg(feature = "image")] 36 | impl Into for Image { 37 | fn into(self) -> image::RgbaImage { 38 | image::RgbaImage::from_raw(self.width, self.height, self.data) 39 | .expect("discord_game_sdk: invalid size for image buffer") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /discord_game_sdk/src/image_handle.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, ImageKind, Snowflake, UserID}; 2 | 3 | /// Image Handle 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/images#data-models-imagehandle-struct) 6 | #[derive(Clone, Eq, Hash, PartialEq)] 7 | #[repr(transparent)] 8 | pub struct ImageHandle(pub(crate) sys::DiscordImageHandle); 9 | 10 | impl ImageHandle { 11 | /// What sort of image it is 12 | pub fn kind(&self) -> ImageKind { 13 | self.0.type_.into() 14 | } 15 | 16 | /// A unique ID related to the image, when kind is User, it is the ID of said user 17 | pub fn id(&self) -> Snowflake { 18 | self.0.id 19 | } 20 | 21 | /// The resolution desired 22 | pub fn size(&self) -> u32 { 23 | self.0.size 24 | } 25 | 26 | /// Create new Image Handle 27 | pub fn from_user_id(user_id: UserID, size: u32) -> Self { 28 | let mut handle = sys::DiscordImageHandle::default(); 29 | 30 | handle.type_ = ImageKind::User.into(); 31 | handle.id = user_id; 32 | handle.size = size; 33 | 34 | Self(handle) 35 | } 36 | } 37 | 38 | impl std::fmt::Debug for ImageHandle { 39 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | fmt.debug_struct("ImageHandle") 41 | .field("kind", &self.kind()) 42 | .field("id", &self.id()) 43 | .field("size", &self.size()) 44 | .finish() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /discord_game_sdk/src/image_kind.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Image Type 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/images#data-models-imagetype-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum ImageKind { 8 | /// User Avatar 9 | User, 10 | /// Safety net for missing definitions 11 | Undefined(sys::EDiscordImageType), 12 | } 13 | 14 | impl From for ImageKind { 15 | fn from(source: sys::EDiscordImageType) -> Self { 16 | match source { 17 | sys::DiscordImageType_User => Self::User, 18 | _ => Self::Undefined(source), 19 | } 20 | } 21 | } 22 | 23 | impl Into for ImageKind { 24 | fn into(self) -> sys::EDiscordImageType { 25 | match self { 26 | Self::User => sys::DiscordImageType_User, 27 | Self::Undefined(n) => n, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /discord_game_sdk/src/input_mode.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | sys, 3 | utils::{charbuf_to_str, write_charbuf}, 4 | InputModeKind, 5 | }; 6 | 7 | /// Input Mode 8 | /// 9 | /// > [Struct in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice#data-models-inputmode-struct) 10 | /// > [Shortcut keys in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice#data-models-shortcut-keys) 11 | #[derive(Clone, Eq, PartialEq)] 12 | #[repr(transparent)] 13 | pub struct InputMode(pub(crate) sys::DiscordInputMode); 14 | 15 | impl InputMode { 16 | /// What triggers voice to be transmitted 17 | pub fn kind(&self) -> InputModeKind { 18 | self.0.type_.into() 19 | } 20 | 21 | /// The combination of keys to transmit voice when kind is [`PushToTalk`]. 22 | /// 23 | /// [`PushToTalk`]: enum.InputModeKind.html#variant.PushToTalk 24 | pub fn shortcut(&self) -> &str { 25 | charbuf_to_str(&self.0.shortcut) 26 | } 27 | 28 | /// Create a new Input Mode with kind [`VoiceActivity`]. 29 | /// 30 | /// [`VoiceActivity`]: enum.InputModeKind.html#variant.VoiceActivity 31 | pub fn voice_activity() -> Self { 32 | Self(sys::DiscordInputMode { 33 | type_: sys::DiscordInputModeType_VoiceActivity, 34 | ..sys::DiscordInputMode::default() 35 | }) 36 | } 37 | 38 | /// Create a new Input Mode with kind [`PushToTalk`] and a shortcut. 39 | /// 40 | /// Only the first 256 bytes will be written. 41 | /// 42 | /// [`PushToTalk`]: enum.InputModeKind.html#variant.PushToTalk 43 | pub fn push_to_talk(shortcut: &str) -> Self { 44 | let mut mode = sys::DiscordInputMode { 45 | type_: sys::DiscordInputModeType_PushToTalk, 46 | ..sys::DiscordInputMode::default() 47 | }; 48 | 49 | write_charbuf(&mut mode.shortcut, shortcut); 50 | 51 | Self(mode) 52 | } 53 | } 54 | 55 | impl std::fmt::Debug for InputMode { 56 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 57 | fmt.debug_struct("InputMode") 58 | .field("kind", &self.kind()) 59 | .field("shortcut", &self.shortcut()) 60 | .finish() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /discord_game_sdk/src/input_mode_kind.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Input Mode Type 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice#data-models-inputmodetype-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum InputModeKind { 8 | /// Voice is transmitted when a key is pushed 9 | PushToTalk, 10 | /// Voice is transmitted when detected by Discord 11 | VoiceActivity, 12 | /// Safety net for missing definitions 13 | Undefined(sys::EDiscordInputModeType), 14 | } 15 | 16 | impl From for InputModeKind { 17 | fn from(source: sys::EDiscordInputModeType) -> Self { 18 | match source { 19 | sys::DiscordInputModeType_PushToTalk => Self::PushToTalk, 20 | sys::DiscordInputModeType_VoiceActivity => Self::VoiceActivity, 21 | _ => Self::Undefined(source), 22 | } 23 | } 24 | } 25 | 26 | impl Into for InputModeKind { 27 | fn into(self) -> sys::EDiscordInputModeType { 28 | match self { 29 | Self::PushToTalk => sys::DiscordInputModeType_PushToTalk, 30 | Self::VoiceActivity => sys::DiscordInputModeType_VoiceActivity, 31 | Self::Undefined(n) => n, 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /discord_game_sdk/src/iter.rs: -------------------------------------------------------------------------------- 1 | pub(crate) struct Collection<'r, I> { 2 | getter: Box I>, 3 | count: u32, 4 | index: u32, 5 | back_index: u32, 6 | } 7 | 8 | impl<'r, I> Collection<'r, I> { 9 | pub(crate) fn new(getter: Box I>, count: u32) -> Self { 10 | Self { 11 | getter, 12 | count, 13 | index: 0, 14 | back_index: 0, 15 | } 16 | } 17 | } 18 | 19 | impl Iterator for Collection<'_, I> { 20 | type Item = I; 21 | 22 | fn next(&mut self) -> Option { 23 | if self.index + self.back_index < self.count { 24 | self.index += 1; 25 | Some((self.getter)(self.index - 1)) 26 | } else { 27 | None 28 | } 29 | } 30 | 31 | fn size_hint(&self) -> (usize, Option) { 32 | (self.count as usize, Some(self.count as usize)) 33 | } 34 | } 35 | 36 | impl DoubleEndedIterator for Collection<'_, I> { 37 | fn next_back(&mut self) -> Option { 38 | if self.index + self.back_index < self.count { 39 | self.back_index += 1; 40 | Some((self.getter)(self.count - self.back_index)) 41 | } else { 42 | None 43 | } 44 | } 45 | } 46 | 47 | impl ExactSizeIterator for Collection<'_, I> {} 48 | 49 | impl std::iter::FusedIterator for Collection<'_, I> {} 50 | 51 | impl std::fmt::Debug for Collection<'_, I> { 52 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 53 | fmt.debug_struct("Collection") 54 | .field("getter", &(..)) 55 | .field("count", &self.count) 56 | .field("index", &self.index) 57 | .field("back_index", &self.back_index) 58 | .finish() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /discord_game_sdk/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides a safe interface to the [Discord Game SDK]. 2 | //! 3 | //! *This crate is not official, it is not supported by the Discord Game SDK Developers.* 4 | //! 5 | //! The [Discord Game SDK] provides features such as, but not limited to: 6 | //! 7 | //! - Activities (Rich Presence) 8 | //! - Users, Avatars and Relationships 9 | //! - Lobbies, Matchmaking and Voice communication 10 | //! - Faux-P2P Networking on Discord's Infrastructure 11 | //! - Cloud Synchronized Storage 12 | //! - Store Transactions 13 | //! - Achievements 14 | //! 15 | //! *Version requirement: Rust 1.47 and up.* 16 | //! 17 | //! *[Release Notes](https://github.com/ldesgoui/discord_game_sdk/releases)* 18 | //! 19 | //! 20 | //! # Usage 21 | //! 22 | //! Add this to your `Cargo.toml`: 23 | //! 24 | //! ```toml 25 | //! [dependencies] 26 | //! discord_game_sdk = "1.0.1" 27 | //! ``` 28 | //! 29 | //! Read up on potential [`bindgen` requirements]. 30 | //! 31 | //! Download the [Discord Game SDK] and set the following environment variable to where you extracted it: 32 | //! 33 | //! ```sh 34 | //! export DISCORD_GAME_SDK_PATH=/path/to/discord_game_sdk 35 | //! ``` 36 | //! 37 | //! If you're also planning on using the default `link` feature, keep reading below. 38 | //! 39 | //! 40 | //! # Features: 41 | //! 42 | //! ### `link` 43 | //! 44 | //! Enabled by default, delegates to `discord_game_sdk_sys/link`. 45 | //! 46 | //! Provides functional linking with the caveat that libraries are renamed and some additional 47 | //! set-up is required: 48 | //! 49 | //! ```sh 50 | //! # Linux: prepend with `lib` and add to library search path 51 | //! cp $DISCORD_GAME_SDK_PATH/lib/x86_64/{,lib}discord_game_sdk.so 52 | //! export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}$DISCORD_GAME_SDK_PATH/lib/x86_64 53 | //! 54 | //! # Mac OS: prepend with `lib` and add to library search path 55 | //! cp $DISCORD_GAME_SDK_PATH/lib/x86_64/{,lib}discord_game_sdk.dylib 56 | //! export DYLD_LIBRARY_PATH=${DYLD_LIBRARY_PATH:+${DYLD_LIBRARY_PATH}:}$DISCORD_GAME_SDK_PATH/lib/x86_64 57 | //! 58 | //! # Windows: change `dll.lib` to `lib` (won't affect library searching) 59 | //! cp $DISCORD_GAME_SDK_PATH/lib/x86_64/discord_game_sdk.{dll.lib,lib} 60 | //! cp $DISCORD_GAME_SDK_PATH/lib/x86/discord_game_sdk.{dll.lib,lib} 61 | //! ``` 62 | //! 63 | //! This allows for `cargo run` to function. 64 | //! 65 | //! 66 | //! ### [`image`](https://docs.rs/image) 67 | //! 68 | //! Optional crate. 69 | //! 70 | //! Provides a conversion from our `Image` to `image::RgbaImage`. 71 | //! 72 | //! 73 | //! # Safety 74 | //! 75 | //! This crate relies on the SDK to provide correct data and behavior: 76 | //! 77 | //! - Non-null pointers to valid memory 78 | //! - UTF-8, NUL-terminated strings 79 | //! - No mutation of memory it should have no ownership of 80 | //! - No use of pointers after `destroy` is called 81 | //! 82 | //! Some of these are tested when compiled with `debug_assertions`. 83 | //! 84 | //! 85 | //! # Legal 86 | //! 87 | //! You *MUST* acquaint yourself with and agree to the [official terms of the Discord Game SDK]. 88 | //! 89 | //! The code of the Rust crates `discord_game_sdk` and `discord_game_sdk_sys` 90 | //! are licensed at your option under either of: 91 | //! 92 | //! * [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) 93 | //! * [MIT License](https://opensource.org/licenses/MIT) 94 | //! 95 | //! Unless you explicitly state otherwise, any contribution intentionally 96 | //! submitted for inclusion in the work by you, as defined in the Apache-2.0 97 | //! license, shall be dual licensed as above, without any additional terms or 98 | //! conditions. 99 | //! 100 | //! 101 | //! [Discord Game SDK]: https://discordapp.com/developers/docs/game-sdk/sdk-starter-guide 102 | //! [`bindgen` requirements]: https://rust-lang.github.io/rust-bindgen/requirements.html 103 | //! [official terms of the Discord Game SDK]: https://discordapp.com/developers/docs/legal 104 | 105 | #![doc(html_root_url = "https://docs.rs/discord_game_sdk/1.0.1")] 106 | 107 | mod action; 108 | mod activity; 109 | mod activity_kind; 110 | mod aliases; 111 | mod cast; 112 | mod comparison; 113 | mod create_flags; 114 | mod discord; 115 | mod distance; 116 | mod entitlement; 117 | mod entitlement_kind; 118 | mod error; 119 | mod event_handler; 120 | pub(crate) mod events; 121 | mod fetch_kind; 122 | mod file_stat; 123 | mod image; 124 | mod image_handle; 125 | mod image_kind; 126 | mod input_mode; 127 | mod input_mode_kind; 128 | pub(crate) mod iter; 129 | mod lobby; 130 | mod lobby_kind; 131 | mod lobby_member_transaction; 132 | mod lobby_transaction; 133 | mod oauth2_token; 134 | mod premium_kind; 135 | mod presence; 136 | mod relationship; 137 | mod relationship_kind; 138 | mod reliability; 139 | mod request_reply; 140 | mod search_query; 141 | mod sku; 142 | mod sku_kind; 143 | mod status; 144 | mod to_result; 145 | mod user; 146 | mod user_achievement; 147 | mod user_flags; 148 | pub(crate) mod utils; 149 | 150 | mod methods { 151 | mod core; 152 | 153 | mod achievements; 154 | mod activities; 155 | mod applications; 156 | mod images; 157 | mod lobbies; 158 | mod networking; 159 | mod overlay; 160 | mod relationships; 161 | mod storage; 162 | mod store; 163 | mod users; 164 | mod voice; 165 | 166 | mod callback; 167 | } 168 | 169 | #[cfg(test)] 170 | mod mock; 171 | 172 | pub(crate) use discord_game_sdk_sys as sys; 173 | 174 | pub use self::{ 175 | action::Action, 176 | activity::Activity, 177 | activity_kind::ActivityKind, 178 | aliases::*, 179 | cast::Cast, 180 | comparison::Comparison, 181 | create_flags::CreateFlags, 182 | discord::Discord, 183 | distance::Distance, 184 | entitlement::Entitlement, 185 | entitlement_kind::EntitlementKind, 186 | error::{Error, Result}, 187 | event_handler::EventHandler, 188 | fetch_kind::FetchKind, 189 | file_stat::FileStat, 190 | image::Image, 191 | image_handle::ImageHandle, 192 | image_kind::ImageKind, 193 | input_mode::InputMode, 194 | input_mode_kind::InputModeKind, 195 | lobby::Lobby, 196 | lobby_kind::LobbyKind, 197 | lobby_member_transaction::LobbyMemberTransaction, 198 | lobby_transaction::LobbyTransaction, 199 | oauth2_token::OAuth2Token, 200 | premium_kind::PremiumKind, 201 | presence::Presence, 202 | relationship::Relationship, 203 | relationship_kind::RelationshipKind, 204 | reliability::Reliability, 205 | request_reply::RequestReply, 206 | search_query::SearchQuery, 207 | sku::Sku, 208 | sku_kind::SkuKind, 209 | status::Status, 210 | user::User, 211 | user_achievement::UserAchievement, 212 | user_flags::UserFlags, 213 | }; 214 | -------------------------------------------------------------------------------- /discord_game_sdk/src/lobby.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, utils::charbuf_to_str, LobbyID, LobbyKind, UserID}; 2 | 3 | /// Lobby 4 | /// 5 | /// > [Struct in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#data-models-lobby-struct) 6 | #[derive(Clone, Eq, PartialEq)] 7 | #[repr(transparent)] 8 | pub struct Lobby(pub(crate) sys::DiscordLobby); 9 | 10 | impl Lobby { 11 | /// The unique ID of the lobby 12 | pub fn id(&self) -> LobbyID { 13 | self.0.id 14 | } 15 | 16 | /// What sort of lobby it is 17 | pub fn kind(&self) -> LobbyKind { 18 | self.0.type_.into() 19 | } 20 | 21 | /// The unique ID of the user owning the lobby 22 | pub fn owner_id(&self) -> UserID { 23 | self.0.owner_id 24 | } 25 | 26 | /// The password to the lobby 27 | pub fn secret(&self) -> &str { 28 | charbuf_to_str(&self.0.secret) 29 | } 30 | 31 | /// The maximum number of players that can join 32 | pub fn capacity(&self) -> u32 { 33 | self.0.capacity 34 | } 35 | 36 | /// Whether the lobby can be joined or not 37 | pub fn locked(&self) -> bool { 38 | self.0.locked 39 | } 40 | } 41 | 42 | impl std::fmt::Debug for Lobby { 43 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 44 | fmt.debug_struct("Lobby") 45 | .field("id", &self.id()) 46 | .field("kind", &self.kind()) 47 | .field("owner_id", &self.owner_id()) 48 | .field("secret", &self.secret()) 49 | .field("capacity", &self.capacity()) 50 | .field("locked", &self.locked()) 51 | .finish() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /discord_game_sdk/src/lobby_kind.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Lobby Type 4 | /// 5 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#data-models-lobbytype-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum LobbyKind { 8 | /// Lobby is public 9 | Public, 10 | /// Lobby is private (cannot be joined through matchmaking) 11 | Private, 12 | /// Safety net for missing definitions 13 | Undefined(sys::EDiscordLobbyType), 14 | } 15 | 16 | impl From for LobbyKind { 17 | fn from(source: sys::EDiscordLobbyType) -> Self { 18 | match source { 19 | sys::DiscordLobbyType_Public => Self::Public, 20 | sys::DiscordLobbyType_Private => Self::Private, 21 | _ => Self::Undefined(source), 22 | } 23 | } 24 | } 25 | 26 | impl Into for LobbyKind { 27 | fn into(self) -> sys::EDiscordLobbyType { 28 | match self { 29 | Self::Public => sys::DiscordLobbyType_Public, 30 | Self::Private => sys::DiscordLobbyType_Private, 31 | Self::Undefined(n) => n, 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /discord_game_sdk/src/lobby_member_transaction.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, to_result::ToResult, Result}; 2 | use std::collections::HashMap; 3 | 4 | /// Lobby Member Transaction 5 | /// 6 | /// > [Struct in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#data-models-lobbymembertransaction-struct) 7 | #[derive(Clone, Debug, Default)] 8 | pub struct LobbyMemberTransaction { 9 | pub(crate) metadata: HashMap>, 10 | } 11 | 12 | impl LobbyMemberTransaction { 13 | /// Gets a member update transaction. 14 | /// 15 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#getmemberupdatetransaction) 16 | pub fn new() -> Self { 17 | Self::default() 18 | } 19 | 20 | /// Sets metadata value under a given key for the user. 21 | /// 22 | /// ## Performance 23 | /// 24 | /// A nul byte will be appended to `key` and `value` if one is not present. 25 | /// 26 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#lobbymembertransactionsetmetadata) 27 | pub fn add_metadata(&mut self, mut key: String, mut value: String) -> &mut Self { 28 | if !key.ends_with('\0') { 29 | key.push('\0') 30 | } 31 | 32 | if !value.ends_with('\0') { 33 | value.push('\0') 34 | } 35 | 36 | let _ = self.metadata.insert(key, Some(value)); 37 | 38 | self 39 | } 40 | 41 | /// Deletes metadata value under a given key for the user 42 | /// 43 | /// ## Performance 44 | /// 45 | /// A nul byte will be appended to `key` if one is not present. 46 | /// 47 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#lobbymembertransactiondeletemetadata) 48 | pub fn delete_metadata(&mut self, mut key: String) -> &mut Self { 49 | if !key.ends_with('\0') { 50 | key.push('\0') 51 | } 52 | 53 | let _ = self.metadata.insert(key, None); 54 | self 55 | } 56 | 57 | pub(crate) unsafe fn process( 58 | &self, 59 | tx: *mut sys::IDiscordLobbyMemberTransaction, 60 | ) -> Result<()> { 61 | for (key, value) in &self.metadata { 62 | match value { 63 | Some(value) => { 64 | (*tx).set_metadata.unwrap()( 65 | tx, 66 | // XXX: *mut should be *const 67 | key.as_ptr() as *mut u8, 68 | // XXX: *mut should be *const 69 | value.as_ptr() as *mut u8, 70 | ) 71 | .to_result()?; 72 | } 73 | None => { 74 | (*tx).delete_metadata.unwrap()( 75 | tx, 76 | // XXX: *mut should be *const 77 | key.as_ptr() as *mut u8, 78 | ) 79 | .to_result()?; 80 | } 81 | } 82 | } 83 | 84 | Ok(()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /discord_game_sdk/src/lobby_transaction.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, to_result::ToResult, LobbyKind, Result, UserID}; 2 | use std::collections::HashMap; 3 | 4 | /// Lobby Transaction 5 | /// 6 | /// > [Struct in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#data-models-lobbytransaction-struct) 7 | #[derive(Clone, Debug, Default)] 8 | pub struct LobbyTransaction { 9 | pub(crate) kind: Option, 10 | pub(crate) owner: Option, 11 | pub(crate) capacity: Option, 12 | pub(crate) locked: Option, 13 | pub(crate) metadata: HashMap>, 14 | } 15 | 16 | impl LobbyTransaction { 17 | /// Gets a Lobby transaction used for creating or updating a new lobby. 18 | /// 19 | /// > [`GetLobbyCreateTransaction` in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#getlobbycreatetransaction) 20 | /// > [`GetLobbyUpdateTransaction` in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#getlobbyupdatetransaction) 21 | pub fn new() -> Self { 22 | Self::default() 23 | } 24 | 25 | /// Marks the lobby as private or public 26 | /// 27 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#lobbytransactionsettype) 28 | pub fn kind(&mut self, kind: LobbyKind) -> &mut Self { 29 | self.kind = Some(kind); 30 | self 31 | } 32 | 33 | /// Sets the ID of the user owning the lobby 34 | /// 35 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#lobbytransactionsetowner) 36 | pub fn owner(&mut self, user_id: UserID) -> &mut Self { 37 | self.owner = Some(user_id); 38 | self 39 | } 40 | 41 | /// Sets the maximum amount of players that can join 42 | /// 43 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#lobbytransactionsetcapacity) 44 | pub fn capacity(&mut self, capacity: u32) -> &mut Self { 45 | self.capacity = Some(capacity); 46 | self 47 | } 48 | 49 | /// Set metadata value under a given key for the lobby 50 | /// 51 | /// ## Performance 52 | /// 53 | /// A nul byte will be appended to `key` and `value` if one is not present. 54 | /// 55 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#lobbytransactionsetmetadata) 56 | pub fn add_metadata(&mut self, mut key: String, mut value: String) -> &mut Self { 57 | if !key.ends_with('\0') { 58 | key.push('\0') 59 | } 60 | 61 | if !value.ends_with('\0') { 62 | value.push('\0') 63 | } 64 | 65 | let _ = self.metadata.insert(key, Some(value)); 66 | self 67 | } 68 | 69 | /// Deletes metadata value under a given key for the lobby 70 | /// 71 | /// ## Performance 72 | /// 73 | /// A nul byte will be appended to `key` if one is not present. 74 | /// 75 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#lobbytransactiondeletemetadata) 76 | pub fn delete_metadata(&mut self, mut key: String) -> &mut Self { 77 | if !key.ends_with('\0') { 78 | key.push('\0') 79 | } 80 | 81 | let _ = self.metadata.insert(key, None); 82 | self 83 | } 84 | 85 | /// Sets whether the lobby is locked or not. When locked, new users cannot join 86 | /// 87 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#lobbytransactionsetlocked) 88 | pub fn locked(&mut self, locked: bool) -> &mut Self { 89 | self.locked = Some(locked); 90 | self 91 | } 92 | 93 | pub(crate) unsafe fn process(&self, tx: *mut sys::IDiscordLobbyTransaction) -> Result<()> { 94 | if let Some(kind) = self.kind { 95 | (*tx).set_type.unwrap()(tx, kind.into()).to_result()?; 96 | } 97 | 98 | if let Some(user_id) = self.owner { 99 | (*tx).set_owner.unwrap()(tx, user_id).to_result()?; 100 | } 101 | 102 | if let Some(capacity) = self.capacity { 103 | (*tx).set_capacity.unwrap()(tx, capacity).to_result()?; 104 | } 105 | 106 | if let Some(locked) = self.locked { 107 | (*tx).set_locked.unwrap()(tx, locked).to_result()?; 108 | } 109 | 110 | for (key, value) in &self.metadata { 111 | match value { 112 | Some(value) => { 113 | (*tx).set_metadata.unwrap()( 114 | tx, 115 | // XXX: *mut should be *const 116 | key.as_ptr() as *mut u8, 117 | // XXX: *mut should be *const 118 | value.as_ptr() as *mut u8, 119 | ) 120 | .to_result()?; 121 | } 122 | 123 | None => { 124 | (*tx).delete_metadata.unwrap()( 125 | tx, 126 | // XXX: *mut should be *const 127 | key.as_ptr() as *mut u8, 128 | ) 129 | .to_result()?; 130 | } 131 | } 132 | } 133 | 134 | Ok(()) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /discord_game_sdk/src/methods/achievements.rs: -------------------------------------------------------------------------------- 1 | use crate::{iter, sys, to_result::ToResult, Discord, Result, Snowflake, UserAchievement}; 2 | use std::convert::TryInto; 3 | 4 | /// # Achievements 5 | /// 6 | /// Achievements are managed in the [Developer Portal](https://discordapp.com/developers/applications). 7 | /// 8 | /// Some operations will require an HTTP client, or must be ran from your game backend: 9 | /// [Reference](https://discordapp.com/developers/docs/game-sdk/achievements#the-api-way). 10 | /// 11 | /// > [Chapter in official docs](https://discordapp.com/developers/docs/game-sdk/achievements) 12 | impl<'d, E> Discord<'d, E> { 13 | /// Updates the current user's completion for a given achievement. 14 | /// 15 | /// `percent_complete` must be in the range `0..=100`. 16 | /// 17 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/achievements#setuserachievement) 18 | /// 19 | /// ```rust 20 | /// # use discord_game_sdk::*; 21 | /// # #[derive(Default)] struct GameAchievement { id: Snowflake, progress: u32, completion: u32 } 22 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 23 | /// # let achievement = GameAchievement::default(); 24 | /// discord.set_user_achievement( 25 | /// achievement.id, 26 | /// (achievement.progress * 100 / achievement.completion) as u8, 27 | /// |discord, result| { 28 | /// if let Err(error) = result { 29 | /// eprintln!("failed setting user achievement: {}", error); 30 | /// } 31 | /// }, 32 | /// ); 33 | /// # Ok(()) } 34 | /// ``` 35 | pub fn set_user_achievement( 36 | &self, 37 | achievement_id: Snowflake, 38 | percent_complete: u8, 39 | callback: impl 'd + FnOnce(&Discord<'d, E>, Result<()>), 40 | ) { 41 | debug_assert!((0..=100).contains(&percent_complete)); 42 | 43 | let (ptr, fun) = self 44 | .one_param(move |discord, res: sys::EDiscordResult| callback(discord, res.to_result())); 45 | 46 | unsafe { 47 | let mgr = self.achievement_manager(); 48 | 49 | (*mgr).set_user_achievement.unwrap()(mgr, achievement_id, percent_complete, ptr, fun); 50 | } 51 | } 52 | 53 | /// Loads the current user's achievements. 54 | /// 55 | /// The user achievements will remain loaded after `callback` returns. 56 | /// 57 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/achievements#fetchuserachievements) 58 | /// 59 | /// ```rust 60 | /// # use discord_game_sdk::*; 61 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 62 | /// discord.fetch_user_achievements( 63 | /// |discord, result| { 64 | /// if let Err(error) = result { 65 | /// return eprintln!("failed fetching user achievements: {}", error); 66 | /// } 67 | /// 68 | /// for achievement in discord.iter_user_achievements() { 69 | /// // ... 70 | /// } 71 | /// }, 72 | /// ); 73 | /// # Ok(()) } 74 | /// ``` 75 | pub fn fetch_user_achievements(&self, callback: impl 'd + FnOnce(&Discord<'d, E>, Result<()>)) { 76 | let (ptr, fun) = self 77 | .one_param(move |discord, res: sys::EDiscordResult| callback(discord, res.to_result())); 78 | 79 | unsafe { 80 | let mgr = self.achievement_manager(); 81 | 82 | (*mgr).fetch_user_achievements.unwrap()(mgr, ptr, fun); 83 | } 84 | } 85 | 86 | /// Gets the user achievement for the given achievement ID. 87 | /// 88 | /// [`fetch_user_achievements`](#method.fetch_user_achievements) must have completed first. 89 | /// 90 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/achievements#getuserachievement) 91 | /// 92 | /// ```rust 93 | /// # use discord_game_sdk::*; 94 | /// # const ACHIEVEMENT_ID: Snowflake = 0; 95 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 96 | /// discord.fetch_user_achievements( 97 | /// |discord, result| { 98 | /// if let Err(error) = result { 99 | /// return eprintln!("failed fetching user achievements: {}", error); 100 | /// } 101 | /// 102 | /// let achievement = discord.user_achievement(ACHIEVEMENT_ID); 103 | /// 104 | /// if let Err(error) = achievement { 105 | /// return eprintln!("failed getting user achievement: {}", error); 106 | /// } 107 | /// }, 108 | /// ); 109 | /// # Ok(()) } 110 | /// ``` 111 | pub fn user_achievement(&self, achievement_id: Snowflake) -> Result { 112 | let mut achievement = UserAchievement(sys::DiscordUserAchievement::default()); 113 | 114 | unsafe { 115 | let mgr = self.achievement_manager(); 116 | 117 | (*mgr).get_user_achievement.unwrap()(mgr, achievement_id, &mut achievement.0) 118 | .to_result()?; 119 | } 120 | 121 | Ok(achievement) 122 | } 123 | 124 | /// Gets the number of user achievements available. 125 | /// 126 | /// Prefer using [`iter_user_achievements`](#method.iter_user_achievements). 127 | /// 128 | /// [`fetch_user_achievements`](#method.fetch_user_achievements) must have completed first. 129 | /// 130 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/achievements#countuserachievements) 131 | pub fn user_achievement_count(&self) -> u32 { 132 | let mut count = 0; 133 | 134 | unsafe { 135 | let mgr = self.achievement_manager(); 136 | 137 | (*mgr).count_user_achievements.unwrap()(mgr, &mut count); 138 | } 139 | 140 | // XXX: i32 should be u32 141 | count.try_into().unwrap() 142 | } 143 | 144 | /// Gets a user achievement by index. 145 | /// 146 | /// Prefer using [`iter_user_achievements`](#method.iter_user_achievements). 147 | /// 148 | /// [`fetch_user_achievements`](#method.fetch_user_achievements) must have completed first. 149 | /// 150 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/achievements#getuserachievementat) 151 | pub fn user_achievement_at(&self, index: u32) -> Result { 152 | let mut achievement = UserAchievement(sys::DiscordUserAchievement::default()); 153 | 154 | unsafe { 155 | let mgr = self.achievement_manager(); 156 | 157 | (*mgr).get_user_achievement_at.unwrap()( 158 | mgr, 159 | // XXX: i32 should be u32 160 | index.try_into().unwrap(), 161 | &mut achievement.0, 162 | ) 163 | .to_result()?; 164 | } 165 | 166 | Ok(achievement) 167 | } 168 | 169 | /// Returns an `Iterator` over all user achievements available. 170 | /// 171 | /// [`fetch_user_achievements`](#method.fetch_user_achievements) must have completed first and must not 172 | /// be called during the iteration. 173 | /// 174 | /// ```rust 175 | /// # use discord_game_sdk::*; 176 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 177 | /// discord.fetch_user_achievements( 178 | /// |discord, result| { 179 | /// if let Err(error) = result { 180 | /// return eprintln!("failed fetching user achievements: {}", error); 181 | /// } 182 | /// 183 | /// for achievement in discord.iter_user_achievements() { 184 | /// // ... 185 | /// } 186 | /// }, 187 | /// ); 188 | /// # Ok(()) } 189 | /// ``` 190 | pub fn iter_user_achievements( 191 | &self, 192 | ) -> impl '_ 193 | + Iterator> 194 | + DoubleEndedIterator 195 | + ExactSizeIterator 196 | + std::iter::FusedIterator 197 | + std::fmt::Debug { 198 | iter::Collection::new( 199 | Box::new(move |i| self.ref_copy().user_achievement_at(i)), 200 | self.user_achievement_count(), 201 | ) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /discord_game_sdk/src/methods/activities.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, to_result::ToResult, Action, Activity, Discord, RequestReply, Result, UserID}; 2 | use std::borrow::Cow; 3 | 4 | /// # Activities 5 | /// 6 | /// Also known as Rich Presence. 7 | /// 8 | /// > [Chapter in official docs](https://discordapp.com/developers/docs/game-sdk/activities) 9 | impl<'d, E> Discord<'d, E> { 10 | /// Registers a command by which Discord can launch your game. 11 | /// 12 | /// This might be a custom protocol, like `my-awesome-game://`, or a path to an executable. 13 | /// It also supports any launch parameters that may be needed, like `game.exe --full-screen`. 14 | /// 15 | /// On macOS, due to the way Discord registers executables, 16 | /// your game needs to be bundled for this command to work. 17 | /// That means it should be a `.app`. 18 | /// 19 | /// ## Performance 20 | /// 21 | /// A nul byte will be appended to `command` if one is not present. 22 | /// 23 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/activities#registercommand) 24 | /// 25 | /// ```rust 26 | /// # use discord_game_sdk::*; 27 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 28 | /// discord.register_launch_command("my-awesome-game://run --full-screen")?; 29 | /// # Ok(()) } 30 | /// ``` 31 | pub fn register_launch_command<'s>(&self, command: impl Into>) -> Result<()> { 32 | let mut command = command.into(); 33 | 34 | if !command.ends_with('\0') { 35 | command.to_mut().push('\0') 36 | } 37 | 38 | unsafe { 39 | let mgr = self.activity_manager(); 40 | 41 | (*mgr).register_command.unwrap()(mgr, command.as_ptr()).to_result() 42 | } 43 | } 44 | 45 | /// Used if you are distributing this SDK on Steam. 46 | /// 47 | /// Registers your game's Steam app ID for the protocol `steam://run-game-id/`. 48 | /// 49 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/activities#registersteam) 50 | /// 51 | /// ```rust 52 | /// # use discord_game_sdk::*; 53 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 54 | /// # let now = 0; 55 | /// discord.clear_activity(|discord, result| { 56 | /// if let Err(error) = result { 57 | /// return eprintln!("failed to clear activity: {}", error); 58 | /// } 59 | /// }); 60 | /// # Ok(()) } 61 | /// ``` 62 | pub fn register_steam(&self, steam_game_id: u32) -> Result<()> { 63 | unsafe { 64 | let mgr = self.activity_manager(); 65 | 66 | (*mgr).register_steam.unwrap()(mgr, steam_game_id).to_result() 67 | } 68 | } 69 | 70 | /// Sets a user's presence in Discord to a new Activity. 71 | /// 72 | /// This has a rate limit of 5 updates per 20 seconds. 73 | /// 74 | /// It is possible for users to hide their presence on Discord (User Settings -> Game Activity). 75 | /// Presence set through this SDK may not be visible when this setting is toggled off. 76 | /// 77 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/activities#updateactivity) 78 | /// 79 | /// ```rust 80 | /// # use discord_game_sdk::*; 81 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 82 | /// # let now = 0; 83 | /// discord.update_activity( 84 | /// &Activity::empty() 85 | /// .with_state("On Main Menu") 86 | /// .with_start_time(now), 87 | /// |discord, result| { 88 | /// if let Err(error) = result { 89 | /// return eprintln!("failed to update activity: {}", error); 90 | /// } 91 | /// }, 92 | /// ); 93 | /// # Ok(()) } 94 | /// ``` 95 | pub fn update_activity( 96 | &self, 97 | activity: &Activity, 98 | callback: impl 'd + FnOnce(&Discord<'d, E>, Result<()>), 99 | ) { 100 | let (ptr, fun) = self 101 | .one_param(move |discord, res: sys::EDiscordResult| callback(discord, res.to_result())); 102 | 103 | unsafe { 104 | let mgr = self.activity_manager(); 105 | 106 | (*mgr).update_activity.unwrap()( 107 | mgr, 108 | // XXX: *mut should be *const 109 | &activity.0 as *const sys::DiscordActivity as *mut sys::DiscordActivity, 110 | ptr, 111 | fun, 112 | ) 113 | } 114 | } 115 | 116 | /// Clears a user's presence in Discord to make it show nothing. 117 | /// 118 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/activities#clearactivity) 119 | /// 120 | /// ```rust 121 | /// # use discord_game_sdk::*; 122 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 123 | /// # let now = 0; 124 | /// discord.clear_activity(|discord, result| { 125 | /// if let Err(error) = result { 126 | /// return eprintln!("failed to clear activity: {}", error); 127 | /// } 128 | /// }); 129 | /// # Ok(()) } 130 | /// ``` 131 | pub fn clear_activity(&self, callback: impl 'd + FnOnce(&Discord<'d, E>, Result<()>)) { 132 | let (ptr, fun) = self 133 | .one_param(move |discord, res: sys::EDiscordResult| callback(discord, res.to_result())); 134 | 135 | unsafe { 136 | let mgr = self.activity_manager(); 137 | 138 | (*mgr).clear_activity.unwrap()(mgr, ptr, fun) 139 | } 140 | } 141 | 142 | /// Sends a reply to an Ask to Join request. 143 | /// 144 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/activities#sendrequestreply) 145 | /// 146 | /// ```rust 147 | /// # use discord_game_sdk::*; 148 | /// struct MyEventHandler; 149 | /// 150 | /// impl EventHandler for MyEventHandler { 151 | /// fn on_activity_join_request(&mut self, discord: &Discord<'_, Self>, user: &User) { 152 | /// println!( 153 | /// "received join request from {}#{}", 154 | /// user.username(), 155 | /// user.discriminator() 156 | /// ); 157 | /// 158 | /// discord.send_request_reply(user.id(), RequestReply::Yes, |discord, result| { 159 | /// if let Err(error) = result { 160 | /// return eprintln!("failed to reply: {}", error); 161 | /// } 162 | /// }); 163 | /// } 164 | /// } 165 | /// ``` 166 | pub fn send_request_reply( 167 | &self, 168 | user_id: UserID, 169 | reply: RequestReply, 170 | callback: impl 'd + FnOnce(&Discord<'d, E>, Result<()>), 171 | ) { 172 | let (ptr, fun) = self 173 | .one_param(move |discord, res: sys::EDiscordResult| callback(discord, res.to_result())); 174 | 175 | unsafe { 176 | let mgr = self.activity_manager(); 177 | 178 | (*mgr).send_request_reply.unwrap()(mgr, user_id, reply.into(), ptr, fun) 179 | } 180 | } 181 | 182 | /// Sends a game invite to a given user. 183 | /// 184 | /// ## Performance 185 | /// 186 | /// A nul byte will be appended to `content` if one is not present. 187 | /// 188 | /// ## Error 189 | /// 190 | /// If the [required fields] are missing, this will return an error. 191 | /// 192 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/activities#sendinvite) 193 | /// 194 | /// [required fields]: struct.Activity.html 195 | /// 196 | /// ```rust 197 | /// # use discord_game_sdk::*; 198 | /// # fn example(discord: Discord<'_, ()>, friend: User) -> Result<()> { 199 | /// discord.send_invite( 200 | /// friend.id(), 201 | /// Action::Join, 202 | /// "Let's play some Survival!\0", 203 | /// |discord, result| { 204 | /// if let Err(error) = result { 205 | /// return eprintln!("failed to invite: {}", error); 206 | /// } 207 | /// }, 208 | /// ); 209 | /// # Ok(()) } 210 | /// ``` 211 | pub fn send_invite<'s>( 212 | &self, 213 | user_id: UserID, 214 | action: Action, 215 | content: impl Into>, 216 | callback: impl 'd + FnOnce(&Discord<'d, E>, Result<()>), 217 | ) { 218 | let mut content = content.into(); 219 | 220 | if !content.ends_with('\0') { 221 | content.to_mut().push('\0') 222 | } 223 | 224 | let (ptr, fun) = self 225 | .one_param(move |discord, res: sys::EDiscordResult| callback(discord, res.to_result())); 226 | 227 | unsafe { 228 | let mgr = self.activity_manager(); 229 | 230 | (*mgr).send_invite.unwrap()(mgr, user_id, action.into(), content.as_ptr(), ptr, fun) 231 | } 232 | } 233 | 234 | /// Accepts a user's game invitation. 235 | /// 236 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/activities#acceptinvite) 237 | /// 238 | /// ```rust 239 | /// # use discord_game_sdk::*; 240 | /// struct MyEventHandler; 241 | /// 242 | /// impl EventHandler for MyEventHandler { 243 | /// fn on_activity_invite( 244 | /// &mut self, 245 | /// discord: &Discord<'_, Self>, 246 | /// action: Action, 247 | /// user: &User, 248 | /// activity: &Activity, 249 | /// ) { 250 | /// println!( 251 | /// "received invitation to {} from {}#{}", 252 | /// if action == Action::Join { 253 | /// "join" 254 | /// } else { 255 | /// "spectate" 256 | /// }, 257 | /// user.username(), 258 | /// user.discriminator() 259 | /// ); 260 | /// 261 | /// discord.accept_invite(user.id(), |discord, result| { 262 | /// if let Err(error) = result { 263 | /// return eprintln!("failed to accept invite: {}", error); 264 | /// } 265 | /// }); 266 | /// } 267 | /// } 268 | /// ``` 269 | pub fn accept_invite( 270 | &self, 271 | user_id: UserID, 272 | callback: impl 'd + FnOnce(&Discord<'d, E>, Result<()>), 273 | ) { 274 | let (ptr, fun) = self 275 | .one_param(move |discord, res: sys::EDiscordResult| callback(discord, res.to_result())); 276 | 277 | unsafe { 278 | let mgr = self.activity_manager(); 279 | 280 | (*mgr).accept_invite.unwrap()(mgr, user_id, ptr, fun) 281 | } 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /discord_game_sdk/src/methods/applications.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, to_result::ToResult, utils, Discord, OAuth2Token, Result}; 2 | use std::mem::size_of; 3 | 4 | /// # Applications 5 | /// 6 | /// Authentication and various helper functions 7 | /// 8 | /// > [Chapter in official docs](https://discordapp.com/developers/docs/game-sdk/applications) 9 | impl<'d, E> Discord<'d, E> { 10 | /// The locale that was set by the current user in their Discord settings. 11 | /// 12 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/applications#getcurrentlocale) 13 | /// 14 | /// ```rust 15 | /// # use discord_game_sdk::*; 16 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 17 | /// println!("current locale is {}", discord.current_locale()); 18 | /// # Ok(()) } 19 | /// ``` 20 | pub fn current_locale(&self) -> String { 21 | let mut locale: sys::DiscordLocale = [0; size_of::()]; 22 | 23 | unsafe { 24 | let mgr = self.application_manager(); 25 | 26 | (*mgr).get_current_locale.unwrap()(mgr, &mut locale) 27 | } 28 | 29 | utils::charbuf_to_str(&locale).to_string() 30 | } 31 | 32 | /// Get the name of pushed branch on which the game is running. 33 | /// 34 | /// These are branches that you created and pushed using 35 | /// [Dispatch](https://discordapp.com/developers/docs/dispatch/dispatch-and-you). 36 | /// 37 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/applications#getcurrentbranch) 38 | /// 39 | /// ```rust 40 | /// # use discord_game_sdk::*; 41 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 42 | /// println!("current branch is {}", discord.current_branch()); 43 | /// # Ok(()) } 44 | /// ``` 45 | pub fn current_branch(&self) -> String { 46 | let mut branch: sys::DiscordBranch = [0; size_of::()]; 47 | 48 | unsafe { 49 | let mgr = self.application_manager(); 50 | 51 | (*mgr).get_current_branch.unwrap()(mgr, &mut branch); 52 | } 53 | 54 | utils::charbuf_to_str(&branch).to_string() 55 | } 56 | 57 | /// Checks if the current user has the entitlement to run this game. 58 | /// 59 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/applications#validateorexit) 60 | /// 61 | /// ```rust 62 | /// # use discord_game_sdk::*; 63 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 64 | /// discord.validate_or_exit(|discord, result| { 65 | /// // ... 66 | /// }); 67 | /// # Ok(()) } 68 | /// ``` 69 | pub fn validate_or_exit(&self, callback: impl 'd + FnOnce(&Discord<'d, E>, Result<()>)) { 70 | let (ptr, fun) = self 71 | .one_param(move |discord, res: sys::EDiscordResult| callback(discord, res.to_result())); 72 | 73 | unsafe { 74 | let mgr = self.application_manager(); 75 | 76 | (*mgr).validate_or_exit.unwrap()(mgr, ptr, fun) 77 | } 78 | } 79 | 80 | /// Retrieve an OAuth 2.0 Bearer token for the current user. 81 | /// 82 | /// If your game was launched from Discord and you call this function, 83 | /// you will automatically receive the token. 84 | /// If the game was not launched from Discord and this method is called, 85 | /// Discord will focus itself and prompt the user for authorization. 86 | /// 87 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/applications#getoauth2token) 88 | /// 89 | /// ```rust 90 | /// # use discord_game_sdk::*; 91 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 92 | /// discord.oauth2_token(|discord, token| { 93 | /// match token { 94 | /// Ok(token) => { 95 | /// //... 96 | /// }, 97 | /// Err(error) => eprintln!("failed to retrieve OAuth2 token: {}", error), 98 | /// } 99 | /// }); 100 | /// # Ok(()) } 101 | /// ``` 102 | pub fn oauth2_token(&self, callback: impl 'd + FnOnce(&Discord<'d, E>, Result<&OAuth2Token>)) { 103 | let (ptr, fun) = self.two_params( 104 | move |discord, res: sys::EDiscordResult, token: *mut sys::DiscordOAuth2Token| { 105 | callback( 106 | discord, 107 | res.to_result() 108 | .map(|()| unsafe { &*(token as *mut OAuth2Token) }), 109 | ) 110 | }, 111 | ); 112 | 113 | unsafe { 114 | let mgr = self.application_manager(); 115 | 116 | (*mgr).get_oauth2_token.unwrap()(mgr, ptr, fun) 117 | } 118 | } 119 | 120 | /// Get the signed app ticket for the current user. 121 | /// 122 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/applications#getticket) 123 | /// 124 | /// ```rust 125 | /// # use discord_game_sdk::*; 126 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 127 | /// discord.app_ticket(|discord, ticket| { 128 | /// match ticket { 129 | /// Ok(ticket) => { 130 | /// //... 131 | /// }, 132 | /// Err(error) => eprintln!("failed to retrieve signed app ticket: {}", error), 133 | /// } 134 | /// }); 135 | /// # Ok(()) } 136 | /// ``` 137 | pub fn app_ticket(&self, callback: impl 'd + FnOnce(&Discord<'d, E>, Result<&str>)) { 138 | let (ptr, fun) = self.two_params( 139 | move |discord, res: sys::EDiscordResult, string: *const u8| { 140 | callback( 141 | discord, 142 | res.to_result() 143 | .map(|()| unsafe { utils::charptr_to_str(string) }), 144 | ) 145 | }, 146 | ); 147 | 148 | unsafe { 149 | let mgr = self.application_manager(); 150 | 151 | (*mgr).get_ticket.unwrap()(mgr, ptr, fun) 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /discord_game_sdk/src/methods/callback.rs: -------------------------------------------------------------------------------- 1 | use crate::{utils, Discord}; 2 | use std::{ffi::c_void, panic::UnwindSafe}; 3 | 4 | impl<'d, E> Discord<'d, E> { 5 | pub(crate) fn one_param( 6 | &self, 7 | callback: impl 'd + FnOnce(&Discord<'d, E>, A), 8 | ) -> (*mut c_void, Option) { 9 | extern "C" fn one_param_from_c(ptr: *mut c_void, a: A) { 10 | utils::abort_on_panic(|| { 11 | // SAFETY: 12 | // lifetime of F was ellided when it was turned into a raw pointer 13 | // in all method calls, F is bound to 'd, which is the lifetime of the Discord instance 14 | // this is a bit risky, but it seems like the SDK will send `Err(TransactionAborted)` to 15 | // all waiting callbacks if we destroy the instance, we're relying on this behavior 16 | let callback = unsafe { Box::from_raw(ptr as *mut F) }; 17 | callback(a) 18 | }) 19 | } 20 | 21 | fn one_param_align_types( 22 | callback: F, 23 | ) -> (*mut c_void, Option) { 24 | ( 25 | Box::into_raw(Box::new(callback)) as *mut _, 26 | Some(one_param_from_c::), 27 | ) 28 | } 29 | 30 | let dref = self.ref_copy(); 31 | one_param_align_types(move |a| callback(&*dref, a)) 32 | } 33 | 34 | pub(crate) fn two_params( 35 | &self, 36 | callback: impl 'd + FnOnce(&Discord<'d, E>, A, B), 37 | ) -> (*mut c_void, Option) { 38 | extern "C" fn two_params_from_c( 39 | ptr: *mut c_void, 40 | a: A, 41 | b: B, 42 | ) { 43 | utils::abort_on_panic(|| { 44 | // SAFETY: see `one_param` 45 | let callback = unsafe { Box::from_raw(ptr as *mut F) }; 46 | callback(a, b) 47 | }) 48 | } 49 | 50 | fn two_params_align_types( 51 | callback: F, 52 | ) -> (*mut c_void, Option) { 53 | ( 54 | Box::into_raw(Box::new(callback)) as *mut _, 55 | Some(two_params_from_c::), 56 | ) 57 | } 58 | 59 | let dref = self.ref_copy(); 60 | two_params_align_types(move |a, b| callback(&*dref, a, b)) 61 | } 62 | 63 | pub(crate) fn three_params( 64 | &self, 65 | callback: impl 'd + FnOnce(&Discord<'d, E>, A, B, C), 66 | ) -> ( 67 | *mut c_void, 68 | Option, 69 | ) { 70 | extern "C" fn three_params_from_c< 71 | F: FnOnce(A, B, C), 72 | A: UnwindSafe, 73 | B: UnwindSafe, 74 | C: UnwindSafe, 75 | >( 76 | ptr: *mut c_void, 77 | a: A, 78 | b: B, 79 | c: C, 80 | ) { 81 | utils::abort_on_panic(|| { 82 | // SAFETY: see `one_param` 83 | let callback = unsafe { Box::from_raw(ptr as *mut F) }; 84 | callback(a, b, c) 85 | }) 86 | } 87 | 88 | fn three_params_align_types< 89 | F: FnOnce(A, B, C), 90 | A: UnwindSafe, 91 | B: UnwindSafe, 92 | C: UnwindSafe, 93 | >( 94 | callback: F, 95 | ) -> ( 96 | *mut c_void, 97 | Option, 98 | ) { 99 | ( 100 | Box::into_raw(Box::new(callback)) as *mut _, 101 | Some(three_params_from_c::), 102 | ) 103 | } 104 | 105 | let dref = self.ref_copy(); 106 | three_params_align_types(move |a, b, c| callback(&*dref, a, b, c)) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /discord_game_sdk/src/methods/core.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | discord::{Discord, DiscordInner}, 3 | events, sys, 4 | to_result::ToResult, 5 | utils, ClientID, CreateFlags, EventHandler, Result, 6 | }; 7 | use std::{cell::UnsafeCell, convert::TryFrom, marker::PhantomData}; 8 | 9 | /// # Core 10 | /// 11 | /// > [Chapter in official docs](https://discordapp.com/developers/docs/game-sdk/discord) 12 | /// 13 | /// ```rust 14 | /// # fn example() { 15 | /// use discord_game_sdk::Discord; 16 | /// 17 | /// # const DISCORD_CLIENT_ID: discord_game_sdk::ClientID = 0; 18 | /// # #[derive(Debug, Default)] struct MyEventHandler; 19 | /// # impl discord_game_sdk::EventHandler for MyEventHandler {} 20 | /// 21 | /// fn main() -> Result<(), Box> { 22 | /// let mut discord = Discord::new(DISCORD_CLIENT_ID)?; 23 | /// *discord.event_handler_mut() = Some(MyEventHandler::default()); 24 | /// 25 | /// loop { 26 | /// discord.run_callbacks()?; 27 | /// } 28 | /// 29 | /// Ok(()) 30 | /// } 31 | /// # } 32 | /// ``` 33 | impl Discord<'_, E> { 34 | /// Calls [`with_create_flags`] with [`CreateFlags::Default`]. 35 | /// 36 | /// [`with_create_flags`]: #method.with_create_flags 37 | /// [`CreateFlags::Default`]: enum.CreateFlags.html#variant.Default 38 | pub fn new(client_id: ClientID) -> Result 39 | where 40 | E: EventHandler, 41 | { 42 | Self::with_create_flags(client_id, CreateFlags::Default) 43 | } 44 | 45 | /// Creates an instance of the main interface with the Discord Game SDK. 46 | /// 47 | /// SDK log messages are forwarded to [`log`](https://docs.rs/log) 48 | /// 49 | /// > [`Create` in official docs](https://discordapp.com/developers/docs/game-sdk/discord#create) 50 | /// > [`SetLogHook` in official docs](https://discordapp.com/developers/docs/game-sdk/discord#setloghook) 51 | pub fn with_create_flags(client_id: ClientID, flags: CreateFlags) -> Result 52 | where 53 | E: EventHandler, 54 | { 55 | // This is a mess 56 | // 57 | // - We want to call `sys::DiscordCreate`, it gives us a `*mut sys::IDiscordCore` 58 | // - We provide `&mut EventHandler` and `&Discord` during event handlers 59 | // - That means we need to mutate `DiscordInner::event_handler`, which is fine via `UnsafeCell` 60 | // - That also means we need to duplicate the `*mut DiscordInner` 61 | // - `sys::DiscordCreate` wants `sys::DiscordCreateParams` + `*mut *mut sys::IDiscordCore` 62 | // - `sys::DiscordCreateParams` wants our `event_data: *mut c_void` 63 | // - Our `event_data` is `*mut DiscordInner` 64 | // - We need to build the `Discord` first to pass a valid pointer 65 | 66 | log::debug!("instantiating with client ID {}", client_id); 67 | 68 | let mut instance = Discord(Box::into_raw(Box::new(DiscordInner { 69 | _invariant_lifetime: PhantomData, 70 | 71 | // SAFETY: overwritten by `sys::DiscordCreate`, not deref'd until then 72 | core: std::ptr::null_mut(), 73 | client_id, 74 | event_handler: UnsafeCell::new(None), 75 | 76 | achievement_events: events::achievement::(), 77 | activity_events: events::activity::(), 78 | lobby_events: events::lobby::(), 79 | network_events: events::network::(), 80 | overlay_events: events::overlay::(), 81 | relationship_events: events::relationship::(), 82 | store_events: events::store::(), 83 | user_events: events::user::(), 84 | voice_events: events::voice::(), 85 | }))); 86 | 87 | let mut params = instance.create_params(flags.into()); 88 | 89 | unsafe { 90 | sys::DiscordCreate( 91 | sys::DISCORD_VERSION, 92 | &mut params, 93 | &mut instance.inner_mut().core, 94 | ) 95 | .to_result()?; 96 | } 97 | 98 | log::trace!("received pointer to {:p}", instance.inner().core); 99 | 100 | instance.set_log_hook(); 101 | instance.kickstart_managers(); 102 | 103 | Ok(instance) 104 | } 105 | 106 | pub(crate) fn create_params( 107 | &self, 108 | flags: sys::EDiscordCreateFlags, 109 | ) -> sys::DiscordCreateParams { 110 | sys::DiscordCreateParams { 111 | client_id: self.client_id(), 112 | flags: u64::try_from(flags).unwrap(), 113 | 114 | events: std::ptr::null_mut(), 115 | event_data: self.0 as *mut std::ffi::c_void, 116 | 117 | // SAFETY: pointers are safe 118 | // they last until `DiscordInner` is dropped, 119 | // and the SDK won't dereference them after that 120 | achievement_events: &self.inner().achievement_events as *const _ as *mut _, 121 | achievement_version: sys::DISCORD_ACHIEVEMENT_MANAGER_VERSION, 122 | 123 | activity_events: &self.inner().activity_events as *const _ as *mut _, 124 | activity_version: sys::DISCORD_ACTIVITY_MANAGER_VERSION, 125 | 126 | application_events: std::ptr::null_mut(), 127 | application_version: sys::DISCORD_APPLICATION_MANAGER_VERSION, 128 | 129 | image_events: std::ptr::null_mut(), 130 | image_version: sys::DISCORD_IMAGE_MANAGER_VERSION, 131 | 132 | lobby_events: &self.inner().lobby_events as *const _ as *mut _, 133 | lobby_version: sys::DISCORD_LOBBY_MANAGER_VERSION, 134 | 135 | network_events: &self.inner().network_events as *const _ as *mut _, 136 | network_version: sys::DISCORD_NETWORK_MANAGER_VERSION, 137 | 138 | overlay_events: &self.inner().overlay_events as *const _ as *mut _, 139 | overlay_version: sys::DISCORD_OVERLAY_MANAGER_VERSION, 140 | 141 | relationship_events: &self.inner().relationship_events as *const _ as *mut _, 142 | relationship_version: sys::DISCORD_RELATIONSHIP_MANAGER_VERSION, 143 | 144 | storage_events: std::ptr::null_mut(), 145 | storage_version: sys::DISCORD_STORAGE_MANAGER_VERSION, 146 | 147 | store_events: &self.inner().store_events as *const _ as *mut _, 148 | store_version: sys::DISCORD_STORE_MANAGER_VERSION, 149 | 150 | user_events: &self.inner().user_events as *const _ as *mut _, 151 | user_version: sys::DISCORD_USER_MANAGER_VERSION, 152 | 153 | voice_events: &self.inner().voice_events as *const _ as *mut _, 154 | voice_version: sys::DISCORD_VOICE_MANAGER_VERSION, 155 | } 156 | } 157 | 158 | fn set_log_hook(&self) { 159 | extern "C" fn log_hook( 160 | _: *mut std::ffi::c_void, 161 | level: sys::EDiscordLogLevel, 162 | message: *const u8, 163 | ) { 164 | utils::abort_on_panic(|| { 165 | let level = match level { 166 | sys::DiscordLogLevel_Error => log::Level::Error, 167 | sys::DiscordLogLevel_Warn => log::Level::Warn, 168 | sys::DiscordLogLevel_Info => log::Level::Info, 169 | sys::DiscordLogLevel_Debug => log::Level::Debug, 170 | _ => log::Level::Trace, 171 | }; 172 | 173 | log::log!(level, "SDK: {}", unsafe { utils::charptr_to_str(message) }); 174 | }) 175 | } 176 | 177 | unsafe { 178 | (*self.inner().core).set_log_hook.unwrap()( 179 | self.inner().core, 180 | sys::DiscordLogLevel_Debug, 181 | // SAFETY: this is never used 182 | std::ptr::null_mut(), 183 | Some(log_hook), 184 | ); 185 | } 186 | } 187 | 188 | // To start producing events, the SDK must initialize the related manager 189 | // We initialize all managers that produce events to kickstart event passing 190 | fn kickstart_managers(&self) { 191 | unsafe { 192 | self.achievement_manager(); 193 | self.activity_manager(); 194 | self.lobby_manager(); 195 | self.network_manager(); 196 | self.overlay_manager(); 197 | self.relationship_manager(); 198 | self.store_manager(); 199 | self.user_manager(); 200 | self.voice_manager(); 201 | } 202 | } 203 | 204 | /// Runs all pending SDK callbacks. 205 | /// 206 | /// This should be called often, like in the main loop if you're writing a game. 207 | /// 208 | /// Make sure to overwrite the [`EventHandler`](trait.EventHandler.html) 209 | /// (with [`event_handler_mut`](#method.event_handler_mut)) 210 | /// before calling this method. 211 | /// 212 | /// ## Errors 213 | /// 214 | /// If the Discord client was closed, [`Error::NotRunning`](enum.Error.html#variant.NotRunning) will be returned. 215 | /// 216 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/discord#runcallbacks) 217 | // We require &mut self to prevent calling during callbacks 218 | pub fn run_callbacks(&mut self) -> Result<()> { 219 | unsafe { (*self.inner().core).run_callbacks.unwrap()(self.inner().core).to_result() } 220 | } 221 | 222 | pub(crate) unsafe fn achievement_manager(&self) -> *mut sys::IDiscordAchievementManager { 223 | (*self.inner().core).get_achievement_manager.unwrap()(self.inner().core) 224 | } 225 | 226 | pub(crate) unsafe fn activity_manager(&self) -> *mut sys::IDiscordActivityManager { 227 | (*self.inner().core).get_activity_manager.unwrap()(self.inner().core) 228 | } 229 | 230 | pub(crate) unsafe fn application_manager(&self) -> *mut sys::IDiscordApplicationManager { 231 | (*self.inner().core).get_application_manager.unwrap()(self.inner().core) 232 | } 233 | 234 | pub(crate) unsafe fn image_manager(&self) -> *mut sys::IDiscordImageManager { 235 | (*self.inner().core).get_image_manager.unwrap()(self.inner().core) 236 | } 237 | 238 | pub(crate) unsafe fn lobby_manager(&self) -> *mut sys::IDiscordLobbyManager { 239 | (*self.inner().core).get_lobby_manager.unwrap()(self.inner().core) 240 | } 241 | 242 | pub(crate) unsafe fn network_manager(&self) -> *mut sys::IDiscordNetworkManager { 243 | (*self.inner().core).get_network_manager.unwrap()(self.inner().core) 244 | } 245 | 246 | pub(crate) unsafe fn overlay_manager(&self) -> *mut sys::IDiscordOverlayManager { 247 | (*self.inner().core).get_overlay_manager.unwrap()(self.inner().core) 248 | } 249 | 250 | pub(crate) unsafe fn relationship_manager(&self) -> *mut sys::IDiscordRelationshipManager { 251 | (*self.inner().core).get_relationship_manager.unwrap()(self.inner().core) 252 | } 253 | 254 | pub(crate) unsafe fn storage_manager(&self) -> *mut sys::IDiscordStorageManager { 255 | (*self.inner().core).get_storage_manager.unwrap()(self.inner().core) 256 | } 257 | 258 | pub(crate) unsafe fn store_manager(&self) -> *mut sys::IDiscordStoreManager { 259 | (*self.inner().core).get_store_manager.unwrap()(self.inner().core) 260 | } 261 | 262 | pub(crate) unsafe fn user_manager(&self) -> *mut sys::IDiscordUserManager { 263 | (*self.inner().core).get_user_manager.unwrap()(self.inner().core) 264 | } 265 | 266 | pub(crate) unsafe fn voice_manager(&self) -> *mut sys::IDiscordVoiceManager { 267 | (*self.inner().core).get_voice_manager.unwrap()(self.inner().core) 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /discord_game_sdk/src/methods/images.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, to_result::ToResult, Discord, FetchKind, Image, ImageHandle, Result}; 2 | use std::convert::{TryFrom, TryInto}; 3 | 4 | /// # Images 5 | /// 6 | /// > [Chapter in official docs](https://discordapp.com/developers/docs/game-sdk/images) 7 | /// 8 | /// ```rust 9 | /// # use discord_game_sdk::*; 10 | /// # fn example(discord: Discord<'_, ()>, user: User) -> Result<()> { 11 | /// discord.fetch_image( 12 | /// ImageHandle::from_user_id(user.id(), 128), 13 | /// FetchKind::UseCached, 14 | /// |discord, handle| { 15 | /// match handle.and_then(|handle| discord.image(handle)) { 16 | /// Ok(image) => { 17 | /// println!("image dimensions: {:?}", image.dimensions()); 18 | /// // ... 19 | /// }, 20 | /// Err(error) => eprintln!("failed to fetch image: {}", error), 21 | /// } 22 | /// }, 23 | /// ); 24 | /// # Ok(()) } 25 | /// ``` 26 | impl<'d, E> Discord<'d, E> { 27 | /// Prepares an image. 28 | /// 29 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/images#fetch) 30 | pub fn fetch_image( 31 | &self, 32 | handle: ImageHandle, 33 | refresh: FetchKind, 34 | callback: impl 'd + FnOnce(&Discord<'d, E>, Result), 35 | ) { 36 | let (ptr, fun) = self.two_params( 37 | move |discord, res: sys::EDiscordResult, image_handle: sys::DiscordImageHandle| { 38 | callback(discord, res.to_result().map(|()| ImageHandle(image_handle))) 39 | }, 40 | ); 41 | 42 | unsafe { 43 | let mgr = self.image_manager(); 44 | 45 | (*mgr).fetch.unwrap()(mgr, handle.0, refresh.into(), ptr, fun) 46 | } 47 | } 48 | 49 | /// Get's the dimensions of the source image. 50 | /// 51 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/images#getdimensions) 52 | pub fn image_dimensions(&self, handle: ImageHandle) -> Result<(u32, u32)> { 53 | let mut dimensions = sys::DiscordImageDimensions::default(); 54 | 55 | unsafe { 56 | let mgr = self.image_manager(); 57 | 58 | (*mgr).get_dimensions.unwrap()(mgr, handle.0, &mut dimensions).to_result()?; 59 | } 60 | 61 | Ok((dimensions.width, dimensions.height)) 62 | } 63 | 64 | /// Retrieves the data for an image. 65 | /// 66 | /// The image must be [fetched](#method.fetch_image) first. 67 | /// 68 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/images#getdata) 69 | pub fn image(&self, handle: ImageHandle) -> Result { 70 | let (width, height) = self.image_dimensions(handle.clone())?; 71 | let mut data = vec![0; 4 * width as usize * height as usize]; 72 | 73 | debug_assert!(u32::try_from(data.len()).is_ok()); 74 | 75 | unsafe { 76 | let mgr = self.image_manager(); 77 | 78 | (*mgr).get_data.unwrap()( 79 | mgr, 80 | handle.0, 81 | data.as_mut_ptr(), 82 | data.len().try_into().unwrap_or(u32::max_value()), 83 | ) 84 | .to_result()?; 85 | } 86 | 87 | Ok(Image { 88 | width, 89 | height, 90 | data, 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /discord_game_sdk/src/methods/networking.rs: -------------------------------------------------------------------------------- 1 | use crate::{to_result::ToResult, Discord, NetworkChannelID, NetworkPeerID, Reliability, Result}; 2 | use std::{ 3 | borrow::Cow, 4 | convert::{TryFrom, TryInto}, 5 | }; 6 | 7 | /// # Networking 8 | /// 9 | /// Lower level networking functionality. 10 | /// 11 | /// > [Chapter in official docs](https://discordapp.com/developers/docs/game-sdk/networking) 12 | impl Discord<'_, E> { 13 | /// Get the networking peer ID for the current user, allowing other users to send packets to them. 14 | /// 15 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/networking#getpeerid) 16 | pub fn peer_id(&self) -> NetworkPeerID { 17 | let mut peer_id = 0; 18 | 19 | unsafe { 20 | let mgr = self.network_manager(); 21 | 22 | (*mgr).get_peer_id.unwrap()(mgr, &mut peer_id) 23 | } 24 | 25 | peer_id 26 | } 27 | 28 | /// Flushes the network. Run this near the end of your game's loop, 29 | /// once you've finished sending all you need to send. 30 | /// 31 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/networking#flush) 32 | pub fn flush_network(&self) -> Result<()> { 33 | unsafe { 34 | let mgr = self.network_manager(); 35 | 36 | (*mgr).flush.unwrap()(mgr).to_result() 37 | } 38 | } 39 | 40 | /// Opens a network connection to another Discord user. 41 | /// 42 | /// ## Performance 43 | /// 44 | /// A nul byte will be appended to `route` if one is not present. 45 | /// 46 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/networking#openpeer) 47 | pub fn open_peer<'s>( 48 | &self, 49 | peer_id: NetworkPeerID, 50 | route: impl Into>, 51 | ) -> Result<()> { 52 | let mut route = route.into(); 53 | 54 | if !route.ends_with('\0') { 55 | route.to_mut().push('\0') 56 | } 57 | 58 | unsafe { 59 | let mgr = self.network_manager(); 60 | 61 | (*mgr).open_peer.unwrap()(mgr, peer_id, route.as_ptr()).to_result() 62 | } 63 | } 64 | 65 | /// Updates the network connection to another Discord user. 66 | /// 67 | /// You'll want to call this when notified that the route to another user has changed, 68 | /// most likely from a lobby member update event. 69 | /// 70 | /// ## Performance 71 | /// 72 | /// A nul byte will be appended to `route` if one is not present. 73 | /// 74 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/networking#updatepeer) 75 | pub fn update_peer<'s>( 76 | &self, 77 | peer_id: NetworkPeerID, 78 | route: impl Into>, 79 | ) -> Result<()> { 80 | let mut route = route.into(); 81 | 82 | if !route.ends_with('\0') { 83 | route.to_mut().push('\0') 84 | } 85 | 86 | unsafe { 87 | let mgr = self.network_manager(); 88 | 89 | (*mgr).update_peer.unwrap()(mgr, peer_id, route.as_ptr()).to_result() 90 | } 91 | } 92 | 93 | /// Disconnects the network session to another Discord user. 94 | /// 95 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/networking#closepeer) 96 | pub fn close_peer(&self, peer_id: NetworkPeerID) -> Result<()> { 97 | unsafe { 98 | let mgr = self.network_manager(); 99 | 100 | (*mgr).close_peer.unwrap()(mgr, peer_id).to_result() 101 | } 102 | } 103 | 104 | /// Opens a network connection to another Discord user. 105 | /// 106 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/networking#openchannel) 107 | pub fn open_channel( 108 | &self, 109 | peer_id: NetworkPeerID, 110 | channel_id: NetworkChannelID, 111 | reliable: Reliability, 112 | ) -> Result<()> { 113 | unsafe { 114 | let mgr = self.network_manager(); 115 | 116 | (*mgr).open_channel.unwrap()(mgr, peer_id, channel_id, reliable.into()).to_result() 117 | } 118 | } 119 | 120 | /// Close the connection to a given user by peer ID on the given channel. 121 | /// 122 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/networking#closechannel) 123 | pub fn close_channel( 124 | &self, 125 | peer_id: NetworkPeerID, 126 | channel_id: NetworkChannelID, 127 | ) -> Result<()> { 128 | unsafe { 129 | let mgr = self.network_manager(); 130 | 131 | (*mgr).close_channel.unwrap()(mgr, peer_id, channel_id).to_result() 132 | } 133 | } 134 | 135 | /// Sends data to a given peer ID through the given channel. 136 | /// 137 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/networking#sendmessage) 138 | pub fn send_message( 139 | &self, 140 | peer_id: NetworkPeerID, 141 | channel_id: NetworkChannelID, 142 | buffer: impl AsRef<[u8]>, 143 | ) -> Result<()> { 144 | let buffer = buffer.as_ref(); 145 | 146 | debug_assert!(u32::try_from(buffer.len()).is_ok()); 147 | 148 | unsafe { 149 | let mgr = self.network_manager(); 150 | 151 | (*mgr).send_message.unwrap()( 152 | mgr, 153 | peer_id, 154 | channel_id, 155 | // XXX: *mut should be *const 156 | buffer.as_ptr() as *mut u8, 157 | // XXX: u32 should be u64 158 | buffer.len().try_into().unwrap_or(u32::max_value()), 159 | ) 160 | .to_result() 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /discord_game_sdk/src/methods/overlay.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, to_result::ToResult, Action, Discord, Result}; 2 | use std::borrow::Cow; 3 | 4 | /// # Overlay 5 | /// 6 | /// The terminology employed by the Game SDK is confusing, this crate employs the terms "opened" 7 | /// and "closed" instead: 8 | /// 9 | /// | | Game SDK | `discord_game_sdk` | 10 | /// |------------------------------------------|----------|--------------------| 11 | /// | Overlay is appearing and has taken focus | unlocked | opened | 12 | /// | Overlay is hidden | locked | closed | 13 | /// 14 | /// > [Chapter in official docs](https://discordapp.com/developers/docs/game-sdk/overlay) 15 | impl<'d, E> Discord<'d, E> { 16 | /// Check whether the user has the overlay enabled or disabled. 17 | /// 18 | /// If the overlay is disabled, all the functionality in this manager will still work. 19 | /// The calls will instead focus the Discord client and show the modal there instead. 20 | /// 21 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/overlay#isenabled) 22 | /// 23 | /// ```rust 24 | /// # use discord_game_sdk::*; 25 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 26 | /// if discord.overlay_enabled() { 27 | /// // ... 28 | /// } 29 | /// # Ok(()) } 30 | /// ``` 31 | pub fn overlay_enabled(&self) -> bool { 32 | let mut enabled = false; 33 | 34 | unsafe { 35 | let mgr = self.overlay_manager(); 36 | 37 | (*mgr).is_enabled.unwrap()(mgr, &mut enabled) 38 | } 39 | 40 | enabled 41 | } 42 | 43 | /// Whether the overlay is appearing and has taken focus. 44 | /// 45 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/overlay#islocked) 46 | /// 47 | /// ```rust 48 | /// # use discord_game_sdk::*; 49 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 50 | /// if discord.overlay_opened() { 51 | /// // ... 52 | /// } 53 | /// # Ok(()) } 54 | /// ``` 55 | pub fn overlay_opened(&self) -> bool { 56 | let mut locked = false; 57 | 58 | unsafe { 59 | let mgr = self.overlay_manager(); 60 | 61 | (*mgr).is_locked.unwrap()(mgr, &mut locked) 62 | } 63 | 64 | !locked 65 | } 66 | 67 | /// Open or close the overlay. 68 | /// 69 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/overlay#setlocked) 70 | /// 71 | /// ```rust 72 | /// # use discord_game_sdk::*; 73 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 74 | /// discord.set_overlay_opened(false, |discord, result| { 75 | /// if let Err(error) = result { 76 | /// return eprintln!("failed to set overlay open: {}", error); 77 | /// } 78 | /// }); 79 | /// # Ok(()) } 80 | /// ``` 81 | pub fn set_overlay_opened( 82 | &self, 83 | opened: bool, 84 | callback: impl 'd + FnOnce(&Discord<'d, E>, Result<()>), 85 | ) { 86 | let (ptr, fun) = self 87 | .one_param(move |discord, res: sys::EDiscordResult| callback(discord, res.to_result())); 88 | 89 | unsafe { 90 | let mgr = self.overlay_manager(); 91 | 92 | (*mgr).set_locked.unwrap()(mgr, !opened, ptr, fun) 93 | } 94 | } 95 | 96 | /// Opens the overlay modal for sending game invitations to users, channels, and servers. 97 | /// If you do not have a valid activity with all the required fields, this call will error. 98 | /// 99 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/overlay#openactivityinvite) 100 | /// 101 | /// ```rust 102 | /// # use discord_game_sdk::*; 103 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 104 | /// discord.open_invite_overlay(Action::Join, |discord, result| { 105 | /// if let Err(error) = result { 106 | /// return eprintln!("failed open invite overlay: {}", error); 107 | /// } 108 | /// }); 109 | /// # Ok(()) } 110 | /// ``` 111 | pub fn open_invite_overlay( 112 | &self, 113 | action: Action, 114 | callback: impl 'd + FnOnce(&Discord<'d, E>, Result<()>), 115 | ) { 116 | let (ptr, fun) = self 117 | .one_param(move |discord, res: sys::EDiscordResult| callback(discord, res.to_result())); 118 | 119 | unsafe { 120 | let mgr = self.overlay_manager(); 121 | 122 | (*mgr).open_activity_invite.unwrap()(mgr, action.into(), ptr, fun) 123 | } 124 | } 125 | 126 | /// Opens the overlay modal for joining a Discord guild, given its invite code 127 | /// (e.g.: `ABCDEF` in `https://discord.gg/ABCDEF` or `https://discordapp.com/invite/ABCDEF`). 128 | /// 129 | /// Receiving `Ok(())` does not necessarily mean that the user has joined the guild. 130 | /// 131 | /// ## Performance 132 | /// 133 | /// A nul byte will be appended to `code` if one is not present. 134 | /// 135 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/overlay#openguildinvite) 136 | /// 137 | /// ```rust 138 | /// # use discord_game_sdk::*; 139 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 140 | /// discord.open_guild_invite_overlay("discord-gamesdk\0", |discord, result| { 141 | /// if let Err(error) = result { 142 | /// return eprintln!("failed open guild invite overlay: {}", error); 143 | /// } 144 | /// }); 145 | /// # Ok(()) } 146 | /// ``` 147 | pub fn open_guild_invite_overlay<'s>( 148 | &self, 149 | code: impl Into>, 150 | callback: impl 'd + FnOnce(&Discord<'d, E>, Result<()>), 151 | ) { 152 | let mut code = code.into(); 153 | 154 | if !code.ends_with('\0') { 155 | code.to_mut().push('\0') 156 | } 157 | 158 | let (ptr, fun) = self 159 | .one_param(move |discord, res: sys::EDiscordResult| callback(discord, res.to_result())); 160 | 161 | unsafe { 162 | let mgr = self.overlay_manager(); 163 | 164 | (*mgr).open_guild_invite.unwrap()(mgr, code.as_ptr(), ptr, fun) 165 | } 166 | } 167 | 168 | /// Opens the overlay widget for voice settings for the currently connected application. 169 | /// These settings are unique to each user within the context of your application. 170 | /// That means that a user can have different favorite voice settings for each of their games. 171 | /// 172 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/overlay#openvoicesettings) 173 | /// 174 | /// ```rust 175 | /// # use discord_game_sdk::*; 176 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 177 | /// discord.open_voice_settings(|discord, result| { 178 | /// if let Err(error) = result { 179 | /// return eprintln!("failed open voice settings overlay: {}", error); 180 | /// } 181 | /// }); 182 | /// # Ok(()) } 183 | /// ``` 184 | pub fn open_voice_settings(&self, callback: impl 'd + FnOnce(&Discord<'d, E>, Result<()>)) { 185 | let (ptr, fun) = self 186 | .one_param(move |discord, res: sys::EDiscordResult| callback(discord, res.to_result())); 187 | 188 | unsafe { 189 | let mgr = self.overlay_manager(); 190 | 191 | (*mgr).open_voice_settings.unwrap()(mgr, ptr, fun) 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /discord_game_sdk/src/methods/relationships.rs: -------------------------------------------------------------------------------- 1 | use crate::{iter, sys, to_result::ToResult, utils, Discord, Relationship, Result, UserID}; 2 | use std::convert::TryInto; 3 | 4 | /// # Relationships 5 | /// 6 | /// > [Chapter in official docs](https://discordapp.com/developers/docs/game-sdk/relationships) 7 | impl Discord<'_, E> { 8 | /// Get the relationship between the current user and a given user by ID. 9 | /// 10 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/relationships#get) 11 | /// 12 | /// ```rust 13 | /// # use discord_game_sdk::*; 14 | /// # fn example(discord: Discord<'_, ()>, user: User) -> Result<()> { 15 | /// let relationship = discord.relationship_with(user.id())?; 16 | /// # Ok(()) } 17 | /// ``` 18 | pub fn relationship_with(&self, user_id: UserID) -> Result { 19 | let mut relationship = Relationship(sys::DiscordRelationship::default()); 20 | 21 | unsafe { 22 | let mgr = self.relationship_manager(); 23 | 24 | (*mgr).get.unwrap()(mgr, user_id, &mut relationship.0).to_result()?; 25 | } 26 | 27 | Ok(relationship) 28 | } 29 | 30 | /// Filter all relationships by a given predicate. 31 | /// 32 | /// [`RelationshipsRefreshed`](event/relationships/struct.Refresh.html) 33 | /// must have fired first. 34 | /// 35 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/relationships#filter) 36 | /// 37 | /// ```rust 38 | /// # use discord_game_sdk::*; 39 | /// # const DISCORD_CLIENT_ID: ClientID = 0; 40 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 41 | /// discord.filter_relationships(|relationship| { 42 | /// relationship.presence().activity().application_id() == DISCORD_CLIENT_ID 43 | /// }); 44 | /// # Ok(()) } 45 | /// ``` 46 | pub fn filter_relationships bool>(&self, mut filter: F) { 47 | unsafe extern "C" fn filter_relationship( 48 | callback_ptr: *mut std::ffi::c_void, 49 | relationship_ptr: *mut sys::DiscordRelationship, 50 | ) -> bool 51 | where 52 | F: FnMut(&Relationship) -> bool, 53 | { 54 | utils::abort_on_panic(|| { 55 | (*(callback_ptr as *mut F))(&*(relationship_ptr as *const Relationship)) 56 | }) 57 | } 58 | 59 | unsafe { 60 | let mgr = self.relationship_manager(); 61 | 62 | (*mgr).filter.unwrap()( 63 | mgr, 64 | &mut filter as *mut F as *mut std::ffi::c_void, 65 | Some(filter_relationship::), 66 | ) 67 | } 68 | } 69 | 70 | /// Returns the number of relationships matching the filter. 71 | /// 72 | /// [`RelationshipsRefreshed`](event/relationships/struct.Refresh.html) 73 | /// must have fired first. 74 | /// 75 | /// Prefer using [`iter_relationships`](#method.iter_relationships). 76 | /// 77 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/relationships#count) 78 | pub fn relationship_count(&self) -> Result { 79 | let mut count = 0; 80 | 81 | unsafe { 82 | let mgr = self.relationship_manager(); 83 | 84 | (*mgr).count.unwrap()(mgr, &mut count).to_result()?; 85 | } 86 | 87 | // XXX: i32 should be u32 88 | Ok(count.try_into().unwrap()) 89 | } 90 | 91 | /// Returns the relationship matching the filter at a given index. 92 | /// 93 | /// [`RelationshipsRefreshed`](event/relationships/struct.Refresh.html) 94 | /// must have fired first. 95 | /// 96 | /// Prefer using [`iter_relationships`](#method.iter_relationships). 97 | /// 98 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/relationships#getat) 99 | pub fn relationship_at(&self, index: u32) -> Result { 100 | let mut relationship = Relationship(sys::DiscordRelationship::default()); 101 | 102 | unsafe { 103 | let mgr = self.relationship_manager(); 104 | 105 | (*mgr).get_at.unwrap()(mgr, index, &mut relationship.0).to_result()?; 106 | } 107 | 108 | Ok(relationship) 109 | } 110 | 111 | /// Returns an `Iterator` over the relationships matching the filter. 112 | /// 113 | /// [`RelationshipsRefreshed`](event/relationships/struct.Refresh.html) 114 | /// must have fired first. 115 | /// 116 | /// ```rust 117 | /// # use discord_game_sdk::*; 118 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 119 | /// for relationship in discord.iter_relationships()? { 120 | /// let relationship = relationship?; 121 | /// // .. 122 | /// } 123 | /// # Ok(()) } 124 | /// ``` 125 | pub fn iter_relationships( 126 | &self, 127 | ) -> Result< 128 | impl '_ 129 | + Iterator> 130 | + DoubleEndedIterator 131 | + ExactSizeIterator 132 | + std::iter::FusedIterator 133 | + std::fmt::Debug, 134 | > { 135 | Ok(iter::Collection::new( 136 | Box::new(move |i| self.ref_copy().relationship_at(i)), 137 | self.relationship_count()?, 138 | )) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /discord_game_sdk/src/methods/users.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, to_result::ToResult, Discord, PremiumKind, Result, User, UserFlags, UserID}; 2 | 3 | /// # Users 4 | /// 5 | /// > [Chapter in official docs](https://discordapp.com/developers/docs/game-sdk/users) 6 | impl<'d, E> Discord<'d, E> { 7 | /// Get the current user. 8 | /// 9 | /// More information can be found through the HTTP API. 10 | /// 11 | /// ## Errors 12 | /// 13 | /// Returns [`Error::NotFound`](enum.Error.html#variant.NotFound) until the event 14 | /// [`EventHandler::on_current_user_update`](trait.EventHandler.html#method.on_current_user_update) 15 | /// has fired. 16 | /// 17 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/users#getcurrentuser) 18 | /// 19 | /// ```rust 20 | /// # use discord_game_sdk::*; 21 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 22 | /// let current_user = discord.current_user()?; 23 | /// # Ok(()) } 24 | /// ``` 25 | pub fn current_user(&self) -> Result { 26 | let mut user = User(sys::DiscordUser::default()); 27 | 28 | unsafe { 29 | let mgr = self.user_manager(); 30 | 31 | (*mgr).get_current_user.unwrap()(mgr, &mut user.0).to_result()?; 32 | } 33 | 34 | Ok(user) 35 | } 36 | 37 | /// Get user information for a given ID. 38 | /// 39 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/users#getuser) 40 | /// 41 | /// ```rust 42 | /// # use discord_game_sdk::*; 43 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 44 | /// # let id_to_lookup = 0; 45 | /// discord.user(id_to_lookup, |discord, result| { 46 | /// match result { 47 | /// Ok(user) => { 48 | /// // ... 49 | /// } 50 | /// Err(error) => eprintln!("failed to fetch user: {}", error), 51 | /// } 52 | /// }); 53 | /// # Ok(()) } 54 | /// ``` 55 | pub fn user( 56 | &self, 57 | user_id: UserID, 58 | callback: impl 'd + FnOnce(&Discord<'d, E>, Result<&User>), 59 | ) { 60 | let (ptr, fun) = self.two_params( 61 | move |discord, res: sys::EDiscordResult, user: *mut sys::DiscordUser| { 62 | callback( 63 | discord, 64 | res.to_result().map(|()| unsafe { &*(user as *mut User) }), 65 | ) 66 | }, 67 | ); 68 | 69 | unsafe { 70 | let mgr = self.user_manager(); 71 | 72 | (*mgr).get_user.unwrap()(mgr, user_id, ptr, fun) 73 | } 74 | } 75 | 76 | /// Get the Premium type for the currently connected user. 77 | /// 78 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/users#getcurrentuserpremiumtype) 79 | /// 80 | /// ```rust 81 | /// # use discord_game_sdk::*; 82 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 83 | /// let premium = discord.current_user_premium_kind()?; 84 | /// # Ok(()) } 85 | /// ``` 86 | pub fn current_user_premium_kind(&self) -> Result { 87 | let mut premium_type = sys::EDiscordPremiumType::default(); 88 | 89 | unsafe { 90 | let mgr = self.user_manager(); 91 | 92 | (*mgr).get_current_user_premium_type.unwrap()(mgr, &mut premium_type).to_result()?; 93 | } 94 | 95 | Ok(PremiumKind::from(premium_type)) 96 | } 97 | 98 | /// Return a bitfield of all flags set for the current user. 99 | /// 100 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/users#currentuserhasflag) 101 | /// 102 | /// ```rust 103 | /// # use discord_game_sdk::*; 104 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 105 | /// let flags = discord.current_user_flags()?; 106 | /// # Ok(()) } 107 | /// ``` 108 | pub fn current_user_flags(&self) -> Result { 109 | let mut flags = UserFlags::empty(); 110 | 111 | for flag in &[ 112 | UserFlags::PARTNER, 113 | UserFlags::HYPE_SQUAD_EVENTS, 114 | UserFlags::HYPE_SQUAD_HOUSE_1, 115 | UserFlags::HYPE_SQUAD_HOUSE_2, 116 | UserFlags::HYPE_SQUAD_HOUSE_3, 117 | ] { 118 | let mut contains = false; 119 | 120 | unsafe { 121 | let mgr = self.user_manager(); 122 | 123 | (*mgr).current_user_has_flag.unwrap()(mgr, flag.bits(), &mut contains) 124 | .to_result()?; 125 | } 126 | 127 | flags.set(*flag, contains); 128 | } 129 | 130 | Ok(flags) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /discord_game_sdk/src/methods/voice.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, to_result::ToResult, Discord, InputMode, Result, UserID}; 2 | 3 | /// # Voice 4 | /// 5 | /// > [Chapter in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice) 6 | impl<'d, E> Discord<'d, E> { 7 | /// Get the voice input mode for the current user. 8 | /// 9 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice#getinputmode) 10 | /// 11 | /// ```rust 12 | /// # use discord_game_sdk::*; 13 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 14 | /// let input_mode = discord.input_mode()?; 15 | /// # Ok(()) } 16 | /// ``` 17 | pub fn input_mode(&self) -> Result { 18 | let mut input_mode = InputMode(sys::DiscordInputMode::default()); 19 | 20 | unsafe { 21 | let mgr = self.voice_manager(); 22 | 23 | (*mgr).get_input_mode.unwrap()(mgr, &mut input_mode.0).to_result()?; 24 | } 25 | 26 | Ok(input_mode) 27 | } 28 | 29 | /// Sets a new voice input mode for the user. 30 | /// 31 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice#setinputmode) 32 | /// 33 | /// ```rust 34 | /// # use discord_game_sdk::*; 35 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 36 | /// discord.set_input_mode( 37 | /// InputMode::push_to_talk("caps lock"), 38 | /// |discord, result| { 39 | /// if let Err(error) = result { 40 | /// return eprintln!("failed to set voice input mode: {}", error); 41 | /// } 42 | /// }, 43 | /// ); 44 | /// # Ok(()) } 45 | /// ``` 46 | pub fn set_input_mode( 47 | &self, 48 | input_mode: InputMode, 49 | callback: impl 'd + FnOnce(&Discord<'d, E>, Result<()>), 50 | ) { 51 | let (ptr, fun) = self 52 | .one_param(move |discord, res: sys::EDiscordResult| callback(discord, res.to_result())); 53 | 54 | unsafe { 55 | let mgr = self.voice_manager(); 56 | 57 | (*mgr).set_input_mode.unwrap()(mgr, input_mode.0, ptr, fun) 58 | } 59 | } 60 | 61 | /// Whether the current user is muted. 62 | /// 63 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice#isselfmute) 64 | /// 65 | /// ```rust 66 | /// # use discord_game_sdk::*; 67 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 68 | /// if discord.self_muted()? { 69 | /// // ... 70 | /// } 71 | /// # Ok(()) } 72 | /// ``` 73 | pub fn self_muted(&self) -> Result { 74 | let mut muted = false; 75 | 76 | unsafe { 77 | let mgr = self.voice_manager(); 78 | 79 | (*mgr).is_self_mute.unwrap()(mgr, &mut muted).to_result()?; 80 | } 81 | 82 | Ok(muted) 83 | } 84 | 85 | /// Whether the current used is deafened. 86 | /// 87 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice#isselfdeaf) 88 | /// 89 | /// ```rust 90 | /// # use discord_game_sdk::*; 91 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 92 | /// if discord.self_deafened()? { 93 | /// // ... 94 | /// } 95 | /// # Ok(()) } 96 | /// ``` 97 | pub fn self_deafened(&self) -> Result { 98 | let mut deafened = false; 99 | 100 | unsafe { 101 | let mgr = self.voice_manager(); 102 | 103 | (*mgr).is_self_deaf.unwrap()(mgr, &mut deafened).to_result()?; 104 | } 105 | 106 | Ok(deafened) 107 | } 108 | 109 | /// Mutes or unmutes the current user. 110 | /// 111 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice#setselfmute) 112 | /// 113 | /// ```rust 114 | /// # use discord_game_sdk::*; 115 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 116 | /// discord.set_self_mute(false)?; 117 | /// # Ok(()) } 118 | /// ``` 119 | pub fn set_self_mute(&self, muted: bool) -> Result<()> { 120 | unsafe { 121 | let mgr = self.voice_manager(); 122 | 123 | (*mgr).set_self_mute.unwrap()(mgr, muted).to_result() 124 | } 125 | } 126 | 127 | /// Deafens or undeafens the current user. 128 | /// 129 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice#setselfdeaf) 130 | /// 131 | /// ```rust 132 | /// # use discord_game_sdk::*; 133 | /// # fn example(discord: Discord<'_, ()>) -> Result<()> { 134 | /// discord.set_self_deaf(false)?; 135 | /// # Ok(()) } 136 | /// ``` 137 | pub fn set_self_deaf(&self, deafened: bool) -> Result<()> { 138 | unsafe { 139 | let mgr = self.voice_manager(); 140 | 141 | (*mgr).set_self_deaf.unwrap()(mgr, deafened).to_result() 142 | } 143 | } 144 | 145 | /// Whether a given user is locally muted. 146 | /// 147 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice#islocalmute) 148 | /// 149 | /// ```rust 150 | /// # use discord_game_sdk::*; 151 | /// # fn example(discord: Discord<'_, ()>, user: User) -> Result<()> { 152 | /// if discord.local_muted(user.id())? { 153 | /// // ... 154 | /// } 155 | /// # Ok(()) } 156 | /// ``` 157 | pub fn local_muted(&self, user_id: UserID) -> Result { 158 | let mut muted = false; 159 | 160 | unsafe { 161 | let mgr = self.voice_manager(); 162 | 163 | (*mgr).is_local_mute.unwrap()(mgr, user_id, &mut muted).to_result()?; 164 | } 165 | 166 | Ok(muted) 167 | } 168 | 169 | /// Gets the local volume for a given user, in the range `[0..=200]`, `100` being the default. 170 | /// 171 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice#getlocalvolume) 172 | /// 173 | /// ```rust 174 | /// # use discord_game_sdk::*; 175 | /// # fn example(discord: Discord<'_, ()>, user: User) -> Result<()> { 176 | /// discord.set_local_volume(user.id(), discord.local_volume(user.id())? + 10)?; 177 | /// # Ok(()) } 178 | /// ``` 179 | pub fn local_volume(&self, user_id: UserID) -> Result { 180 | let mut volume = 0; 181 | 182 | unsafe { 183 | let mgr = self.voice_manager(); 184 | 185 | (*mgr).get_local_volume.unwrap()(mgr, user_id, &mut volume).to_result()?; 186 | } 187 | 188 | debug_assert!((0..=200).contains(&volume)); 189 | 190 | Ok(volume) 191 | } 192 | 193 | /// Locally mutes or unmutes a given user. 194 | /// 195 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice#setlocalmute) 196 | /// 197 | /// ```rust 198 | /// # use discord_game_sdk::*; 199 | /// # fn example(discord: Discord<'_, ()>, user: User) -> Result<()> { 200 | /// discord.set_local_mute(user.id(), true)?; 201 | /// # Ok(()) } 202 | /// ``` 203 | pub fn set_local_mute(&self, user_id: UserID, muted: bool) -> Result<()> { 204 | unsafe { 205 | let mgr = self.voice_manager(); 206 | 207 | (*mgr).set_local_mute.unwrap()(mgr, user_id, muted).to_result() 208 | } 209 | } 210 | 211 | /// Sets the local volume for a given user. 212 | /// 213 | /// In the range `[0..=200]`, `100` being the default. 214 | /// 215 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/discord-voice#setlocalvolume) 216 | /// 217 | /// ```rust 218 | /// # use discord_game_sdk::*; 219 | /// # fn example(discord: Discord<'_, ()>, user: User) -> Result<()> { 220 | /// discord.set_local_volume(user.id(), discord.local_volume(user.id())? + 10)?; 221 | /// # Ok(()) } 222 | /// ``` 223 | pub fn set_local_volume(&self, user_id: UserID, volume: u8) -> Result<()> { 224 | debug_assert!((0..=200).contains(&volume)); 225 | 226 | unsafe { 227 | let mgr = self.voice_manager(); 228 | 229 | (*mgr).set_local_volume.unwrap()(mgr, user_id, volume).to_result() 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /discord_game_sdk/src/mock/ffi.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | use std::ffi::c_void; 3 | 4 | const CORE: &sys::IDiscordCore = &sys::IDiscordCore { 5 | destroy: { 6 | unsafe extern "C" fn destroy(_: *mut sys::IDiscordCore) { 7 | while let Some(closure) = STATE.as_mut().unwrap().queue.pop() { 8 | closure() 9 | } 10 | 11 | drop(STATE.take()); 12 | } 13 | 14 | Some(destroy) 15 | }, 16 | 17 | run_callbacks: { 18 | unsafe extern "C" fn run_callbacks(_: *mut sys::IDiscordCore) -> sys::EDiscordResult { 19 | while let Some(closure) = STATE.as_mut().unwrap().queue.pop() { 20 | closure() 21 | } 22 | 23 | sys::DiscordResult_Ok 24 | } 25 | 26 | Some(run_callbacks) 27 | }, 28 | 29 | get_achievement_manager: { 30 | unsafe extern "C" fn get_achievement_manager( 31 | _: *mut sys::IDiscordCore, 32 | ) -> *mut sys::IDiscordAchievementManager { 33 | ACHIEVEMENT_MANAGER as *const _ as *mut _ 34 | } 35 | 36 | Some(get_achievement_manager) 37 | }, 38 | 39 | set_log_hook: None, 40 | get_application_manager: None, 41 | get_user_manager: None, 42 | get_image_manager: None, 43 | get_activity_manager: None, 44 | get_relationship_manager: None, 45 | get_lobby_manager: None, 46 | get_network_manager: None, 47 | get_overlay_manager: None, 48 | get_storage_manager: None, 49 | get_store_manager: None, 50 | get_voice_manager: None, 51 | }; 52 | 53 | const ACHIEVEMENT_MANAGER: &sys::IDiscordAchievementManager = &sys::IDiscordAchievementManager { 54 | set_user_achievement: { 55 | unsafe extern "C" fn set_user_achievements( 56 | _: *mut sys::IDiscordAchievementManager, 57 | achievement_id: sys::DiscordSnowflake, 58 | percent_complete: u8, 59 | callback_data: *mut c_void, 60 | callback: Option, 61 | ) { 62 | STATE.as_mut().unwrap().queue.push(Box::new(move || { 63 | let state = STATE.as_mut().unwrap(); 64 | 65 | for achievement in state.achievements.iter_mut() { 66 | if achievement.achievement_id == achievement_id { 67 | achievement.percent_complete = percent_complete; 68 | 69 | (*state.params.achievement_events) 70 | .on_user_achievement_update 71 | .unwrap()( 72 | state.params.event_data, achievement as *const _ as *mut _ 73 | ) 74 | } 75 | } 76 | 77 | callback.unwrap()(callback_data, sys::DiscordResult_Ok); 78 | })) 79 | } 80 | 81 | Some(set_user_achievements) 82 | }, 83 | 84 | fetch_user_achievements: { 85 | unsafe extern "C" fn fetch_user_achievements( 86 | _: *mut sys::IDiscordAchievementManager, 87 | callback_data: *mut c_void, 88 | callback: Option, 89 | ) { 90 | STATE.as_mut().unwrap().queue.push(Box::new(move || { 91 | callback.unwrap()(callback_data, sys::DiscordResult_Ok); 92 | })) 93 | } 94 | 95 | Some(fetch_user_achievements) 96 | }, 97 | 98 | get_user_achievement: { 99 | unsafe extern "C" fn get_user_achievement( 100 | _: *mut sys::IDiscordAchievementManager, 101 | user_achievement_id: sys::DiscordSnowflake, 102 | user_achievement: *mut sys::DiscordUserAchievement, 103 | ) -> sys::EDiscordResult { 104 | for achievement in &STATE.as_ref().unwrap().achievements { 105 | if achievement.achievement_id == user_achievement_id { 106 | *user_achievement = *achievement; 107 | 108 | return sys::DiscordResult_Ok; 109 | } 110 | } 111 | 112 | sys::DiscordResult_NotFound 113 | } 114 | 115 | Some(get_user_achievement) 116 | }, 117 | 118 | count_user_achievements: { 119 | unsafe extern "C" fn count_user_achievements( 120 | _: *mut sys::IDiscordAchievementManager, 121 | count: *mut i32, 122 | ) { 123 | *count = STATE.as_ref().unwrap().achievements.len() as i32; 124 | } 125 | Some(count_user_achievements) 126 | }, 127 | 128 | get_user_achievement_at: { 129 | unsafe extern "C" fn get_user_achievement_at( 130 | _: *mut sys::IDiscordAchievementManager, 131 | index: i32, 132 | user_achievement: *mut sys::DiscordUserAchievement, 133 | ) -> sys::EDiscordResult { 134 | *user_achievement = STATE.as_ref().unwrap().achievements[index as usize]; 135 | 136 | sys::DiscordResult_Ok 137 | } 138 | 139 | Some(get_user_achievement_at) 140 | }, 141 | }; 142 | 143 | #[derive(Default)] 144 | struct State { 145 | params: sys::DiscordCreateParams, 146 | achievements: Vec, 147 | queue: Vec>, 148 | } 149 | 150 | static mut STATE: Option = None; 151 | 152 | pub(crate) unsafe fn create_mock(params: sys::DiscordCreateParams) -> *mut sys::IDiscordCore { 153 | if STATE.is_some() { 154 | panic!("can only hold one instance lol"); 155 | } 156 | 157 | STATE = Some(State { 158 | params, 159 | achievements: (0..10) 160 | .map(|achievement_id| sys::DiscordUserAchievement { 161 | user_id: 0, 162 | achievement_id, 163 | percent_complete: 0, 164 | unlocked_at: [0; 64], 165 | }) 166 | .collect(), 167 | ..Default::default() 168 | }); 169 | 170 | CORE as *const _ as *mut _ 171 | } 172 | -------------------------------------------------------------------------------- /discord_game_sdk/src/mock/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | discord::{Discord, DiscordInner}, 3 | events, CreateFlags, EventHandler, UserAchievement, 4 | }; 5 | use std::{cell::UnsafeCell, marker::PhantomData}; 6 | 7 | mod ffi; 8 | 9 | impl Discord<'_, E> { 10 | pub(crate) fn mock() -> Self 11 | where 12 | E: EventHandler, 13 | { 14 | let mut instance = Discord(Box::into_raw(Box::new(DiscordInner { 15 | _invariant_lifetime: PhantomData, 16 | 17 | core: std::ptr::null_mut(), 18 | client_id: 0, 19 | event_handler: UnsafeCell::new(None), 20 | 21 | achievement_events: events::achievement::(), 22 | activity_events: events::activity::(), 23 | lobby_events: events::lobby::(), 24 | network_events: events::network::(), 25 | overlay_events: events::overlay::(), 26 | relationship_events: events::relationship::(), 27 | store_events: events::store::(), 28 | user_events: events::user::(), 29 | voice_events: events::voice::(), 30 | }))); 31 | 32 | let params = instance.create_params(CreateFlags::Default.into()); 33 | 34 | instance.inner_mut().core = unsafe { ffi::create_mock(params) }; 35 | 36 | instance 37 | } 38 | } 39 | 40 | #[test] 41 | fn miri_tests() { 42 | struct E; 43 | 44 | impl EventHandler for E { 45 | fn on_user_achievement_update( 46 | &mut self, 47 | discord: &Discord<'_, Self>, 48 | user_achievement: &UserAchievement, 49 | ) { 50 | for a in discord.iter_user_achievements() { 51 | let a = a.unwrap(); 52 | eprintln!( 53 | "in event_handler {}: {}%", 54 | a.achievement_id(), 55 | a.percent_complete() 56 | ); 57 | } 58 | 59 | if user_achievement.percent_complete() == 99 { 60 | discord.set_user_achievement( 61 | user_achievement.achievement_id(), 62 | 100, 63 | |discord, _res| { 64 | for a in discord.iter_user_achievements() { 65 | let a = a.unwrap(); 66 | eprintln!( 67 | "in event_handler in set {}: {}%", 68 | a.achievement_id(), 69 | a.percent_complete() 70 | ); 71 | } 72 | }, 73 | ); 74 | } 75 | } 76 | } 77 | 78 | let mut discord = Discord::mock(); 79 | *discord.event_handler_mut() = Some(E); 80 | 81 | discord.fetch_user_achievements(|discord, _res| { 82 | discord.set_user_achievement(0, 99, |discord, _res| { 83 | for a in discord.iter_user_achievements() { 84 | let a = a.unwrap(); 85 | eprintln!( 86 | "in fetch in set {}: {}%", 87 | a.achievement_id(), 88 | a.percent_complete() 89 | ); 90 | } 91 | }); 92 | }); 93 | 94 | for _ in 0..100 { 95 | discord.run_callbacks().unwrap(); 96 | } 97 | 98 | discord.fetch_user_achievements(|discord, _res| { 99 | discord.set_user_achievement(0, 99, |_discord, _res| {}); 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /discord_game_sdk/src/oauth2_token.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, utils::charbuf_to_str, UnixTimestamp}; 2 | 3 | /// OAuth 2.0 Token 4 | /// 5 | /// > [Struct in official docs](https://discordapp.com/developers/docs/game-sdk/applications#data-models-oauth2token-struct) 6 | #[derive(Clone, Eq, PartialEq)] 7 | #[repr(transparent)] 8 | pub struct OAuth2Token(pub(crate) sys::DiscordOAuth2Token); 9 | 10 | impl OAuth2Token { 11 | /// A bearer token for the current user 12 | pub fn access_token(&self) -> &str { 13 | charbuf_to_str(&self.0.access_token) 14 | } 15 | 16 | /// The list of `OAuth2` scopes separated by spaces 17 | /// 18 | /// ```rust 19 | /// # use discord_game_sdk::*; 20 | /// # fn example(token: OAuth2Token) -> Result<()> { 21 | /// for scope in token.scopes().split(' ') { 22 | /// println!("we have access to: {}", scope); 23 | /// } 24 | /// # Ok(()) } 25 | /// ``` 26 | pub fn scopes(&self) -> &str { 27 | charbuf_to_str(&self.0.scopes) 28 | } 29 | 30 | /// When the token exires, in UNIX Time 31 | pub fn expires(&self) -> UnixTimestamp { 32 | self.0.expires 33 | } 34 | } 35 | 36 | impl std::fmt::Debug for OAuth2Token { 37 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 38 | fmt.debug_struct("OAuth2Token") 39 | .field("access_token", &self.access_token()) 40 | .field("scopes", &self.scopes()) 41 | .field("expires", &self.expires()) 42 | .finish() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /discord_game_sdk/src/premium_kind.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Premium Type 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/users#data-models-premiumtype-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum PremiumKind { 8 | /// Not a Nitro subscriber 9 | None, 10 | /// Nitro Classic subscriber 11 | Tier1, 12 | /// Nitro subscriber 13 | Tier2, 14 | /// Safety net for missing definitions 15 | Undefined(sys::EDiscordPremiumType), 16 | } 17 | 18 | impl From for PremiumKind { 19 | fn from(source: sys::EDiscordPremiumType) -> Self { 20 | match source { 21 | sys::DiscordPremiumType_None => Self::None, 22 | sys::DiscordPremiumType_Tier1 => Self::Tier1, 23 | sys::DiscordPremiumType_Tier2 => Self::Tier2, 24 | _ => Self::Undefined(source), 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /discord_game_sdk/src/presence.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, Activity, Status}; 2 | 3 | /// User Presence 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/relationships#data-models-presence-struct) 6 | #[derive(Clone, Eq, PartialEq)] 7 | #[repr(transparent)] 8 | pub struct Presence(pub(crate) sys::DiscordPresence); 9 | 10 | impl Presence { 11 | /// The user's current online status 12 | pub fn status(&self) -> Status { 13 | self.0.status.into() 14 | } 15 | 16 | /// The user's current activity 17 | pub fn activity(&self) -> &Activity { 18 | unsafe { &*(&self.0.activity as *const sys::DiscordActivity as *const Activity) } 19 | } 20 | } 21 | 22 | impl std::fmt::Debug for Presence { 23 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 24 | fmt.debug_struct("Presence") 25 | .field("status", &self.status()) 26 | .field("activity", &self.activity()) 27 | .finish() 28 | } 29 | } 30 | 31 | impl std::fmt::Display for Presence { 32 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 | write!(fmt, "{}, {}", self.status(), self.activity()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /discord_game_sdk/src/relationship.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, Presence, RelationshipKind, User}; 2 | 3 | /// Relationship 4 | /// 5 | /// > [Struct in official docs](https://discordapp.com/developers/docs/game-sdk/relationships#data-models-relationship-struct) 6 | #[derive(Clone, Eq, PartialEq)] 7 | #[repr(transparent)] 8 | pub struct Relationship(pub(crate) sys::DiscordRelationship); 9 | 10 | impl Relationship { 11 | /// What sort of relationship it is 12 | pub fn kind(&self) -> RelationshipKind { 13 | self.0.type_.into() 14 | } 15 | 16 | /// The target of the relationship 17 | pub fn user(&self) -> &User { 18 | unsafe { &*(&self.0.user as *const sys::DiscordUser as *const User) } 19 | } 20 | 21 | /// The target's current presence 22 | pub fn presence(&self) -> &Presence { 23 | unsafe { &*(&self.0.presence as *const sys::DiscordPresence as *const Presence) } 24 | } 25 | } 26 | 27 | impl std::fmt::Debug for Relationship { 28 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | fmt.debug_struct("Relationship") 30 | .field("kind", &self.kind()) 31 | .field("user", &self.user()) 32 | .field("presence", &self.presence()) 33 | .finish() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /discord_game_sdk/src/relationship_kind.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Relationship Type 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/relationships#data-models-relationshiptype-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum RelationshipKind { 8 | /// User is blocked 9 | Blocked, 10 | /// User is a friend 11 | Friend, 12 | /// Not a friend but interacts with current user often (frequency + recency) 13 | Implicit, 14 | /// User has no intrinsic relationship 15 | None, 16 | /// User has a pending incoming friend request to current user 17 | PendingIncoming, 18 | /// Current user has a pending outgoing friend request to user 19 | PendingOutgoing, 20 | /// Safety net for missing definitions 21 | Undefined(sys::EDiscordRelationshipType), 22 | } 23 | 24 | impl From for RelationshipKind { 25 | fn from(source: sys::EDiscordRelationshipType) -> Self { 26 | match source { 27 | sys::DiscordRelationshipType_Blocked => Self::Blocked, 28 | sys::DiscordRelationshipType_Friend => Self::Friend, 29 | sys::DiscordRelationshipType_Implicit => Self::Implicit, 30 | sys::DiscordRelationshipType_None => Self::None, 31 | sys::DiscordRelationshipType_PendingIncoming => Self::PendingIncoming, 32 | sys::DiscordRelationshipType_PendingOutgoing => Self::PendingOutgoing, 33 | _ => Self::Undefined(source), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /discord_game_sdk/src/reliability.rs: -------------------------------------------------------------------------------- 1 | /// Network Channel Reliability 2 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 3 | pub enum Reliability { 4 | /// All data will be received 5 | Reliable, 6 | /// Some data will be lost 7 | Unreliable, 8 | } 9 | 10 | impl Into for Reliability { 11 | fn into(self) -> bool { 12 | match self { 13 | Self::Reliable => true, 14 | Self::Unreliable => false, 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /discord_game_sdk/src/request_reply.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// Activity Join Request Reply 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/activities#data-models-activityjoinrequestreply-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum RequestReply { 8 | /// Accept the request 9 | Yes, 10 | /// Deny the request 11 | No, 12 | /// Ignore the request 13 | Ignore, 14 | /// Safety net for missing definitions 15 | Undefined(sys::EDiscordActivityJoinRequestReply), 16 | } 17 | 18 | impl Into for RequestReply { 19 | fn into(self) -> sys::EDiscordActivityJoinRequestReply { 20 | match self { 21 | Self::Yes => sys::DiscordActivityJoinRequestReply_Yes, 22 | Self::No => sys::DiscordActivityJoinRequestReply_No, 23 | Self::Ignore => sys::DiscordActivityJoinRequestReply_Ignore, 24 | Self::Undefined(n) => n, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /discord_game_sdk/src/search_query.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, to_result::ToResult, Cast, Comparison, Distance, Result}; 2 | 3 | /// Lobby Search 4 | /// 5 | /// > [Struct in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#data-models-lobbysearchquery-struct) 6 | #[derive(Clone, Debug, Default)] 7 | pub struct SearchQuery { 8 | pub(crate) filter: Option<(String, String, Comparison, Cast)>, 9 | pub(crate) sort: Option<(String, String, Cast)>, 10 | pub(crate) limit: Option, 11 | pub(crate) distance: Option, 12 | } 13 | 14 | impl SearchQuery { 15 | /// Creates a search object to search available lobbies. 16 | /// 17 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#getsearchquery) 18 | pub fn new() -> Self { 19 | Self::default() 20 | } 21 | 22 | /// Filters lobbies based on metadata comparison. 23 | /// 24 | /// ## Performance 25 | /// 26 | /// A nul byte will be appended to `key` and `value` if one is not present. 27 | /// 28 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#lobbysearchfilter) 29 | pub fn filter( 30 | &mut self, 31 | mut key: String, 32 | comparison: Comparison, 33 | mut value: String, 34 | cast: Cast, 35 | ) -> &mut Self { 36 | if !key.ends_with('\0') { 37 | key.push('\0') 38 | } 39 | 40 | if !value.ends_with('\0') { 41 | value.push('\0') 42 | } 43 | 44 | self.filter = Some((key, value, comparison, cast)); 45 | self 46 | } 47 | 48 | /// Sorts the filtered lobbies based on "near-ness" to a given value 49 | /// 50 | /// ## Performance 51 | /// 52 | /// A nul byte will be appended to `key` and `value` if one is not present. 53 | /// 54 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#lobbysearchsort) 55 | pub fn sort(&mut self, mut key: String, mut value: String, cast: Cast) -> &mut Self { 56 | if !key.ends_with('\0') { 57 | key.push('\0') 58 | } 59 | 60 | if !value.ends_with('\0') { 61 | value.push('\0') 62 | } 63 | 64 | self.sort = Some((key, value, cast)); 65 | self 66 | } 67 | 68 | /// Limits the number of lobbies returned in a search 69 | /// 70 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#lobbysearchlimit) 71 | pub fn limit(&mut self, limit: u32) -> &mut Self { 72 | self.limit = Some(limit); 73 | self 74 | } 75 | 76 | /// Filters lobby results to within certain regions relative to the user's location 77 | /// 78 | /// > [Method in official docs](https://discordapp.com/developers/docs/game-sdk/lobbies#lobbysearchdistance) 79 | pub fn distance(&mut self, distance: Distance) -> &mut Self { 80 | self.distance = Some(distance); 81 | self 82 | } 83 | 84 | pub(crate) unsafe fn process(&self, tx: *mut sys::IDiscordLobbySearchQuery) -> Result<()> { 85 | if let Some((key, value, comparison, cast)) = self.filter.as_ref() { 86 | (*tx).filter.unwrap()( 87 | tx, 88 | // XXX: *mut should be *const 89 | key.as_ptr() as *mut u8, 90 | (*comparison).into(), 91 | (*cast).into(), 92 | // XXX: *mut should be *const 93 | value.as_ptr() as *mut u8, 94 | ) 95 | .to_result()?; 96 | } 97 | 98 | if let Some((key, value, cast)) = self.sort.as_ref() { 99 | (*tx).sort.unwrap()( 100 | tx, 101 | // XXX: *mut should be *const 102 | key.as_ptr() as *mut u8, 103 | (*cast).into(), 104 | // XXX: *mut should be *const 105 | value.as_ptr() as *mut u8, 106 | ) 107 | .to_result()?; 108 | } 109 | 110 | if let Some(limit) = self.limit { 111 | (*tx).limit.unwrap()(tx, limit).to_result()?; 112 | } 113 | 114 | if let Some(distance) = self.distance { 115 | (*tx).distance.unwrap()(tx, distance.into()).to_result()?; 116 | } 117 | 118 | Ok(()) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /discord_game_sdk/src/sku.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, utils::charbuf_to_str, SkuKind, Snowflake}; 2 | 3 | /// SKU (stock keeping unit) 4 | /// 5 | /// > [Struct in official docs](https://discordapp.com/developers/docs/game-sdk/store#data-models-sku-struct) 6 | #[derive(Clone, Eq, PartialEq)] 7 | #[repr(transparent)] 8 | pub struct Sku(pub(crate) sys::DiscordSku); 9 | 10 | impl Sku { 11 | /// The unique ID of the SKU 12 | pub fn id(&self) -> Snowflake { 13 | self.0.id 14 | } 15 | 16 | /// What sort of SKU it is 17 | pub fn kind(&self) -> SkuKind { 18 | self.0.type_.into() 19 | } 20 | 21 | /// The name of the SKU 22 | pub fn name(&self) -> &str { 23 | charbuf_to_str(&self.0.name) 24 | } 25 | 26 | /// The amount of money that the SKU costs 27 | pub fn price_amount(&self) -> u32 { 28 | self.0.price.amount 29 | } 30 | 31 | /// The currency that [`price_amount`](#method.price_currency) is in 32 | pub fn price_currency(&self) -> &str { 33 | charbuf_to_str(&self.0.price.currency) 34 | } 35 | } 36 | 37 | impl std::fmt::Debug for Sku { 38 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | fmt.debug_struct("Sku") 40 | .field("id", &self.id()) 41 | .field("kind", &self.kind()) 42 | .field("name", &self.name()) 43 | .field("price_amount", &self.price_amount()) 44 | .field("price_currency", &self.price_currency()) 45 | .finish() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /discord_game_sdk/src/sku_kind.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// SKU Type 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/store#data-models-skutype-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum SkuKind { 8 | /// SKU is a game 9 | Application, 10 | /// SKU is a bundle (comprising the 3 other types) 11 | Bundle, 12 | /// Bundle is a consumable (in-app purchase) 13 | Consumable, 14 | /// Bundle is a DLC 15 | DLC, 16 | /// Safety net for missing definitions 17 | Undefined(sys::EDiscordSkuType), 18 | } 19 | 20 | impl From for SkuKind { 21 | fn from(source: sys::EDiscordSkuType) -> Self { 22 | match source { 23 | sys::DiscordSkuType_Application => Self::Application, 24 | sys::DiscordSkuType_Bundle => Self::Bundle, 25 | sys::DiscordSkuType_Consumable => Self::Consumable, 26 | sys::DiscordSkuType_DLC => Self::DLC, 27 | _ => Self::Undefined(source), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /discord_game_sdk/src/status.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | /// User Status 4 | /// 5 | /// > [Enum in official docs](https://discordapp.com/developers/docs/game-sdk/relationships#data-models-status-enum) 6 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 7 | pub enum Status { 8 | /// User does not want to be disturbed (red dot) 9 | DoNotDisturb, 10 | /// User is idle (yellow dot) 11 | Idle, 12 | /// User is offline (grey dot) 13 | Offline, 14 | /// User is online (green dot) 15 | Online, 16 | /// Safety net for missing definitions 17 | Undefined(sys::EDiscordStatus), 18 | } 19 | 20 | impl From for Status { 21 | fn from(source: sys::EDiscordStatus) -> Self { 22 | match source { 23 | sys::DiscordStatus_DoNotDisturb => Self::DoNotDisturb, 24 | sys::DiscordStatus_Idle => Self::Idle, 25 | sys::DiscordStatus_Offline => Self::Offline, 26 | sys::DiscordStatus_Online => Self::Online, 27 | _ => Self::Undefined(source), 28 | } 29 | } 30 | } 31 | 32 | impl std::fmt::Display for Status { 33 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 | write!( 35 | fmt, 36 | "{}", 37 | match self { 38 | Self::DoNotDisturb => "do not disturb", 39 | Self::Idle => "idle", 40 | Self::Offline => "offline", 41 | Self::Online => "online", 42 | Self::Undefined(n) => return write!(fmt, "undefined status ({})", n), 43 | } 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /discord_game_sdk/src/to_result.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, Error, Result}; 2 | 3 | pub(crate) trait ToResult: Sized { 4 | fn to_result(self) -> Result<()>; 5 | } 6 | 7 | impl ToResult for sys::EDiscordResult { 8 | fn to_result(self) -> Result<()> { 9 | use Error::*; 10 | 11 | Err(match self { 12 | sys::DiscordResult_Ok => return Ok(()), 13 | sys::DiscordResult_ServiceUnavailable => ServiceUnavailable, 14 | sys::DiscordResult_InvalidVersion => InvalidVersion, 15 | sys::DiscordResult_LockFailed => LockFailed, 16 | sys::DiscordResult_InternalError => Internal, 17 | sys::DiscordResult_InvalidPayload => InvalidPayload, 18 | sys::DiscordResult_InvalidCommand => InvalidCommand, 19 | sys::DiscordResult_InvalidPermissions => InvalidPermissions, 20 | sys::DiscordResult_NotFetched => NotFetched, 21 | sys::DiscordResult_NotFound => NotFound, 22 | sys::DiscordResult_Conflict => Conflict, 23 | sys::DiscordResult_InvalidSecret => InvalidSecret, 24 | sys::DiscordResult_InvalidJoinSecret => InvalidJoinSecret, 25 | sys::DiscordResult_NoEligibleActivity => NoEligibleActivity, 26 | sys::DiscordResult_InvalidInvite => InvalidInvite, 27 | sys::DiscordResult_NotAuthenticated => NotAuthenticated, 28 | sys::DiscordResult_InvalidAccessToken => InvalidAccessToken, 29 | sys::DiscordResult_ApplicationMismatch => ApplicationMismatch, 30 | sys::DiscordResult_InvalidDataUrl => InvalidDataUrl, 31 | sys::DiscordResult_InvalidBase64 => InvalidBase64, 32 | sys::DiscordResult_NotFiltered => NotFiltered, 33 | sys::DiscordResult_LobbyFull => LobbyFull, 34 | sys::DiscordResult_InvalidLobbySecret => InvalidLobbySecret, 35 | sys::DiscordResult_InvalidFilename => InvalidFilename, 36 | sys::DiscordResult_InvalidFileSize => InvalidFileSize, 37 | sys::DiscordResult_InvalidEntitlement => InvalidEntitlement, 38 | sys::DiscordResult_NotInstalled => NotInstalled, 39 | sys::DiscordResult_NotRunning => NotRunning, 40 | sys::DiscordResult_InsufficientBuffer => InsufficientBuffer, 41 | sys::DiscordResult_PurchaseCanceled => PurchaseCanceled, 42 | sys::DiscordResult_InvalidGuild => InvalidGuild, 43 | sys::DiscordResult_InvalidEvent => InvalidEvent, 44 | sys::DiscordResult_InvalidChannel => InvalidChannel, 45 | sys::DiscordResult_InvalidOrigin => InvalidOrigin, 46 | sys::DiscordResult_RateLimited => RateLimited, 47 | sys::DiscordResult_OAuth2Error => OAuth2, 48 | sys::DiscordResult_SelectChannelTimeout => SelectChannelTimeout, 49 | sys::DiscordResult_GetGuildTimeout => GetGuildTimeout, 50 | sys::DiscordResult_SelectVoiceForceRequired => SelectVoiceForceRequired, 51 | sys::DiscordResult_CaptureShortcutAlreadyListening => CaptureShortcutAlreadyListening, 52 | sys::DiscordResult_UnauthorizedForAchievement => UnauthorizedForAchievement, 53 | sys::DiscordResult_InvalidGiftCode => InvalidGiftCode, 54 | sys::DiscordResult_PurchaseError => Purchase, 55 | sys::DiscordResult_TransactionAborted => TransactionAborted, 56 | _ => Undefined(self), 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /discord_game_sdk/src/user.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, utils::charbuf_to_str, ImageHandle, UserID}; 2 | 3 | /// User 4 | /// 5 | /// > [Struct in official docs](https://discordapp.com/developers/docs/game-sdk/users#data-models-user-struct) 6 | #[derive(Clone, Eq, PartialEq)] 7 | #[repr(transparent)] 8 | pub struct User(pub(crate) sys::DiscordUser); 9 | 10 | impl User { 11 | /// The unique ID of the user 12 | pub fn id(&self) -> UserID { 13 | self.0.id 14 | } 15 | 16 | /// Their name 17 | pub fn username(&self) -> &str { 18 | charbuf_to_str(&self.0.username) 19 | } 20 | 21 | /// The four digit unique discriminator, often attached to a `#` 22 | pub fn discriminator(&self) -> &str { 23 | charbuf_to_str(&self.0.discriminator) 24 | } 25 | 26 | /// The hash of the user's avatar 27 | pub fn avatar(&self) -> &str { 28 | charbuf_to_str(&self.0.avatar) 29 | } 30 | 31 | /// Whether the user is a bot 32 | pub fn is_bot(&self) -> bool { 33 | self.0.bot 34 | } 35 | 36 | /// Create an [Image Handle](struct.ImageHandle.html) targeting the user's avatar 37 | pub fn image_handle(&self, size: u32) -> ImageHandle { 38 | ImageHandle::from_user_id(self.id(), size) 39 | } 40 | } 41 | 42 | impl std::fmt::Debug for User { 43 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 44 | fmt.debug_struct("User") 45 | .field("id", &self.id()) 46 | .field("username", &self.username()) 47 | .field("discriminator", &self.discriminator()) 48 | .field("avatar", &self.avatar()) 49 | .field("is_bot", &self.is_bot()) 50 | .finish() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /discord_game_sdk/src/user_achievement.rs: -------------------------------------------------------------------------------- 1 | use crate::{sys, utils::charbuf_to_str, Snowflake, UserID}; 2 | 3 | /// User Achievement 4 | /// 5 | /// > [Struct in official docs](https://discordapp.com/developers/docs/game-sdk/achievements#data-models-user-achievement-struct) 6 | #[derive(Clone, Eq, PartialEq)] 7 | #[repr(transparent)] 8 | pub struct UserAchievement(pub(crate) sys::DiscordUserAchievement); 9 | 10 | impl UserAchievement { 11 | /// The unique id of the user completing the achievement 12 | pub fn user_id(&self) -> UserID { 13 | self.0.user_id 14 | } 15 | 16 | /// The unique id of the achievement 17 | pub fn achievement_id(&self) -> Snowflake { 18 | self.0.achievement_id 19 | } 20 | 21 | /// How far along the user is to completing the achievement [0..=100] 22 | pub fn percent_complete(&self) -> u8 { 23 | debug_assert!((0..=100).contains(&self.0.percent_complete)); 24 | 25 | self.0.percent_complete 26 | } 27 | 28 | /// ISO 8601 formatted date at which the user completed the achievement 29 | pub fn unlocked_at(&self) -> &str { 30 | charbuf_to_str(&self.0.unlocked_at) 31 | } 32 | } 33 | 34 | impl std::fmt::Debug for UserAchievement { 35 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 36 | fmt.debug_struct("UserAchievement") 37 | .field("user_id", &self.user_id()) 38 | .field("achievement_id", &self.achievement_id()) 39 | .field("percent_complete", &self.percent_complete()) 40 | .field("unlocked_at", &self.unlocked_at()) 41 | .finish() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /discord_game_sdk/src/user_flags.rs: -------------------------------------------------------------------------------- 1 | use crate::sys; 2 | 3 | bitflags::bitflags! { 4 | /// User Flags 5 | /// 6 | /// > [Bitfield in official docs](https://discordapp.com/developers/docs/game-sdk/users#data-models-userflag-enum) 7 | pub struct UserFlags: sys::EDiscordUserFlag { 8 | /// Discord Partner 9 | const PARTNER = sys::DiscordUserFlag_Partner; 10 | /// HypeSquad Events participant 11 | const HYPE_SQUAD_EVENTS = sys::DiscordUserFlag_HypeSquadEvents; 12 | /// House Bravery 13 | const HYPE_SQUAD_HOUSE_1 = sys::DiscordUserFlag_HypeSquadHouse1; 14 | /// House Brilliance 15 | const HYPE_SQUAD_HOUSE_2 = sys::DiscordUserFlag_HypeSquadHouse2; 16 | /// House Balance 17 | const HYPE_SQUAD_HOUSE_3 = sys::DiscordUserFlag_HypeSquadHouse3; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /discord_game_sdk/src/utils.rs: -------------------------------------------------------------------------------- 1 | // TRACK: 2 | // https://github.com/rust-lang/rust/issues/52652 3 | // https://github.com/rust-lang/rust/issues/58760 4 | // https://github.com/rust-lang/project-ffi-unwind 5 | pub(crate) fn abort_on_panic(callback: impl FnOnce() -> R + std::panic::UnwindSafe) -> R { 6 | const ACROSS_FFI: &str = "[discord_game_sdk] 7 | The program has encountered a `panic` across FFI bounds, unwinding at this 8 | point would be undefined behavior, we will abort the process instead. 9 | Please report this issue to https://github.com/ldesgoui/discord_game_sdk"; 10 | 11 | match std::panic::catch_unwind(callback) { 12 | Ok(r) => r, 13 | Err(e) => { 14 | if let Some(info) = e.downcast_ref::<&str>() { 15 | log::error!("panic across FFI bounds: {}", info); 16 | eprintln!("\n{}\n\n{}\n", ACROSS_FFI, info); 17 | } else if let Some(info) = e.downcast_ref::() { 18 | log::error!("panic across FFI bounds: {}", info); 19 | eprintln!("\n{}\n\n{}\n", ACROSS_FFI, info); 20 | } else { 21 | log::error!("panic across FFI bounds"); 22 | eprintln!("\n{}\n", ACROSS_FFI); 23 | } 24 | 25 | std::process::abort(); 26 | } 27 | } 28 | } 29 | 30 | pub(crate) fn charbuf_to_str(charbuf: &[u8]) -> &str { 31 | let bytes = &charbuf[..charbuf_len(charbuf)]; 32 | 33 | if cfg!(debug_assertions) { 34 | std::str::from_utf8(bytes).unwrap() 35 | } else { 36 | unsafe { std::str::from_utf8_unchecked(bytes) } 37 | } 38 | } 39 | 40 | pub(crate) fn charbuf_len(charbuf: &[u8]) -> usize { 41 | memchr::memchr(0, charbuf).unwrap_or_else(|| charbuf.len()) 42 | } 43 | 44 | pub(crate) fn write_charbuf(charbuf: &mut [u8], value: &str) { 45 | let bytes = value.as_bytes(); 46 | let len = bytes.len(); 47 | 48 | debug_assert!(len <= charbuf.len()); 49 | 50 | charbuf[..len].copy_from_slice(bytes); 51 | 52 | if len < charbuf.len() { 53 | charbuf[len] = 0; 54 | } 55 | } 56 | 57 | pub(crate) unsafe fn charptr_to_str<'a>(ptr: *const u8) -> &'a str { 58 | let bytes = std::ffi::CStr::from_ptr(ptr as *const i8).to_bytes(); 59 | 60 | if cfg!(debug_assertions) { 61 | std::str::from_utf8(bytes).unwrap() 62 | } else { 63 | std::str::from_utf8_unchecked(bytes) 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | use super::*; 70 | 71 | #[test] 72 | fn test_write_charbuf() { 73 | run_test(""); 74 | run_test("1"); 75 | run_test("10 charact"); 76 | run_test("64 characters 64 characters 64 characters 64 characters 64 chara"); 77 | } 78 | 79 | #[test] 80 | #[should_panic] 81 | fn panic_test_write_charbuf() { 82 | run_test("65 characters 65 characters 65 characters 65 characters 65 charac"); 83 | } 84 | 85 | fn run_test(val: &str) { 86 | let mut charbuf = [0u8; 64]; 87 | 88 | write_charbuf(&mut charbuf, val); 89 | 90 | assert_eq!(charbuf_to_str(&charbuf), val); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /discord_game_sdk_sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "discord_game_sdk_sys" 3 | version = "1.0.1" # check sys/src/lib.rs 4 | authors = ["ldesgoui "] 5 | edition = "2018" 6 | description = "Low-level bindings for the Discord Game SDK" 7 | license = "Apache-2.0 OR MIT" 8 | repository = "https://github.com/ldesgoui/discord_game_sdk" 9 | keywords = ["discord", "sdk", "gamedev"] 10 | categories = ["external-ffi-bindings", "game-engines"] 11 | readme = "README.md" 12 | build = "build.rs" 13 | 14 | [package.metadata.docs.rs] 15 | features = ["private-docs-rs"] 16 | 17 | [features] 18 | link = [] 19 | private-docs-rs = [] # DO NOT RELY ON THIS 20 | 21 | [build-dependencies] 22 | bindgen = { version = "0.59", default-features = false, features = ["runtime"] } 23 | -------------------------------------------------------------------------------- /discord_game_sdk_sys/README.md: -------------------------------------------------------------------------------- 1 | # discord_game_sdk_sys 2 | 3 | [![Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/discord_game_sdk_sys) 4 | [![Latest Version](https://img.shields.io/crates/v/discord_game_sdk_sys.svg)](https://crates.io/crates/discord_game_sdk_sys) 5 | ![License](https://img.shields.io/crates/l/discord_game_sdk_sys) 6 | 7 | This crate provides `bindgen`-generated bindings to the [Discord Game SDK]. 8 | 9 | *This crate is not official, it is not supported by the Discord Game SDK Developers.* 10 | 11 | Following the `-sys` package conventions, this crate does not define higher-level abstractions. 12 | 13 | 14 | ## Usage 15 | 16 | Add this to your `Cargo.toml`: 17 | 18 | ```toml 19 | [dependencies] 20 | discord_game_sdk_sys = "1.0.1" 21 | ``` 22 | 23 | Read up on potential [`bindgen` requirements]. 24 | 25 | Download the [Discord Game SDK] and set the following environment variable to where you extracted it: 26 | 27 | ```sh 28 | export DISCORD_GAME_SDK_PATH=/path/to/discord_game_sdk 29 | ``` 30 | 31 | If you're also planning on using the default `link` feature, keep reading below. 32 | 33 | 34 | ## Features: 35 | 36 | #### `link` 37 | 38 | Enabled by default, delegates to `discord_game_sdk_sys/link`. 39 | 40 | Provides functional linking with the caveat that libraries are renamed and some additional 41 | set-up is required: 42 | 43 | ```sh 44 | # Linux: prepend with `lib` and add to library search path 45 | cp $DISCORD_GAME_SDK_PATH/lib/x86_64/{,lib}discord_game_sdk.so 46 | export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}$DISCORD_GAME_SDK_PATH/lib/x86_64 47 | 48 | # Mac OS: prepend with `lib` and add to library search path 49 | cp $DISCORD_GAME_SDK_PATH/lib/x86_64/{,lib}discord_game_sdk.dylib 50 | export DYLD_LIBRARY_PATH=${DYLD_LIBRARY_PATH:+${DYLD_LIBRARY_PATH}:}$DISCORD_GAME_SDK_PATH/lib/x86_64 51 | 52 | # Windows: change `dll.lib` to `lib` (won't affect library search) 53 | cp $DISCORD_GAME_SDK_PATH/lib/x86_64/discord_game_sdk.{dll.lib,lib} 54 | cp $DISCORD_GAME_SDK_PATH/lib/x86/discord_game_sdk.{dll.lib,lib} 55 | ``` 56 | 57 | This allows for `cargo run` to function. 58 | 59 | 60 | ## Legal 61 | 62 | You *MUST* acquaint yourself with and agree to the [official terms of the Discord Game SDK]. 63 | 64 | The code of the Rust crates `discord_game_sdk` and `discord_game_sdk_sys` 65 | are licensed at your option under either of: 66 | 67 | * [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) 68 | * [MIT License](https://opensource.org/licenses/MIT) 69 | 70 | Unless you explicitly state otherwise, any contribution intentionally 71 | submitted for inclusion in the work by you, as defined in the Apache-2.0 72 | license, shall be dual licensed as above, without any additional terms or 73 | conditions. 74 | 75 | [Discord Game SDK]: https://discordapp.com/developers/docs/game-sdk/sdk-starter-guide 76 | [`bindgen` requirements]: https://rust-lang.github.io/rust-bindgen/requirements.html 77 | [official terms of the Discord Game SDK]: https://discordapp.com/developers/docs/legal 78 | -------------------------------------------------------------------------------- /discord_game_sdk_sys/README.tpl: -------------------------------------------------------------------------------- 1 | # {{crate}} 2 | 3 | [![Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/{{crate}}) 4 | [![Latest Version](https://img.shields.io/crates/v/{{crate}}.svg)](https://crates.io/crates/{{crate}}) 5 | ![License](https://img.shields.io/crates/l/{{crate}}) 6 | 7 | {{readme}} 8 | -------------------------------------------------------------------------------- /discord_game_sdk_sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::*}; 2 | 3 | fn main() { 4 | // DO NOT RELY ON THIS 5 | if cfg!(feature = "private-docs-rs") { 6 | return generate_ffi_bindings(bindgen::builder().header("discord_game_sdk.h")); 7 | } 8 | 9 | let sdk_path = PathBuf::from(env::var("DISCORD_GAME_SDK_PATH").expect(MISSING_SDK_PATH)); 10 | println!("cargo:rerun-if-env-changed=DISCORD_GAME_SDK_PATH"); 11 | println!("cargo:rerun-if-changed={}", sdk_path.display()); 12 | 13 | generate_ffi_bindings( 14 | bindgen::builder().header(sdk_path.join("c/discord_game_sdk.h").to_str().unwrap()), 15 | ); 16 | 17 | if cfg!(feature = "link") { 18 | let target = env::var("TARGET").unwrap(); 19 | 20 | verify_installation(&target, &sdk_path); 21 | configure_linkage(&target, &sdk_path); 22 | } 23 | } 24 | 25 | fn verify_installation(target: &str, sdk_path: &Path) { 26 | match target { 27 | "x86_64-unknown-linux-gnu" => { 28 | assert!( 29 | sdk_path.join("lib/x86_64/libdiscord_game_sdk.so").exists(), 30 | "{}", 31 | MISSING_SETUP 32 | ); 33 | } 34 | 35 | "x86_64-apple-darwin" => { 36 | assert!( 37 | sdk_path 38 | .join("lib/x86_64/libdiscord_game_sdk.dylib") 39 | .exists(), 40 | "{}", 41 | MISSING_SETUP 42 | ); 43 | } 44 | 45 | "x86_64-pc-windows-gnu" | "x86_64-pc-windows-msvc" => { 46 | assert!( 47 | sdk_path.join("lib/x86_64/discord_game_sdk.lib").exists(), 48 | "{}", 49 | MISSING_SETUP 50 | ); 51 | } 52 | 53 | "i686-pc-windows-gnu" | "i686-pc-windows-msvc" => { 54 | assert!( 55 | sdk_path.join("lib/x86/discord_game_sdk.lib").exists(), 56 | "{}", 57 | MISSING_SETUP 58 | ); 59 | } 60 | 61 | _ => panic!("{}", INCOMPATIBLE_PLATFORM), 62 | } 63 | } 64 | 65 | fn configure_linkage(target: &str, sdk_path: &Path) { 66 | match target { 67 | "x86_64-unknown-linux-gnu" 68 | | "x86_64-apple-darwin" 69 | | "x86_64-pc-windows-gnu" 70 | | "x86_64-pc-windows-msvc" => { 71 | println!("cargo:rustc-link-lib=discord_game_sdk"); 72 | println!( 73 | "cargo:rustc-link-search={}", 74 | sdk_path.join("lib/x86_64").display() 75 | ); 76 | } 77 | 78 | "i686-pc-windows-gnu" | "i686-pc-windows-msvc" => { 79 | println!("cargo:rustc-link-lib=discord_game_sdk"); 80 | println!( 81 | "cargo:rustc-link-search={}", 82 | sdk_path.join("lib/x86").display() 83 | ); 84 | } 85 | 86 | _ => {} 87 | } 88 | } 89 | 90 | fn generate_ffi_bindings(builder: bindgen::Builder) { 91 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 92 | 93 | builder 94 | .ctypes_prefix("ctypes") 95 | .derive_copy(true) 96 | .derive_debug(true) 97 | .derive_default(true) 98 | .derive_eq(true) 99 | .derive_hash(true) 100 | .derive_partialeq(true) 101 | .generate_comments(false) 102 | .impl_debug(true) 103 | .impl_partialeq(true) 104 | .parse_callbacks(Box::new(Callbacks)) 105 | .prepend_enum_name(false) 106 | .allowlist_function("Discord.+") 107 | .allowlist_type("[EI]?Discord.+") 108 | .allowlist_var("DISCORD_.+") 109 | .generate() 110 | .expect("discord_game_sdk_sys: bindgen could not generate bindings") 111 | .write_to_file(out_path.join("bindings.rs")) 112 | .expect("discord_game_sdk_sys: could not write bindings to file"); 113 | } 114 | 115 | #[derive(Debug)] 116 | struct Callbacks; 117 | 118 | impl bindgen::callbacks::ParseCallbacks for Callbacks { 119 | fn int_macro(&self, name: &str, _value: i64) -> Option { 120 | // Must match sys::DiscordVersion 121 | if name.ends_with("_VERSION") { 122 | Some(bindgen::callbacks::IntKind::I32) 123 | } else { 124 | None 125 | } 126 | } 127 | } 128 | 129 | const MISSING_SDK_PATH: &str = r#" 130 | 131 | discord_game_sdk_sys: Hello, 132 | 133 | You are trying to generate the bindings for the Discord Game SDK. 134 | You will have to download the SDK yourself. 135 | Here are the links to get it: 136 | 137 | https://discordapp.com/developers/docs/game-sdk/sdk-starter-guide 138 | https://dl-game-sdk.discordapp.net/latest/discord_game_sdk.zip 139 | 140 | Once you have downloaded it, extract the contents to a folder 141 | and set the environment variable `DISCORD_GAME_SDK_PATH` to its path. 142 | 143 | Example: 144 | 145 | $ export DISCORD_GAME_SDK_PATH=$HOME/Downloads/discord_game_sdk 146 | 147 | Please report any issues you have at: 148 | https://github.com/ldesgoui/discord_game_sdk 149 | 150 | Thanks, and apologies for the inconvenience 151 | 152 | "#; 153 | 154 | const MISSING_SETUP: &str = r#" 155 | 156 | discord_game_sdk_sys: Hello, 157 | 158 | You are trying to link to the Discord Game SDK. 159 | Some additional set-up is required, namely some files need to be copied for the linker: 160 | 161 | # Linux: prepend with `lib` and add to library search path 162 | $ cp $DISCORD_GAME_SDK_PATH/lib/x86_64/{,lib}discord_game_sdk.so 163 | $ export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}$DISCORD_GAME_SDK_PATH/lib/x86_64 164 | 165 | # Mac OS: prepend with `lib` and add to library search path 166 | $ cp $DISCORD_GAME_SDK_PATH/lib/x86_64/{,lib}discord_game_sdk.dylib 167 | $ export DYLD_LIBRARY_PATH=${DYLD_LIBRARY_PATH:+${DYLD_LIBRARY_PATH}:}$DISCORD_GAME_SDK_PATH/lib/x86_64 168 | 169 | # Windows: copy `*.dll.lib` to `*.lib` (won't affect library search) 170 | $ cp $DISCORD_GAME_SDK_PATH/lib/x86_64/discord_game_sdk.{dll.lib,lib} 171 | $ cp $DISCORD_GAME_SDK_PATH/lib/x86/discord_game_sdk.{dll.lib,lib} 172 | 173 | After all this, `cargo build` and `cargo run` should function as expected. 174 | 175 | Please report any issues you have at: 176 | https://github.com/ldesgoui/discord_game_sdk 177 | 178 | Thanks, and apologies for the inconvenience 179 | 180 | "#; 181 | 182 | const INCOMPATIBLE_PLATFORM: &str = r#" 183 | 184 | discord_game_sdk_sys: Hello, 185 | 186 | You are trying to link to the Discord Game SDK. 187 | Unfortunately, the platform you are trying to target is not supported. 188 | 189 | Please report any issues you have at: 190 | https://github.com/ldesgoui/discord_game_sdk 191 | 192 | Thanks, and apologies for the inconvenience 193 | 194 | "#; 195 | -------------------------------------------------------------------------------- /discord_game_sdk_sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides `bindgen`-generated bindings to the [Discord Game SDK]. 2 | //! 3 | //! *This crate is not official, it is not supported by the Discord Game SDK Developers.* 4 | //! 5 | //! Following the `-sys` package conventions, this crate does not define higher-level abstractions. 6 | //! 7 | //! 8 | //! # Usage 9 | //! 10 | //! Add this to your `Cargo.toml`: 11 | //! 12 | //! ```toml 13 | //! [dependencies] 14 | //! discord_game_sdk_sys = "1.0.1" 15 | //! ``` 16 | //! 17 | //! Read up on potential [`bindgen` requirements]. 18 | //! 19 | //! Download the [Discord Game SDK] and set the following environment variable to where you extracted it: 20 | //! 21 | //! ```sh 22 | //! export DISCORD_GAME_SDK_PATH=/path/to/discord_game_sdk 23 | //! ``` 24 | //! 25 | //! If you're also planning on using the default `link` feature, keep reading below. 26 | //! 27 | //! 28 | //! # Features: 29 | //! 30 | //! ### `link` 31 | //! 32 | //! Enabled by default, delegates to `discord_game_sdk_sys/link`. 33 | //! 34 | //! Provides functional linking with the caveat that libraries are renamed and some additional 35 | //! set-up is required: 36 | //! 37 | //! ```sh 38 | //! # Linux: prepend with `lib` and add to library search path 39 | //! cp $DISCORD_GAME_SDK_PATH/lib/x86_64/{,lib}discord_game_sdk.so 40 | //! export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}$DISCORD_GAME_SDK_PATH/lib/x86_64 41 | //! 42 | //! # Mac OS: prepend with `lib` and add to library search path 43 | //! cp $DISCORD_GAME_SDK_PATH/lib/x86_64/{,lib}discord_game_sdk.dylib 44 | //! export DYLD_LIBRARY_PATH=${DYLD_LIBRARY_PATH:+${DYLD_LIBRARY_PATH}:}$DISCORD_GAME_SDK_PATH/lib/x86_64 45 | //! 46 | //! # Windows: change `dll.lib` to `lib` (won't affect library search) 47 | //! cp $DISCORD_GAME_SDK_PATH/lib/x86_64/discord_game_sdk.{dll.lib,lib} 48 | //! cp $DISCORD_GAME_SDK_PATH/lib/x86/discord_game_sdk.{dll.lib,lib} 49 | //! ``` 50 | //! 51 | //! This allows for `cargo run` to function. 52 | //! 53 | //! 54 | //! # Legal 55 | //! 56 | //! You *MUST* acquaint yourself with and agree to the [official terms of the Discord Game SDK]. 57 | //! 58 | //! The code of the Rust crates `discord_game_sdk` and `discord_game_sdk_sys` 59 | //! are licensed at your option under either of: 60 | //! 61 | //! * [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) 62 | //! * [MIT License](https://opensource.org/licenses/MIT) 63 | //! 64 | //! Unless you explicitly state otherwise, any contribution intentionally 65 | //! submitted for inclusion in the work by you, as defined in the Apache-2.0 66 | //! license, shall be dual licensed as above, without any additional terms or 67 | //! conditions. 68 | //! 69 | //! 70 | //! [Discord Game SDK]: https://discordapp.com/developers/docs/game-sdk/sdk-starter-guide 71 | //! [`bindgen` requirements]: https://rust-lang.github.io/rust-bindgen/requirements.html 72 | //! [official terms of the Discord Game SDK]: https://discordapp.com/developers/docs/legal 73 | 74 | #![allow(clippy::all, warnings)] 75 | #![doc(html_root_url = "https://docs.rs/discord_game_sdk_sys/1.0.1")] 76 | 77 | // Strings in the SDK are already UTF-8, we rarely end up using CStr/CString because of the 78 | // development overhead costs, `u8`s mean we have to do less conversions in code. 79 | pub(crate) mod ctypes { 80 | pub(crate) use std::os::raw::{c_uchar as c_char, *}; 81 | } 82 | 83 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 84 | --------------------------------------------------------------------------------