├── applicationId.lua ├── LICENSE ├── main.lua ├── conf.lua ├── README.md └── discordRPC.lua /applicationId.lua: -------------------------------------------------------------------------------- 1 | return "" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Joel Schumacher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | local discordRPC = require("discordRPC") 2 | 3 | local appId = require("applicationId") 4 | 5 | function discordRPC.ready(userId, username, discriminator, avatar) 6 | print(string.format("Discord: ready (%s, %s, %s, %s)", userId, username, discriminator, avatar)) 7 | end 8 | 9 | function discordRPC.disconnected(errorCode, message) 10 | print(string.format("Discord: disconnected (%d: %s)", errorCode, message)) 11 | end 12 | 13 | function discordRPC.errored(errorCode, message) 14 | print(string.format("Discord: error (%d: %s)", errorCode, message)) 15 | end 16 | 17 | function discordRPC.joinGame(joinSecret) 18 | print(string.format("Discord: join (%s)", joinSecret)) 19 | end 20 | 21 | function discordRPC.spectateGame(spectateSecret) 22 | print(string.format("Discord: spectate (%s)", spectateSecret)) 23 | end 24 | 25 | function discordRPC.joinRequest(userId, username, discriminator, avatar) 26 | print(string.format("Discord: join request (%s, %s, %s, %s)", userId, username, discriminator, avatar)) 27 | discordRPC.respond(userId, "yes") 28 | end 29 | 30 | function love.load() 31 | discordRPC.initialize(appId, true) 32 | local now = os.time(os.date("*t")) 33 | presence = { 34 | state = "Looking to Play", 35 | details = "1v1 (Ranked)", 36 | startTimestamp = now, 37 | endTimestamp = now + 60, 38 | partyId = "party id", 39 | partyMax = 2, 40 | matchSecret = "match secret", 41 | joinSecret = "join secret", 42 | spectateSecret = "spectate secret", 43 | } 44 | 45 | nextPresenceUpdate = 0 46 | end 47 | 48 | function love.update() 49 | if nextPresenceUpdate < love.timer.getTime() then 50 | discordRPC.updatePresence(presence) 51 | nextPresenceUpdate = love.timer.getTime() + 2.0 52 | end 53 | discordRPC.runCallbacks() 54 | end 55 | 56 | function love.quit() 57 | discordRPC.shutdown() 58 | end 59 | -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | function love.conf(t) 2 | t.identity = "lua-discordRPC Example" -- The name of the save directory (string) 3 | t.version = "0.10.2" -- The LÖVE version this game was made for (string) 4 | t.console = true -- Attach a console (boolean, Windows only) 5 | t.accelerometerjoystick = true -- Enable the accelerometer on iOS and Android by exposing it as a Joystick (boolean) 6 | t.gammacorrect = false -- Enable gamma-correct rendering, when supported by the system (boolean) 7 | 8 | t.window.title = "lua-discordRPC Example" -- The window title (string) 9 | t.window.icon = nil -- Filepath to an image to use as the window's icon (string) 10 | t.window.width = 800 -- The window width (number) 11 | t.window.height = 600 -- The window height (number) 12 | t.window.borderless = false -- Remove all border visuals from the window (boolean) 13 | t.window.resizable = false -- Let the window be user-resizable (boolean) 14 | t.window.minwidth = 1 -- Minimum window width if the window is resizable (number) 15 | t.window.minheight = 1 -- Minimum window height if the window is resizable (number) 16 | t.window.fullscreen = false -- Enable fullscreen (boolean) 17 | t.window.fullscreentype = "desktop" -- Choose between "desktop" fullscreen or "exclusive" fullscreen mode (string) 18 | t.window.vsync = false -- Enable vertical sync (boolean) 19 | t.window.msaa = 0 -- The number of samples to use with multi-sampled antialiasing (number) 20 | t.window.display = 1 -- Index of the monitor to show the window in (number) 21 | t.window.highdpi = false -- Enable high-dpi mode for the window on a Retina display (boolean) 22 | t.window.x = nil -- The x-coordinate of the window's position in the specified display (number) 23 | t.window.y = nil -- The y-coordinate of the window's position in the specified display (number) 24 | 25 | t.modules.audio = true -- Enable the audio module (boolean) 26 | t.modules.event = true -- Enable the event module (boolean) 27 | t.modules.graphics = true -- Enable the graphics module (boolean) 28 | t.modules.image = true -- Enable the image module (boolean) 29 | t.modules.joystick = true -- Enable the joystick module (boolean) 30 | t.modules.keyboard = true -- Enable the keyboard module (boolean) 31 | t.modules.math = true -- Enable the math module (boolean) 32 | t.modules.mouse = true -- Enable the mouse module (boolean) 33 | t.modules.physics = true -- Enable the physics module (boolean) 34 | t.modules.sound = true -- Enable the sound module (boolean) 35 | t.modules.system = true -- Enable the system module (boolean) 36 | t.modules.timer = true -- Enable the timer module (boolean), Disabling it will result 0 delta time in love.update 37 | t.modules.touch = true -- Enable the touch module (boolean) 38 | t.modules.video = true -- Enable the video module (boolean) 39 | t.modules.window = true -- Enable the window module (boolean) 40 | t.modules.thread = true -- Enable the thread module (boolean) 41 | end 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-discordRPC 2 | LuaJIT bindings for the [Discord Rich Presence library](https://github.com/discordapp/discord-rpc) (v3.3.0). 3 | 4 | # Usage 5 | To use this library, download the binaries of discord-rpc (or build them yourself) and make sure the dynamic library is in some location it can be loaded from (e.g. on Windows: https://msdn.microsoft.com/en-us/library/windows/desktop/ms682586(v=vs.85).aspx, usually just next to the executable). 6 | 7 | If you downloaded a release from the discord-rpc github, the file you are looking for will be in `discord-rpc/winX-dynamic/bin/discord-rpc.dll` (choose X according to whether your executable is 32 or 64 bit) 8 | 9 | Then just do `local discordRPC = require "discordRPC"` wherever you need it. 10 | 11 | *If you are using löve, just put the appropriate .dll next to your löve executable* 12 | 13 | An example of the usage of the library using [löve](https://love2d.org/) can be found in `main.lua`. 14 | 15 | # Documentation 16 | How and when to use the functions that are part of the Discord RPC API is identical to the C API, therefore you should first make yourself familiar with the API documentation on Discord's website: 17 | 18 | https://discordapp.com/developers/docs/rich-presence/how-to 19 | 20 | All other differences and the function signatures are as follows: 21 | 22 | ## `discordRPC.initialize(applicationId, autoRegister, optionalSteamId)` (*Discord_Initialize*) 23 | * `applicationId` must be a string 24 | * `autoRegister` must be a boolean 25 | * `optionalSteamId` may be nil (i.e. not passed at all) or a string 26 | 27 | You do not have to pass `handlers` to this function, instead you may define functions in the module table of discordRPC: 28 | 29 | **Notes about callbacks**: 30 | Just-In-Time compilation is disabled for callbacks (for reasons, see the comment in the implementation of `discordRPC.runCallbacks`), so try to avoid doing performance critical tasks in them. 31 | 32 | ### `discordRPC.ready(userId, username, discriminator, avatar)` 33 | `userId`, `username`, `discriminator` and `avatar` are all strings 34 | 35 | ### `discordRPC.errored(errorCode, message)` 36 | * `errorCode` is a number 37 | * `message` is a string 38 | 39 | ### `discordRPC.disconnected(errorCode, message)` 40 | * `errorCode` is a number 41 | * `message` is a string 42 | 43 | ### `discordRPC.joinGame(joinSecret)` 44 | `joinSecret` is a string 45 | 46 | ### `discordRPC.spectateGame(spectateSecret)` 47 | `spectateSecret` is a string 48 | 49 | ### `discordRPC.joinRequest(userId, username, discriminator, avatar)` 50 | `userId`, `username`, `discriminator` and `avatar` are all strings 51 | 52 | ## `discordRPC.shutdown()` (*Discord_Shutdown*) 53 | 54 | ## `discordRPC.runCallbacks()` (*Discord_RunCallbacks*) 55 | 56 | ## `discordRPC.updatePresence(presence)` (*Discord_UpdatePresence*) 57 | `presence` must be a table with the following keys (all optional): 58 | * `state` must be a string (max length: 127) 59 | * `details` must be a string (max length: 127) 60 | * `startTimestamp` must be an integer (52 bit, signed) 61 | * `endTimestamp` must be an integer (52 bit, signed) 62 | * `largeImageKey` must be a string (max length: 31) 63 | * `largeImageText` must be a string (max length: 127) 64 | * `smallImageKey` must be a string (max length: 31) 65 | * `smallImageText` must be a string (max length: 127) 66 | * `partyId` must be a string (max length: 127) 67 | * `partySize` must be an integer (32 bit, signed) 68 | * `partyMax` must be an integer (32 bit, signed) 69 | * `matchSecret` must be a string (max length: 127) 70 | * `joinSecret` must be a string (max length: 127) 71 | * `spectateSecret` must be a string (max length: 127) 72 | * `instance` must be an integer (8 bit, signed) 73 | 74 | ## `discordRPC.clearPresence()` (*Discord_ClearPresence*) 75 | 76 | ## `discordRPC.respond(userId, reply)` (*Discord_Respond*) 77 | * `userId` is a string 78 | * `reply` is now a string and must be either (`"no"` (0), `"yes"` (1) or `"ignore"` (2)) 79 | -------------------------------------------------------------------------------- /discordRPC.lua: -------------------------------------------------------------------------------- 1 | local ffi = require "ffi" 2 | local discordRPClib = ffi.load("discord-rpc") 3 | 4 | ffi.cdef[[ 5 | typedef struct DiscordRichPresence { 6 | const char* state; /* max 128 bytes */ 7 | const char* details; /* max 128 bytes */ 8 | int64_t startTimestamp; 9 | int64_t endTimestamp; 10 | const char* largeImageKey; /* max 32 bytes */ 11 | const char* largeImageText; /* max 128 bytes */ 12 | const char* smallImageKey; /* max 32 bytes */ 13 | const char* smallImageText; /* max 128 bytes */ 14 | const char* partyId; /* max 128 bytes */ 15 | int partySize; 16 | int partyMax; 17 | const char* matchSecret; /* max 128 bytes */ 18 | const char* joinSecret; /* max 128 bytes */ 19 | const char* spectateSecret; /* max 128 bytes */ 20 | int8_t instance; 21 | } DiscordRichPresence; 22 | 23 | typedef struct DiscordUser { 24 | const char* userId; 25 | const char* username; 26 | const char* discriminator; 27 | const char* avatar; 28 | } DiscordUser; 29 | 30 | typedef void (*readyPtr)(const DiscordUser* request); 31 | typedef void (*disconnectedPtr)(int errorCode, const char* message); 32 | typedef void (*erroredPtr)(int errorCode, const char* message); 33 | typedef void (*joinGamePtr)(const char* joinSecret); 34 | typedef void (*spectateGamePtr)(const char* spectateSecret); 35 | typedef void (*joinRequestPtr)(const DiscordUser* request); 36 | 37 | typedef struct DiscordEventHandlers { 38 | readyPtr ready; 39 | disconnectedPtr disconnected; 40 | erroredPtr errored; 41 | joinGamePtr joinGame; 42 | spectateGamePtr spectateGame; 43 | joinRequestPtr joinRequest; 44 | } DiscordEventHandlers; 45 | 46 | void Discord_Initialize(const char* applicationId, 47 | DiscordEventHandlers* handlers, 48 | int autoRegister, 49 | const char* optionalSteamId); 50 | 51 | void Discord_Shutdown(void); 52 | 53 | void Discord_RunCallbacks(void); 54 | 55 | void Discord_UpdatePresence(const DiscordRichPresence* presence); 56 | 57 | void Discord_ClearPresence(void); 58 | 59 | void Discord_Respond(const char* userid, int reply); 60 | 61 | void Discord_UpdateHandlers(DiscordEventHandlers* handlers); 62 | ]] 63 | 64 | local discordRPC = {} -- module table 65 | 66 | -- proxy to detect garbage collection of the module 67 | discordRPC.gcDummy = newproxy(true) 68 | 69 | local function unpackDiscordUser(request) 70 | return ffi.string(request.userId), ffi.string(request.username), 71 | ffi.string(request.discriminator), ffi.string(request.avatar) 72 | end 73 | 74 | -- callback proxies 75 | -- note: callbacks are not JIT compiled (= SLOW), try to avoid doing performance critical tasks in them 76 | -- luajit.org/ext_ffi_semantics.html 77 | local ready_proxy = ffi.cast("readyPtr", function(request) 78 | if discordRPC.ready then 79 | discordRPC.ready(unpackDiscordUser(request)) 80 | end 81 | end) 82 | 83 | local disconnected_proxy = ffi.cast("disconnectedPtr", function(errorCode, message) 84 | if discordRPC.disconnected then 85 | discordRPC.disconnected(errorCode, ffi.string(message)) 86 | end 87 | end) 88 | 89 | local errored_proxy = ffi.cast("erroredPtr", function(errorCode, message) 90 | if discordRPC.errored then 91 | discordRPC.errored(errorCode, ffi.string(message)) 92 | end 93 | end) 94 | 95 | local joinGame_proxy = ffi.cast("joinGamePtr", function(joinSecret) 96 | if discordRPC.joinGame then 97 | discordRPC.joinGame(ffi.string(joinSecret)) 98 | end 99 | end) 100 | 101 | local spectateGame_proxy = ffi.cast("spectateGamePtr", function(spectateSecret) 102 | if discordRPC.spectateGame then 103 | discordRPC.spectateGame(ffi.string(spectateSecret)) 104 | end 105 | end) 106 | 107 | local joinRequest_proxy = ffi.cast("joinRequestPtr", function(request) 108 | if discordRPC.joinRequest then 109 | discordRPC.joinRequest(unpackDiscordUser(request)) 110 | end 111 | end) 112 | 113 | -- helpers 114 | local function checkArg(arg, argType, argName, func, maybeNil) 115 | assert(type(arg) == argType or (maybeNil and arg == nil), 116 | string.format("Argument \"%s\" to function \"%s\" has to be of type \"%s\"", 117 | argName, func, argType)) 118 | end 119 | 120 | local function checkStrArg(arg, maxLen, argName, func, maybeNil) 121 | if maxLen then 122 | assert(type(arg) == "string" and arg:len() <= maxLen or (maybeNil and arg == nil), 123 | string.format("Argument \"%s\" of function \"%s\" has to be of type string with maximum length %d", 124 | argName, func, maxLen)) 125 | else 126 | checkArg(arg, "string", argName, func, true) 127 | end 128 | end 129 | 130 | local function checkIntArg(arg, maxBits, argName, func, maybeNil) 131 | maxBits = math.min(maxBits or 32, 52) -- lua number (double) can only store integers < 2^53 132 | local maxVal = 2^(maxBits-1) -- assuming signed integers, which, for now, are the only ones in use 133 | assert(type(arg) == "number" and math.floor(arg) == arg 134 | and arg < maxVal and arg >= -maxVal 135 | or (maybeNil and arg == nil), 136 | string.format("Argument \"%s\" of function \"%s\" has to be a whole number <= %d", 137 | argName, func, maxVal)) 138 | end 139 | 140 | -- function wrappers 141 | function discordRPC.initialize(applicationId, autoRegister, optionalSteamId) 142 | local func = "discordRPC.Initialize" 143 | checkStrArg(applicationId, nil, "applicationId", func) 144 | checkArg(autoRegister, "boolean", "autoRegister", func) 145 | if optionalSteamId ~= nil then 146 | checkStrArg(optionalSteamId, nil, "optionalSteamId", func) 147 | end 148 | 149 | local eventHandlers = ffi.new("struct DiscordEventHandlers") 150 | eventHandlers.ready = ready_proxy 151 | eventHandlers.disconnected = disconnected_proxy 152 | eventHandlers.errored = errored_proxy 153 | eventHandlers.joinGame = joinGame_proxy 154 | eventHandlers.spectateGame = spectateGame_proxy 155 | eventHandlers.joinRequest = joinRequest_proxy 156 | 157 | discordRPClib.Discord_Initialize(applicationId, eventHandlers, 158 | autoRegister and 1 or 0, optionalSteamId) 159 | end 160 | 161 | function discordRPC.shutdown() 162 | discordRPClib.Discord_Shutdown() 163 | end 164 | 165 | function discordRPC.runCallbacks() 166 | discordRPClib.Discord_RunCallbacks() 167 | end 168 | -- http://luajit.org/ext_ffi_semantics.html#callback : 169 | -- It is not allowed, to let an FFI call into a C function (runCallbacks) 170 | -- get JIT-compiled, which in turn calls a callback, calling into Lua again (e.g. discordRPC.ready). 171 | -- Usually this attempt is caught by the interpreter first and the C function 172 | -- is blacklisted for compilation. 173 | -- solution: 174 | -- "Then you'll need to manually turn off JIT-compilation with jit.off() for 175 | -- the surrounding Lua function that invokes such a message polling function." 176 | jit.off(discordRPC.runCallbacks) 177 | 178 | function discordRPC.updatePresence(presence) 179 | local func = "discordRPC.updatePresence" 180 | checkArg(presence, "table", "presence", func) 181 | 182 | -- -1 for string length because of 0-termination 183 | checkStrArg(presence.state, 127, "presence.state", func, true) 184 | checkStrArg(presence.details, 127, "presence.details", func, true) 185 | 186 | checkIntArg(presence.startTimestamp, 64, "presence.startTimestamp", func, true) 187 | checkIntArg(presence.endTimestamp, 64, "presence.endTimestamp", func, true) 188 | 189 | checkStrArg(presence.largeImageKey, 31, "presence.largeImageKey", func, true) 190 | checkStrArg(presence.largeImageText, 127, "presence.largeImageText", func, true) 191 | checkStrArg(presence.smallImageKey, 31, "presence.smallImageKey", func, true) 192 | checkStrArg(presence.smallImageText, 127, "presence.smallImageText", func, true) 193 | checkStrArg(presence.partyId, 127, "presence.partyId", func, true) 194 | 195 | checkIntArg(presence.partySize, 32, "presence.partySize", func, true) 196 | checkIntArg(presence.partyMax, 32, "presence.partyMax", func, true) 197 | 198 | checkStrArg(presence.matchSecret, 127, "presence.matchSecret", func, true) 199 | checkStrArg(presence.joinSecret, 127, "presence.joinSecret", func, true) 200 | checkStrArg(presence.spectateSecret, 127, "presence.spectateSecret", func, true) 201 | 202 | checkIntArg(presence.instance, 8, "presence.instance", func, true) 203 | 204 | local cpresence = ffi.new("struct DiscordRichPresence") 205 | cpresence.state = presence.state 206 | cpresence.details = presence.details 207 | cpresence.startTimestamp = presence.startTimestamp or 0 208 | cpresence.endTimestamp = presence.endTimestamp or 0 209 | cpresence.largeImageKey = presence.largeImageKey 210 | cpresence.largeImageText = presence.largeImageText 211 | cpresence.smallImageKey = presence.smallImageKey 212 | cpresence.smallImageText = presence.smallImageText 213 | cpresence.partyId = presence.partyId 214 | cpresence.partySize = presence.partySize or 0 215 | cpresence.partyMax = presence.partyMax or 0 216 | cpresence.matchSecret = presence.matchSecret 217 | cpresence.joinSecret = presence.joinSecret 218 | cpresence.spectateSecret = presence.spectateSecret 219 | cpresence.instance = presence.instance or 0 220 | 221 | discordRPClib.Discord_UpdatePresence(cpresence) 222 | end 223 | 224 | function discordRPC.clearPresence() 225 | discordRPClib.Discord_ClearPresence() 226 | end 227 | 228 | local replyMap = { 229 | no = 0, 230 | yes = 1, 231 | ignore = 2 232 | } 233 | 234 | -- maybe let reply take ints too (0, 1, 2) and add constants to the module 235 | function discordRPC.respond(userId, reply) 236 | checkStrArg(userId, nil, "userId", "discordRPC.respond") 237 | assert(replyMap[reply], "Argument 'reply' to discordRPC.respond has to be one of \"yes\", \"no\" or \"ignore\"") 238 | discordRPClib.Discord_Respond(userId, replyMap[reply]) 239 | end 240 | 241 | -- garbage collection callback 242 | getmetatable(discordRPC.gcDummy).__gc = function() 243 | discordRPC.shutdown() 244 | ready_proxy:free() 245 | disconnected_proxy:free() 246 | errored_proxy:free() 247 | joinGame_proxy:free() 248 | spectateGame_proxy:free() 249 | joinRequest_proxy:free() 250 | end 251 | 252 | return discordRPC 253 | --------------------------------------------------------------------------------