diff --git a/discord-rpc.dll b/discord-rpc.dll new file mode 100644 index 0000000..8493c54 Binary files /dev/null and b/discord-rpc.dll differ diff --git a/discordRPC.lua b/discordRPC.lua new file mode 100644 index 0000000..d6ba95a --- /dev/null +++ b/discordRPC.lua @@ -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 diff --git a/load/sounds.lua b/load/sounds.lua index 6da3486..c00856f 100644 --- a/load/sounds.lua +++ b/load/sounds.lua @@ -10,6 +10,10 @@ sounds = { }, move = love.audio.newSource("res/se/move.wav", "static"), bottom = love.audio.newSource("res/se/bottom.wav", "static"), + cursor = love.audio.newSource("res/se/cursor.wav", "static"), + cursor_lr = love.audio.newSource("res/se/cursor_lr.wav", "static"), + main_decide = love.audio.newSource("res/se/main_decide.wav", "static"), + mode_decide = love.audio.newSource("res/se/mode_decide.wav", "static"), } function playSE(sound, subsound) diff --git a/main.lua b/main.lua index a83d732..d74761c 100644 --- a/main.lua +++ b/main.lua @@ -1,4 +1,45 @@ +discordRPC = require("discordRPC") +appId = "599778517789573120" + +function discordRPC.ready(userId, username, discriminator, avatar) + print(string.format("Discord: ready (%s, %s, %s, %s)", userId, username, discriminator, avatar)) +end + +function discordRPC.disconnected(errorCode, message) + print(string.format("Discord: disconnected (%d: %s)", errorCode, message)) +end + +function discordRPC.errored(errorCode, message) + print(string.format("Discord: error (%d: %s)", errorCode, message)) +end + +function discordRPC.joinGame(joinSecret) + print(string.format("Discord: join (%s)", joinSecret)) +end + +function discordRPC.spectateGame(spectateSecret) + print(string.format("Discord: spectate (%s)", spectateSecret)) +end + +function discordRPC.joinRequest(userId, username, discriminator, avatar) + print(string.format("Discord: join request (%s, %s, %s, %s)", userId, username, discriminator, avatar)) + discordRPC.respond(userId, "yes") +end + function love.load() + + discordRPC.initialize(appId, true) + local now = os.time(os.date("*t")) + presence = { + startTimestamp = now, + details = "Loading game...", + state = "", + largeImageKey = "icon2", + largeImageText = "Original game by Joe Zeng", + smallImageKey = "", + smallImageText = "" + } + math.randomseed(os.time()) highscores = {} require "load.graphics" @@ -64,7 +105,7 @@ end function love.draw() love.graphics.push() - + -- get offset matrix love.graphics.setDefaultFilter("linear", "nearest") local width = love.graphics.getWidth() diff --git a/res/se/cursor.wav b/res/se/cursor.wav new file mode 100644 index 0000000..b55ae9a Binary files /dev/null and b/res/se/cursor.wav differ diff --git a/res/se/cursor_lr.wav b/res/se/cursor_lr.wav new file mode 100644 index 0000000..9e7245e Binary files /dev/null and b/res/se/cursor_lr.wav differ diff --git a/res/se/main_decide.wav b/res/se/main_decide.wav new file mode 100644 index 0000000..63944ad Binary files /dev/null and b/res/se/main_decide.wav differ diff --git a/res/se/mode_decide.wav b/res/se/mode_decide.wav new file mode 100644 index 0000000..7e31e50 Binary files /dev/null and b/res/se/mode_decide.wav differ diff --git a/scene/game.lua b/scene/game.lua index 6aaffd9..e4e4fce 100644 --- a/scene/game.lua +++ b/scene/game.lua @@ -5,6 +5,13 @@ function GameScene:new(game_mode, ruleset) self.game = game_mode() self.ruleset = ruleset() self.game:initialize(self.ruleset) + if game_mode.name == "Demon Mode" and math.random(1, 7) == 7 then + presence.details = "Suffering" + else + presence.details = "In game" + end + presence.state = game_mode.name + discordRPC.updatePresence(presence) end function GameScene:update() @@ -66,7 +73,7 @@ function GameScene:onKeyPress(e) -- fuck this, this is hacky but the way this codebase is setup prevents anything else -- it seems like all the values that get touched in the child gamemode class -- stop being linked to the values of the GameMode superclass because of how `mt.__index` works - -- not even sure this is the actual problem, but I don't want to have to rebuild everything about + -- not even sure this is the actual problem, but I don't want to have to rebuild everything about -- the core organisation of everything. this hacky way will have to do until someone figures out something. love.keypressed("escape", "escape", false) love.keypressed("return", "return", false) diff --git a/scene/input_config.lua b/scene/input_config.lua index 0ed4b76..86dd305 100644 --- a/scene/input_config.lua +++ b/scene/input_config.lua @@ -22,6 +22,10 @@ function ConfigScene:new() -- load current config self.config = config.input self.input_state = 1 + + presence.details = "In menus" + presence.state = "Changing input config" + discordRPC.updatePresence(presence) end function ConfigScene:update() @@ -34,7 +38,7 @@ function ConfigScene:render() 0, 0, 0, 0.5, 0.5 ) - + love.graphics.setFont(font_3x5_2) for i, input in pairs(configurable_inputs) do if config.input[input] then diff --git a/scene/mode_select.lua b/scene/mode_select.lua index ba40048..0a92dd3 100755 --- a/scene/mode_select.lua +++ b/scene/mode_select.lua @@ -11,7 +11,7 @@ game_modes = { --require 'tetris.modes.strategy', --require 'tetris.modes.interval_training', --require 'tetris.modes.pacer_test', - --require 'tetris.modes.demon_mode', + require 'tetris.modes.demon_mode', require 'tetris.modes.phantom_mania', require 'tetris.modes.phantom_mania2', require 'tetris.modes.phantom_mania_n', @@ -47,6 +47,9 @@ function ModeSelectScene:new() ruleset = current_ruleset, select = "mode", } + presence.details = "In menus" + presence.state = "Choosing a mode" + discordRPC.updatePresence(presence) end function ModeSelectScene:update() @@ -58,7 +61,7 @@ function ModeSelectScene:render() 0, 0, 0, 0.5, 0.5 ) - + if self.menu_state.select == "mode" then love.graphics.setColor(1, 1, 1, 0.5) elseif self.menu_state.select == "ruleset" then @@ -72,7 +75,7 @@ function ModeSelectScene:render() love.graphics.setColor(1, 1, 1, 0.5) end love.graphics.rectangle("fill", 340, 78 + 20 * self.menu_state.ruleset, 200, 22) - + love.graphics.setColor(1, 1, 1, 1) love.graphics.draw(misc_graphics["select_mode"], 20, 40) @@ -92,15 +95,19 @@ function ModeSelectScene:onKeyPress(e) current_ruleset = self.menu_state.ruleset config.current_mode = current_mode config.current_ruleset = current_ruleset + playSE("mode_decide") saveConfig() scene = GameScene(game_modes[self.menu_state.mode], rulesets[self.menu_state.ruleset]) elseif (e.scancode == config.input["up"] or e.scancode == "up") and e.isRepeat == false then self:changeOption(-1) + playSE("cursor") elseif (e.scancode == config.input["down"] or e.scancode == "down") and e.isRepeat == false then self:changeOption(1) + playSE("cursor") elseif (e.scancode == config.input["left"] or e.scancode == "left") or (e.scancode == config.input["right"] or e.scancode == "right") then self:switchSelect() + playSE("cursor_lr") elseif e.scancode == "escape" then scene = TitleScene() end diff --git a/scene/title.lua b/scene/title.lua index 0908a80..60f282a 100644 --- a/scene/title.lua +++ b/scene/title.lua @@ -6,8 +6,13 @@ local main_menu_screens = { ExitScene, } +local mainmenuidle = {"Idle", "Twiddling their thumbs", "Admiring the main menu's BG", "Waiting for spring to come"} + function TitleScene:new() self.main_menu_state = 1 + presence.details = "In menus" + presence.state = mainmenuidle[math.random(#mainmenuidle)] + discordRPC.updatePresence(presence) end function TitleScene:update() @@ -39,11 +44,14 @@ end function TitleScene:onKeyPress(e) if e.scancode == "return" and e.isRepeat == false then + playSE("main_decide") scene = main_menu_screens[self.main_menu_state]() elseif (e.scancode == config.input["up"] or e.scancode == "up") and e.isRepeat == false then self:changeOption(-1) + playSE("cursor") elseif (e.scancode == config.input["down"] or e.scancode == "down") and e.isRepeat == false then self:changeOption(1) + playSE("cursor") elseif e.scancode == "escape" and e.isRepeat == false then love.event.quit() end