|
|
@@ -0,0 +1,252 @@ |
|
|
|
local ffi = require "ffi" |
|
|
|
local discordRPClib = ffi.load("discord-rpc") |
|
|
|
|
|
|
|
ffi.cdef[[ |
|
|
|
typedef struct DiscordRichPresence { |
|
|
|
const char* state; /* max 128 bytes */ |
|
|
|
const char* details; /* max 128 bytes */ |
|
|
|
int64_t startTimestamp; |
|
|
|
int64_t endTimestamp; |
|
|
|
const char* largeImageKey; /* max 32 bytes */ |
|
|
|
const char* largeImageText; /* max 128 bytes */ |
|
|
|
const char* smallImageKey; /* max 32 bytes */ |
|
|
|
const char* smallImageText; /* max 128 bytes */ |
|
|
|
const char* partyId; /* max 128 bytes */ |
|
|
|
int partySize; |
|
|
|
int partyMax; |
|
|
|
const char* matchSecret; /* max 128 bytes */ |
|
|
|
const char* joinSecret; /* max 128 bytes */ |
|
|
|
const char* spectateSecret; /* max 128 bytes */ |
|
|
|
int8_t instance; |
|
|
|
} DiscordRichPresence; |
|
|
|
|
|
|
|
typedef struct DiscordUser { |
|
|
|
const char* userId; |
|
|
|
const char* username; |
|
|
|
const char* discriminator; |
|
|
|
const char* avatar; |
|
|
|
} DiscordUser; |
|
|
|
|
|
|
|
typedef void (*readyPtr)(const DiscordUser* request); |
|
|
|
typedef void (*disconnectedPtr)(int errorCode, const char* message); |
|
|
|
typedef void (*erroredPtr)(int errorCode, const char* message); |
|
|
|
typedef void (*joinGamePtr)(const char* joinSecret); |
|
|
|
typedef void (*spectateGamePtr)(const char* spectateSecret); |
|
|
|
typedef void (*joinRequestPtr)(const DiscordUser* request); |
|
|
|
|
|
|
|
typedef struct DiscordEventHandlers { |
|
|
|
readyPtr ready; |
|
|
|
disconnectedPtr disconnected; |
|
|
|
erroredPtr errored; |
|
|
|
joinGamePtr joinGame; |
|
|
|
spectateGamePtr spectateGame; |
|
|
|
joinRequestPtr joinRequest; |
|
|
|
} DiscordEventHandlers; |
|
|
|
|
|
|
|
void Discord_Initialize(const char* applicationId, |
|
|
|
DiscordEventHandlers* handlers, |
|
|
|
int autoRegister, |
|
|
|
const char* optionalSteamId); |
|
|
|
|
|
|
|
void Discord_Shutdown(void); |
|
|
|
|
|
|
|
void Discord_RunCallbacks(void); |
|
|
|
|
|
|
|
void Discord_UpdatePresence(const DiscordRichPresence* presence); |
|
|
|
|
|
|
|
void Discord_ClearPresence(void); |
|
|
|
|
|
|
|
void Discord_Respond(const char* userid, int reply); |
|
|
|
|
|
|
|
void Discord_UpdateHandlers(DiscordEventHandlers* handlers); |
|
|
|
]] |
|
|
|
|
|
|
|
local discordRPC = {} -- module table |
|
|
|
|
|
|
|
-- proxy to detect garbage collection of the module |
|
|
|
discordRPC.gcDummy = newproxy(true) |
|
|
|
|
|
|
|
local function unpackDiscordUser(request) |
|
|
|
return ffi.string(request.userId), ffi.string(request.username), |
|
|
|
ffi.string(request.discriminator), ffi.string(request.avatar) |
|
|
|
end |
|
|
|
|
|
|
|
-- callback proxies |
|
|
|
-- note: callbacks are not JIT compiled (= SLOW), try to avoid doing performance critical tasks in them |
|
|
|
-- luajit.org/ext_ffi_semantics.html |
|
|
|
local ready_proxy = ffi.cast("readyPtr", function(request) |
|
|
|
if discordRPC.ready then |
|
|
|
discordRPC.ready(unpackDiscordUser(request)) |
|
|
|
end |
|
|
|
end) |
|
|
|
|
|
|
|
local disconnected_proxy = ffi.cast("disconnectedPtr", function(errorCode, message) |
|
|
|
if discordRPC.disconnected then |
|
|
|
discordRPC.disconnected(errorCode, ffi.string(message)) |
|
|
|
end |
|
|
|
end) |
|
|
|
|
|
|
|
local errored_proxy = ffi.cast("erroredPtr", function(errorCode, message) |
|
|
|
if discordRPC.errored then |
|
|
|
discordRPC.errored(errorCode, ffi.string(message)) |
|
|
|
end |
|
|
|
end) |
|
|
|
|
|
|
|
local joinGame_proxy = ffi.cast("joinGamePtr", function(joinSecret) |
|
|
|
if discordRPC.joinGame then |
|
|
|
discordRPC.joinGame(ffi.string(joinSecret)) |
|
|
|
end |
|
|
|
end) |
|
|
|
|
|
|
|
local spectateGame_proxy = ffi.cast("spectateGamePtr", function(spectateSecret) |
|
|
|
if discordRPC.spectateGame then |
|
|
|
discordRPC.spectateGame(ffi.string(spectateSecret)) |
|
|
|
end |
|
|
|
end) |
|
|
|
|
|
|
|
local joinRequest_proxy = ffi.cast("joinRequestPtr", function(request) |
|
|
|
if discordRPC.joinRequest then |
|
|
|
discordRPC.joinRequest(unpackDiscordUser(request)) |
|
|
|
end |
|
|
|
end) |
|
|
|
|
|
|
|
-- helpers |
|
|
|
local function checkArg(arg, argType, argName, func, maybeNil) |
|
|
|
assert(type(arg) == argType or (maybeNil and arg == nil), |
|
|
|
string.format("Argument \"%s\" to function \"%s\" has to be of type \"%s\"", |
|
|
|
argName, func, argType)) |
|
|
|
end |
|
|
|
|
|
|
|
local function checkStrArg(arg, maxLen, argName, func, maybeNil) |
|
|
|
if maxLen then |
|
|
|
assert(type(arg) == "string" and arg:len() <= maxLen or (maybeNil and arg == nil), |
|
|
|
string.format("Argument \"%s\" of function \"%s\" has to be of type string with maximum length %d", |
|
|
|
argName, func, maxLen)) |
|
|
|
else |
|
|
|
checkArg(arg, "string", argName, func, true) |
|
|
|
end |
|
|
|
end |
|
|
|
|
|
|
|
local function checkIntArg(arg, maxBits, argName, func, maybeNil) |
|
|
|
maxBits = math.min(maxBits or 32, 52) -- lua number (double) can only store integers < 2^53 |
|
|
|
local maxVal = 2^(maxBits-1) -- assuming signed integers, which, for now, are the only ones in use |
|
|
|
assert(type(arg) == "number" and math.floor(arg) == arg |
|
|
|
and arg < maxVal and arg >= -maxVal |
|
|
|
or (maybeNil and arg == nil), |
|
|
|
string.format("Argument \"%s\" of function \"%s\" has to be a whole number <= %d", |
|
|
|
argName, func, maxVal)) |
|
|
|
end |
|
|
|
|
|
|
|
-- function wrappers |
|
|
|
function discordRPC.initialize(applicationId, autoRegister, optionalSteamId) |
|
|
|
local func = "discordRPC.Initialize" |
|
|
|
checkStrArg(applicationId, nil, "applicationId", func) |
|
|
|
checkArg(autoRegister, "boolean", "autoRegister", func) |
|
|
|
if optionalSteamId ~= nil then |
|
|
|
checkStrArg(optionalSteamId, nil, "optionalSteamId", func) |
|
|
|
end |
|
|
|
|
|
|
|
local eventHandlers = ffi.new("struct DiscordEventHandlers") |
|
|
|
eventHandlers.ready = ready_proxy |
|
|
|
eventHandlers.disconnected = disconnected_proxy |
|
|
|
eventHandlers.errored = errored_proxy |
|
|
|
eventHandlers.joinGame = joinGame_proxy |
|
|
|
eventHandlers.spectateGame = spectateGame_proxy |
|
|
|
eventHandlers.joinRequest = joinRequest_proxy |
|
|
|
|
|
|
|
discordRPClib.Discord_Initialize(applicationId, eventHandlers, |
|
|
|
autoRegister and 1 or 0, optionalSteamId) |
|
|
|
end |
|
|
|
|
|
|
|
function discordRPC.shutdown() |
|
|
|
discordRPClib.Discord_Shutdown() |
|
|
|
end |
|
|
|
|
|
|
|
function discordRPC.runCallbacks() |
|
|
|
discordRPClib.Discord_RunCallbacks() |
|
|
|
end |
|
|
|
-- http://luajit.org/ext_ffi_semantics.html#callback : |
|
|
|
-- It is not allowed, to let an FFI call into a C function (runCallbacks) |
|
|
|
-- get JIT-compiled, which in turn calls a callback, calling into Lua again (e.g. discordRPC.ready). |
|
|
|
-- Usually this attempt is caught by the interpreter first and the C function |
|
|
|
-- is blacklisted for compilation. |
|
|
|
-- solution: |
|
|
|
-- "Then you'll need to manually turn off JIT-compilation with jit.off() for |
|
|
|
-- the surrounding Lua function that invokes such a message polling function." |
|
|
|
jit.off(discordRPC.runCallbacks) |
|
|
|
|
|
|
|
function discordRPC.updatePresence(presence) |
|
|
|
local func = "discordRPC.updatePresence" |
|
|
|
checkArg(presence, "table", "presence", func) |
|
|
|
|
|
|
|
-- -1 for string length because of 0-termination |
|
|
|
checkStrArg(presence.state, 127, "presence.state", func, true) |
|
|
|
checkStrArg(presence.details, 127, "presence.details", func, true) |
|
|
|
|
|
|
|
checkIntArg(presence.startTimestamp, 64, "presence.startTimestamp", func, true) |
|
|
|
checkIntArg(presence.endTimestamp, 64, "presence.endTimestamp", func, true) |
|
|
|
|
|
|
|
checkStrArg(presence.largeImageKey, 31, "presence.largeImageKey", func, true) |
|
|
|
checkStrArg(presence.largeImageText, 127, "presence.largeImageText", func, true) |
|
|
|
checkStrArg(presence.smallImageKey, 31, "presence.smallImageKey", func, true) |
|
|
|
checkStrArg(presence.smallImageText, 127, "presence.smallImageText", func, true) |
|
|
|
checkStrArg(presence.partyId, 127, "presence.partyId", func, true) |
|
|
|
|
|
|
|
checkIntArg(presence.partySize, 32, "presence.partySize", func, true) |
|
|
|
checkIntArg(presence.partyMax, 32, "presence.partyMax", func, true) |
|
|
|
|
|
|
|
checkStrArg(presence.matchSecret, 127, "presence.matchSecret", func, true) |
|
|
|
checkStrArg(presence.joinSecret, 127, "presence.joinSecret", func, true) |
|
|
|
checkStrArg(presence.spectateSecret, 127, "presence.spectateSecret", func, true) |
|
|
|
|
|
|
|
checkIntArg(presence.instance, 8, "presence.instance", func, true) |
|
|
|
|
|
|
|
local cpresence = ffi.new("struct DiscordRichPresence") |
|
|
|
cpresence.state = presence.state |
|
|
|
cpresence.details = presence.details |
|
|
|
cpresence.startTimestamp = presence.startTimestamp or 0 |
|
|
|
cpresence.endTimestamp = presence.endTimestamp or 0 |
|
|
|
cpresence.largeImageKey = presence.largeImageKey |
|
|
|
cpresence.largeImageText = presence.largeImageText |
|
|
|
cpresence.smallImageKey = presence.smallImageKey |
|
|
|
cpresence.smallImageText = presence.smallImageText |
|
|
|
cpresence.partyId = presence.partyId |
|
|
|
cpresence.partySize = presence.partySize or 0 |
|
|
|
cpresence.partyMax = presence.partyMax or 0 |
|
|
|
cpresence.matchSecret = presence.matchSecret |
|
|
|
cpresence.joinSecret = presence.joinSecret |
|
|
|
cpresence.spectateSecret = presence.spectateSecret |
|
|
|
cpresence.instance = presence.instance or 0 |
|
|
|
|
|
|
|
discordRPClib.Discord_UpdatePresence(cpresence) |
|
|
|
end |
|
|
|
|
|
|
|
function discordRPC.clearPresence() |
|
|
|
discordRPClib.Discord_ClearPresence() |
|
|
|
end |
|
|
|
|
|
|
|
local replyMap = { |
|
|
|
no = 0, |
|
|
|
yes = 1, |
|
|
|
ignore = 2 |
|
|
|
} |
|
|
|
|
|
|
|
-- maybe let reply take ints too (0, 1, 2) and add constants to the module |
|
|
|
function discordRPC.respond(userId, reply) |
|
|
|
checkStrArg(userId, nil, "userId", "discordRPC.respond") |
|
|
|
assert(replyMap[reply], "Argument 'reply' to discordRPC.respond has to be one of \"yes\", \"no\" or \"ignore\"") |
|
|
|
discordRPClib.Discord_Respond(userId, replyMap[reply]) |
|
|
|
end |
|
|
|
|
|
|
|
-- garbage collection callback |
|
|
|
getmetatable(discordRPC.gcDummy).__gc = function() |
|
|
|
discordRPC.shutdown() |
|
|
|
ready_proxy:free() |
|
|
|
disconnected_proxy:free() |
|
|
|
errored_proxy:free() |
|
|
|
joinGame_proxy:free() |
|
|
|
spectateGame_proxy:free() |
|
|
|
joinRequest_proxy:free() |
|
|
|
end |
|
|
|
|
|
|
|
return discordRPC |