diff --git a/.vscode/settings.json b/.vscode/settings.json index 74e9231..d465b13 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,8 @@ "Lua.diagnostics.disable": [ "different-requires", "need-check-nil", - "cast-local-type" + "cast-local-type", + "undefined-field", + "inject-field" ] } \ No newline at end of file diff --git a/characters/bf.json b/characters/bf.json index 10e2a8c..ad50fd7 100644 --- a/characters/bf.json +++ b/characters/bf.json @@ -126,7 +126,7 @@ -26, 5 ], - "loop": true, + "loop": false, "fps": 24, "anim": "scared", "indices": [], diff --git a/characters/gf.json b/characters/gf.json index ee7a565..508db68 100644 --- a/characters/gf.json +++ b/characters/gf.json @@ -176,7 +176,7 @@ "name": "GF Dancing Beat Hair Landing" }, { - "loop": true, + "loop": false, "offsets": [ -2, -17 diff --git a/discord_app.lua b/discord_app.lua new file mode 100644 index 0000000..b8781bd --- /dev/null +++ b/discord_app.lua @@ -0,0 +1 @@ +return "1380367531314778122" \ No newline at end of file diff --git a/main.lua b/main.lua index 3db735f..343c785 100644 --- a/main.lua +++ b/main.lua @@ -5,6 +5,8 @@ local myTypes = require("modules.types") local files = require("modules.files") local json = require("modules.json") local logging = require("modules.logging") +local discord = require("modules.discord") +local applicationid = require("discord_app") local songs = require("charts.songs") @@ -41,16 +43,19 @@ local function setup() logo.position = myTypes.Vector2(-80, 0) freaky:play() + + local timeStamp = os.time() + + discord.updatePresence({ + details = "In menu", + largeImageKey = "bigimage", + startTimestamp = timeStamp, + state = "TaggedEngine: FNF in LUA, better than ever." + }) end local font = love.graphics.newFont("fonts/Phantomuff.ttf", 40) --- for index, song in next, songs do --- curSong = index --- curDiff = song[1] --- break --- end - local gettingKey local settings = json.parse(files.read_file("settings.json")) @@ -165,4 +170,6 @@ end love.window.setMode(1280, 720, { fullscreen = false , resizable = true}) +discord.initialize(applicationid, true) + setup() \ No newline at end of file diff --git a/modules/discord.lua b/modules/discord.lua new file mode 100644 index 0000000..26cbf19 --- /dev/null +++ b/modules/discord.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 \ No newline at end of file diff --git a/modules/states/playstate.lua b/modules/states/playstate.lua index efd57f7..a43537e 100644 --- a/modules/states/playstate.lua +++ b/modules/states/playstate.lua @@ -10,6 +10,7 @@ local function state(songName, songDifficulty) local logger = require("modules.logging") local socket = require("socket") local logging= require("modules.logging") + local discord = require("modules.discord") -- I NEED THEM IMPORTS local startTime = 0 @@ -71,6 +72,7 @@ local function state(songName, songDifficulty) shit = 0, miss = 0, } + local score = 0 local rankWindows = { { @@ -142,6 +144,8 @@ local function state(songName, songDifficulty) local deadBF local restart = false + local startTimestamp = os.time() + local function quit() if restart then return end playing = false @@ -239,6 +243,8 @@ local function state(songName, songDifficulty) notes[closestIndex] = nil closestNote:destroy() + + score = score + rating.score end end @@ -300,6 +306,12 @@ local function state(songName, songDifficulty) end if beat % 4 == 0 then zoom = zoom + .1 + discord.updatePresence({ + details = string.format("Playing %s on difficulty %s", songName, songDifficulty), + largeImageKey = "bigimage", + startTimestamp = startTimestamp, + state = string.format("Score: %s", score) + }) end else if characters.gf and not characters.gf.singing then @@ -365,7 +377,7 @@ local function state(songName, songDifficulty) for index, note in next, notes do if note.mustPress then note.sprite.position = myTypes.Vector2(600 + (79 * (note.direction - 1)), settings.Downscroll and 430 - (note.position-elapsed) * speed or (note.position - elapsed) * speed) - if note.position - elapsed < -150 then + if (note.position - elapsed) * speed < -150 then note:destroy() miss:stop() miss:play() @@ -373,10 +385,11 @@ local function state(songName, songDifficulty) notes[index] = nil ratings.miss = ratings.miss + 1 health = health - note.missHealth + score = score - 200 end else note.sprite.position = myTypes.Vector2(50 + 79 * (note.direction - 1), settings.Downscroll and 430 - (note.position-elapsed) * speed or (note.position - elapsed) * speed) - if note.position - elapsed < 10 then + if (note.position - elapsed) * speed < 10 then notes[index] = nil if section.gfSection or chart.song == "Tutorial" then if section.altAnim or note.altAnim then @@ -400,7 +413,7 @@ local function state(songName, songDifficulty) for index, hold in next, holdNotes do if hold.mustPress then hold.sprite.position = myTypes.Vector2(625 + (79 * (hold.direction - 1)), settings.Downscroll and 430 - (hold.position-elapsed) * speed or (hold.position - elapsed) * speed) - if hold.position - elapsed < 10 then + if (hold.position - elapsed) * speed < 10 then if love.keyboard.isDown(keyBinds[hold.direction]) then if characters.bf.animInfo["sing"..directions[hold.direction].."-alt"] and (section.altAnim or hold.altAnim) then characters.bf:PlayAnimation("sing"..directions[hold.direction].."-alt") @@ -409,20 +422,17 @@ local function state(songName, songDifficulty) end hold:destroy() holdNotes[index] = nil - health = health + hold.hitHealth * 0.1 - elseif hold.position - elapsed < -150 then + health = health + hold.hitHealth * 0.2 + elseif (hold.position - elapsed) * speed < -150 then hold:destroy() - miss:stop() - miss:play() characters.bf:PlayAnimation("sing"..directions[hold.direction].."miss") holdNotes[index] = nil - ratings.miss = ratings.miss + 1 - health = health - hold.missHealth * 0.1 + health = health - hold.missHealth * 0.2 end end else hold.sprite.position = myTypes.Vector2(75 + (79 * (hold.direction - 1)), settings.Downscroll and 430 - (hold.position-elapsed) * speed or (hold.position - elapsed) * speed) - if hold.position - elapsed < 10 then + if (hold.position - elapsed) * speed < 10 then if characters.dad.animInfo["sing"..directions[hold.direction].."-alt"] and (section.altAnim or hold.altAnim) then characters.dad:PlayAnimation("sing"..directions[hold.direction].."-alt") else @@ -651,6 +661,15 @@ local function state(songName, songDifficulty) playing = true startTime = socket.gettime() + + startTimestamp = os.time() + + discord.updatePresence({ + details = string.format("Playing %s on difficulty %s", songName, songDifficulty), + largeImageKey = "bigimage", + startTimestamp = startTimestamp, + state = "Score: 0" + }) end function state.keypressed(key, un, is) @@ -664,6 +683,14 @@ local function state(songName, songDifficulty) voices:pause() end pauseStart = socket.gettime() * 1000 + + local pauseStamp = os.time() + discord.updatePresence({ + details = string.format("Paused %s on difficulty %s", songName, songDifficulty), + largeImageKey = "bigimage", + startTimestamp = pauseStamp, + state = string.format("Score: %s", score) + }) else inst:play() if chart.needsVoices then