You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

252 lines
9.1 KiB

  1. local ffi = require "ffi"
  2. local discordRPClib = ffi.load("discord-rpc")
  3. ffi.cdef[[
  4. typedef struct DiscordRichPresence {
  5. const char* state; /* max 128 bytes */
  6. const char* details; /* max 128 bytes */
  7. int64_t startTimestamp;
  8. int64_t endTimestamp;
  9. const char* largeImageKey; /* max 32 bytes */
  10. const char* largeImageText; /* max 128 bytes */
  11. const char* smallImageKey; /* max 32 bytes */
  12. const char* smallImageText; /* max 128 bytes */
  13. const char* partyId; /* max 128 bytes */
  14. int partySize;
  15. int partyMax;
  16. const char* matchSecret; /* max 128 bytes */
  17. const char* joinSecret; /* max 128 bytes */
  18. const char* spectateSecret; /* max 128 bytes */
  19. int8_t instance;
  20. } DiscordRichPresence;
  21. typedef struct DiscordUser {
  22. const char* userId;
  23. const char* username;
  24. const char* discriminator;
  25. const char* avatar;
  26. } DiscordUser;
  27. typedef void (*readyPtr)(const DiscordUser* request);
  28. typedef void (*disconnectedPtr)(int errorCode, const char* message);
  29. typedef void (*erroredPtr)(int errorCode, const char* message);
  30. typedef void (*joinGamePtr)(const char* joinSecret);
  31. typedef void (*spectateGamePtr)(const char* spectateSecret);
  32. typedef void (*joinRequestPtr)(const DiscordUser* request);
  33. typedef struct DiscordEventHandlers {
  34. readyPtr ready;
  35. disconnectedPtr disconnected;
  36. erroredPtr errored;
  37. joinGamePtr joinGame;
  38. spectateGamePtr spectateGame;
  39. joinRequestPtr joinRequest;
  40. } DiscordEventHandlers;
  41. void Discord_Initialize(const char* applicationId,
  42. DiscordEventHandlers* handlers,
  43. int autoRegister,
  44. const char* optionalSteamId);
  45. void Discord_Shutdown(void);
  46. void Discord_RunCallbacks(void);
  47. void Discord_UpdatePresence(const DiscordRichPresence* presence);
  48. void Discord_ClearPresence(void);
  49. void Discord_Respond(const char* userid, int reply);
  50. void Discord_UpdateHandlers(DiscordEventHandlers* handlers);
  51. ]]
  52. local discordRPC = {} -- module table
  53. -- proxy to detect garbage collection of the module
  54. discordRPC.gcDummy = newproxy(true)
  55. local function unpackDiscordUser(request)
  56. return ffi.string(request.userId), ffi.string(request.username),
  57. ffi.string(request.discriminator), ffi.string(request.avatar)
  58. end
  59. -- callback proxies
  60. -- note: callbacks are not JIT compiled (= SLOW), try to avoid doing performance critical tasks in them
  61. -- luajit.org/ext_ffi_semantics.html
  62. local ready_proxy = ffi.cast("readyPtr", function(request)
  63. if discordRPC.ready then
  64. discordRPC.ready(unpackDiscordUser(request))
  65. end
  66. end)
  67. local disconnected_proxy = ffi.cast("disconnectedPtr", function(errorCode, message)
  68. if discordRPC.disconnected then
  69. discordRPC.disconnected(errorCode, ffi.string(message))
  70. end
  71. end)
  72. local errored_proxy = ffi.cast("erroredPtr", function(errorCode, message)
  73. if discordRPC.errored then
  74. discordRPC.errored(errorCode, ffi.string(message))
  75. end
  76. end)
  77. local joinGame_proxy = ffi.cast("joinGamePtr", function(joinSecret)
  78. if discordRPC.joinGame then
  79. discordRPC.joinGame(ffi.string(joinSecret))
  80. end
  81. end)
  82. local spectateGame_proxy = ffi.cast("spectateGamePtr", function(spectateSecret)
  83. if discordRPC.spectateGame then
  84. discordRPC.spectateGame(ffi.string(spectateSecret))
  85. end
  86. end)
  87. local joinRequest_proxy = ffi.cast("joinRequestPtr", function(request)
  88. if discordRPC.joinRequest then
  89. discordRPC.joinRequest(unpackDiscordUser(request))
  90. end
  91. end)
  92. -- helpers
  93. local function checkArg(arg, argType, argName, func, maybeNil)
  94. assert(type(arg) == argType or (maybeNil and arg == nil),
  95. string.format("Argument \"%s\" to function \"%s\" has to be of type \"%s\"",
  96. argName, func, argType))
  97. end
  98. local function checkStrArg(arg, maxLen, argName, func, maybeNil)
  99. if maxLen then
  100. assert(type(arg) == "string" and arg:len() <= maxLen or (maybeNil and arg == nil),
  101. string.format("Argument \"%s\" of function \"%s\" has to be of type string with maximum length %d",
  102. argName, func, maxLen))
  103. else
  104. checkArg(arg, "string", argName, func, true)
  105. end
  106. end
  107. local function checkIntArg(arg, maxBits, argName, func, maybeNil)
  108. maxBits = math.min(maxBits or 32, 52) -- lua number (double) can only store integers < 2^53
  109. local maxVal = 2^(maxBits-1) -- assuming signed integers, which, for now, are the only ones in use
  110. assert(type(arg) == "number" and math.floor(arg) == arg
  111. and arg < maxVal and arg >= -maxVal
  112. or (maybeNil and arg == nil),
  113. string.format("Argument \"%s\" of function \"%s\" has to be a whole number <= %d",
  114. argName, func, maxVal))
  115. end
  116. -- function wrappers
  117. function discordRPC.initialize(applicationId, autoRegister, optionalSteamId)
  118. local func = "discordRPC.Initialize"
  119. checkStrArg(applicationId, nil, "applicationId", func)
  120. checkArg(autoRegister, "boolean", "autoRegister", func)
  121. if optionalSteamId ~= nil then
  122. checkStrArg(optionalSteamId, nil, "optionalSteamId", func)
  123. end
  124. local eventHandlers = ffi.new("struct DiscordEventHandlers")
  125. eventHandlers.ready = ready_proxy
  126. eventHandlers.disconnected = disconnected_proxy
  127. eventHandlers.errored = errored_proxy
  128. eventHandlers.joinGame = joinGame_proxy
  129. eventHandlers.spectateGame = spectateGame_proxy
  130. eventHandlers.joinRequest = joinRequest_proxy
  131. discordRPClib.Discord_Initialize(applicationId, eventHandlers,
  132. autoRegister and 1 or 0, optionalSteamId)
  133. end
  134. function discordRPC.shutdown()
  135. discordRPClib.Discord_Shutdown()
  136. end
  137. function discordRPC.runCallbacks()
  138. discordRPClib.Discord_RunCallbacks()
  139. end
  140. -- http://luajit.org/ext_ffi_semantics.html#callback :
  141. -- It is not allowed, to let an FFI call into a C function (runCallbacks)
  142. -- get JIT-compiled, which in turn calls a callback, calling into Lua again (e.g. discordRPC.ready).
  143. -- Usually this attempt is caught by the interpreter first and the C function
  144. -- is blacklisted for compilation.
  145. -- solution:
  146. -- "Then you'll need to manually turn off JIT-compilation with jit.off() for
  147. -- the surrounding Lua function that invokes such a message polling function."
  148. jit.off(discordRPC.runCallbacks)
  149. function discordRPC.updatePresence(presence)
  150. local func = "discordRPC.updatePresence"
  151. checkArg(presence, "table", "presence", func)
  152. -- -1 for string length because of 0-termination
  153. checkStrArg(presence.state, 127, "presence.state", func, true)
  154. checkStrArg(presence.details, 127, "presence.details", func, true)
  155. checkIntArg(presence.startTimestamp, 64, "presence.startTimestamp", func, true)
  156. checkIntArg(presence.endTimestamp, 64, "presence.endTimestamp", func, true)
  157. checkStrArg(presence.largeImageKey, 31, "presence.largeImageKey", func, true)
  158. checkStrArg(presence.largeImageText, 127, "presence.largeImageText", func, true)
  159. checkStrArg(presence.smallImageKey, 31, "presence.smallImageKey", func, true)
  160. checkStrArg(presence.smallImageText, 127, "presence.smallImageText", func, true)
  161. checkStrArg(presence.partyId, 127, "presence.partyId", func, true)
  162. checkIntArg(presence.partySize, 32, "presence.partySize", func, true)
  163. checkIntArg(presence.partyMax, 32, "presence.partyMax", func, true)
  164. checkStrArg(presence.matchSecret, 127, "presence.matchSecret", func, true)
  165. checkStrArg(presence.joinSecret, 127, "presence.joinSecret", func, true)
  166. checkStrArg(presence.spectateSecret, 127, "presence.spectateSecret", func, true)
  167. checkIntArg(presence.instance, 8, "presence.instance", func, true)
  168. local cpresence = ffi.new("struct DiscordRichPresence")
  169. cpresence.state = presence.state
  170. cpresence.details = presence.details
  171. cpresence.startTimestamp = presence.startTimestamp or 0
  172. cpresence.endTimestamp = presence.endTimestamp or 0
  173. cpresence.largeImageKey = presence.largeImageKey
  174. cpresence.largeImageText = presence.largeImageText
  175. cpresence.smallImageKey = presence.smallImageKey
  176. cpresence.smallImageText = presence.smallImageText
  177. cpresence.partyId = presence.partyId
  178. cpresence.partySize = presence.partySize or 0
  179. cpresence.partyMax = presence.partyMax or 0
  180. cpresence.matchSecret = presence.matchSecret
  181. cpresence.joinSecret = presence.joinSecret
  182. cpresence.spectateSecret = presence.spectateSecret
  183. cpresence.instance = presence.instance or 0
  184. discordRPClib.Discord_UpdatePresence(cpresence)
  185. end
  186. function discordRPC.clearPresence()
  187. discordRPClib.Discord_ClearPresence()
  188. end
  189. local replyMap = {
  190. no = 0,
  191. yes = 1,
  192. ignore = 2
  193. }
  194. -- maybe let reply take ints too (0, 1, 2) and add constants to the module
  195. function discordRPC.respond(userId, reply)
  196. checkStrArg(userId, nil, "userId", "discordRPC.respond")
  197. assert(replyMap[reply], "Argument 'reply' to discordRPC.respond has to be one of \"yes\", \"no\" or \"ignore\"")
  198. discordRPClib.Discord_Respond(userId, replyMap[reply])
  199. end
  200. -- garbage collection callback
  201. getmetatable(discordRPC.gcDummy).__gc = function()
  202. discordRPC.shutdown()
  203. ready_proxy:free()
  204. disconnected_proxy:free()
  205. errored_proxy:free()
  206. joinGame_proxy:free()
  207. spectateGame_proxy:free()
  208. joinRequest_proxy:free()
  209. end
  210. return discordRPC