if SYSTEM=='Web' then return { update=NULL } end local appId='1288557386700951554' local ffi=require"ffi" local RPC_C if SYSTEM=='Windows' then local suc suc,RPC_C=pcall(ffi.load,"discord-rpc") if not (suc and RPC_C) then print("Failed to load Discord-RPC lib",RPC_C) MES.new('error',"Failed to load Discord-RPC lib") RPC_C=nil end end local RPC if RPC_C then 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 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 RPC.ready then RPC.ready(unpackDiscordUser(request)) end end) local disconnected_proxy=ffi.cast("disconnectedPtr",function(errorCode,message) if RPC.disconnected then RPC.disconnected(errorCode,ffi.string(message)) end end) local errored_proxy=ffi.cast("erroredPtr",function(errorCode,message) if RPC.errored then RPC.errored(errorCode,ffi.string(message)) end end) local joinGame_proxy=ffi.cast("joinGamePtr",function(joinSecret) if RPC.joinGame then RPC.joinGame(ffi.string(joinSecret)) end end) local spectateGame_proxy=ffi.cast("spectateGamePtr",function(spectateSecret) if RPC.spectateGame then RPC.spectateGame(ffi.string(spectateSecret)) end end) local joinRequest_proxy=ffi.cast("joinRequestPtr",function(request) if RPC.joinRequest then RPC.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 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 RPC.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 RPC_C.Discord_Initialize(applicationId,eventHandlers, autoRegister and 1 or 0,optionalSteamId) end function RPC.shutdown() RPC_C.Discord_Shutdown() end function RPC.runCallbacks() RPC_C.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(RPC.runCallbacks) function RPC.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 RPC_C.Discord_UpdatePresence(cpresence) end function RPC.clearPresence() RPC_C.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 RPC.respond(userId,reply) checkStrArg(userId,nil,"userId","discordRPC.respond") assert(replyMap[reply],"Argument 'reply' to discordRPC.respond must be 'yes'|'no'|'ignore'") RPC_C.Discord_Respond(userId,replyMap[reply]) end -- garbage collection callback RPC.gcDummy=newproxy(true) getmetatable(RPC.gcDummy).__gc=function() RPC.shutdown() ready_proxy:free() disconnected_proxy:free() errored_proxy:free() joinGame_proxy:free() spectateGame_proxy:free() joinRequest_proxy:free() end function RPC.ready(userId,username,discriminator,avatar) print(string.format("Discord: ready (%s,%s,%s,%s)",userId,username,discriminator,avatar)) end function RPC.disconnected(errorCode,message) print(string.format("Discord: disconnected (%d: %s)",errorCode,message)) end function RPC.errored(errorCode,message) print(string.format("Discord: error (%d: %s)",errorCode,message)) end function RPC.joinGame(joinSecret) print(string.format("Discord: join (%s)",joinSecret)) end function RPC.spectateGame(spectateSecret) print(string.format("Discord: spectate (%s)",spectateSecret)) end function RPC.joinRequest(userId,username,discriminator,avatar) print(string.format("Discord: join request (%s,%s,%s,%s)",userId,username,discriminator,avatar)) RPC.respond(userId,'yes') end RPC.initialize(appId,true) end local MyRPC={ C=RPC_C, RPC=RPC, presence={ startTimestamp=os.time(), state="Loading...", details="", largeImageKey='', largeImageText="Techmino", smallImageKey='', smallImageText="", }, } ---@class DiscordRPC.presence ---@field state? string ---@field details? string ---@field startTimestamp? number ---@field endTimestamp? number ---@field largeImageKey? string ---@field largeImageText? string ---@field smallImageKey? string ---@field smallImageText? string ---@field partyId? string ---@field partySize? number ---@field partyMax? number ---@field matchSecret? string ---@field joinSecret? string ---@field spectateSecret? string ---@field instance? number ---@overload fun() ---@overload fun(state: DiscordRPC.presence) ---@param state string ---@param details string function MyRPC.update(state,details) if state then for k,v in next, type(state)=='string' and {state=state,details=details} or state do MyRPC.presence[k]=v end end if RPC then RPC.updatePresence(MyRPC.presence) end end return MyRPC