package funkin.backend.utils; import flixel.util.typeLimit.OneOfTwo; import openfl.display.BitmapData; import funkin.backend.system.macros.Utils; import funkin.backend.scripting.events.DiscordPresenceUpdateEvent; import haxe.Json; import flixel.sound.FlxSound; #if DISCORD_RPC import hxdiscord_rpc.Discord; import hxdiscord_rpc.Types; import sys.thread.Thread; import Sys; import lime.app.Application; #end class DiscordUtil { public static var currentID(default, set):String = null; public static var discordThread:#if DISCORD_RPC Thread #else Dynamic #end = null; public static var ready:Bool = false; private static var stopThread:Bool = false; public static var user:#if DISCORD_RPC DUser #else Dynamic #end = null; public static var lastPresence:#if DISCORD_RPC DPresence #else Dynamic #end = null; public static var events:#if DISCORD_RPC DEvents #else Dynamic #end = null; public static var config:#if DISCORD_RPC DiscordJson #else Dynamic #end = null; public static function init() { #if DISCORD_RPC events = {}; reloadJsonData(); discordThread = Thread.create(function() { while (true) { while (!stopThread) { #if DISCORD_DISABLE_IO_THREAD Discord.UpdateConnection(); #end Discord.RunCallbacks(); Sys.sleep(2); } Sys.sleep(1); // to reduce cpu } }); Application.current.onExit.add(function(_) shutdown()); #end } public static function reloadJsonData() { #if DISCORD_RPC var jsonPath = Paths.json("config/discord"); if (Assets.exists(jsonPath)) { try config = Json.parse(Assets.getText(jsonPath)) catch (e) Logs.trace('Couldn\'t load Discord RPC configuration: ${e.toString()}', ERROR); } else { config = {}; } config.logoKey = config.logoKey.getDefault("icon"); config.logoText = config.logoText.getDefault(Application.current.meta.get('title')); config.clientID = config.clientID.getDefault("1027994136193810442"); currentID = config.clientID; #end } public static function changePresence(details:String, state:String, ?smallImageKey:String) { #if DISCORD_RPC changePresenceAdvanced({ state: state, details: details, smallImageKey: smallImageKey }); #end } public static function changeSongPresence(details:String, state:String, audio:FlxSound, ?smallImageKey:String) { #if DISCORD_RPC var start:Float = 0; var end:Float = 0; if (audio != null && audio.playing) { start = Date.now().getTime(); end = start + (audio.length - audio.time); } changePresenceAdvanced({ state: state, details: details, smallImageKey: smallImageKey, startTimestamp: Std.int(start / 1000), endTimestamp: Std.int(end / 1000) }); #end } @:noCompletion public static function fixString(str:String) { return new cpp.ConstCharStar(cast(str, String)); } @:noCompletion public static function toString(str:cpp.ConstCharStar) { return cast(str, String); } public static function changePresenceAdvanced(data:DPresence) { #if DISCORD_RPC if (data == null) return; // copy last presence if (data.largeImageKey == null) data.largeImageKey = config.logoKey; if (data.largeImageText == null) data.largeImageText = config.logoText; #if GLOBAL_SCRIPT var event = funkin.backend.scripting.GlobalScript.event("onDiscordPresenceUpdate", EventManager.get(DiscordPresenceUpdateEvent).recycle(data)); if (event.cancelled) return; data = event.presence; #end lastPresence = data; var dp:DiscordRichPresence = DiscordRichPresence.create(); // TODO: make this use a reflection-like macro Utils.safeSetWrapper(dp.state, data.state, fixString); Utils.safeSetWrapper(dp.details, data.details, fixString); Utils.safeSet(dp.startTimestamp, data.startTimestamp); Utils.safeSet(dp.endTimestamp, data.endTimestamp); Utils.safeSetWrapper(dp.largeImageKey, data.largeImageKey, fixString); Utils.safeSetWrapper(dp.largeImageText, data.largeImageText, fixString); Utils.safeSetWrapper(dp.smallImageKey, data.smallImageKey, fixString); Utils.safeSetWrapper(dp.smallImageText, data.smallImageText, fixString); Utils.safeSetWrapper(dp.partyId, data.partyId, fixString); Utils.safeSet(dp.partySize, data.partySize); Utils.safeSet(dp.partyMax, data.partyMax); Utils.safeSet(dp.partyPrivacy, data.partyPrivacy); Utils.safeSetWrapper(dp.matchSecret, data.matchSecret, fixString); Utils.safeSetWrapper(dp.joinSecret, data.joinSecret, fixString); Utils.safeSetWrapper(dp.spectateSecret, data.spectateSecret, fixString); Utils.safeSet(dp.instance, data.instance); Discord.UpdatePresence(cpp.RawConstPointer.addressOf(dp)); #end } private static function set_currentID(id:String):String { if (currentID == id) return id; #if DISCORD_RPC if (currentID != null) shutdown(); var handlers:DiscordEventHandlers = DiscordEventHandlers.create(); handlers.ready = cpp.Function.fromStaticFunction(onReady); handlers.disconnected = cpp.Function.fromStaticFunction(onDisconnected); handlers.errored = cpp.Function.fromStaticFunction(onError); handlers.joinGame = cpp.Function.fromStaticFunction(onJoin); handlers.joinRequest = cpp.Function.fromStaticFunction(onJoinReq); handlers.spectateGame = cpp.Function.fromStaticFunction(onSpectate); Discord.Initialize(id, cpp.RawPointer.addressOf(handlers), 1, null); stopThread = false; #end return currentID = id; } public static function shutdown() { ready = false; stopThread = true; #if DISCORD_RPC Discord.Shutdown(); #end } // HANDLERS #if DISCORD_RPC private static function onReady(request:cpp.RawConstPointer):Void { user = new DUser(); user.init(cpp.ConstPointer.fromRaw(request).ptr); Logs.traceColored([ Logs.logText("[Discord] ", BLUE), Logs.logText("Connected to User ("), Logs.logText(user.tag, GRAY), Logs.logText(")") ], INFO); ready = true; if(events.ready != null) events.ready(user); } private static function onDisconnected(errorCode:Int, message:cpp.ConstCharStar):Void { var finalMsg:String = cast (message, String); Logs.traceColored([ Logs.logText("[Discord] ", BLUE), Logs.logText("Disconnected ("), Logs.logText('$errorCode: $finalMsg', RED), Logs.logText(")") ], INFO); if(events.disconnected != null) events.disconnected(errorCode, finalMsg); } private static function onError(errorCode:Int, message:cpp.ConstCharStar):Void { var finalMsg:String = cast (message, String); Logs.traceColored([ Logs.logText("[Discord] ", BLUE), Logs.logText("Error ("), Logs.logText('$errorCode: $finalMsg', RED), Logs.logText(")") ], ERROR); if(events.errored != null) events.errored(errorCode, finalMsg); } private static function onJoin(joinSecret:cpp.ConstCharStar):Void { Logs.traceColored([ Logs.logText("[Discord] ", BLUE), Logs.logText("Someone has just joined", GREEN) ], INFO); if(events.joinGame != null) events.joinGame(cast (joinSecret, String)); } private static function onSpectate(spectateSecret:cpp.ConstCharStar):Void { Logs.traceColored([ Logs.logText("[Discord] ", BLUE), Logs.logText("Someone started spectating your game", YELLOW) ], INFO); if(events.spectateGame != null) events.spectateGame(cast (spectateSecret, String)); } private static function onJoinReq(request:cpp.RawConstPointer):Void { Logs.traceColored([ Logs.logText("[Discord] ", BLUE), Logs.logText("Someone has just requested to join", YELLOW) ], WARNING); if(events.joinRequest == null) return; var req:DUser = new DUser(); req.init(cpp.ConstPointer.fromRaw(request).ptr); events.joinRequest(req); } #end } typedef DiscordJson = { var ?clientID:String; var ?logoKey:String; var ?logoText:String; } @:noCustomClass final class DUser { /** * The username + discriminator if they have it **/ public var tag:String; /** * The user id, aka 860561967383445535 **/ public var userId:String; /** * The user's username **/ public var username:String; /** * The #number from before discord changed to usernames only, if the user has changed to a username them its just a 0 **/ public var discriminator:Int; /** * The user's avatar filename **/ public var avatar:String; public function new() { } public function init(userData:cpp.Star) { userId = userData.userId; username = userData.username; discriminator = Std.parseInt(userData.discriminator); avatar = userData.avatar; if (discriminator != 0) tag = '${username}#${discriminator}'; else tag = '${username}'; } /** * Calling this function gets the BitmapData of the user **/ public function getAvatar(size:Int = 256):BitmapData return BitmapData.fromBytes(HttpUtil.requestBytes('https://cdn.discordapp.com/avatars/$userId/$avatar.png?size=$size')); } typedef DPresence = { var ?state:String; /* max 128 bytes */ var ?details:String; /* max 128 bytes */ var ?startTimestamp:OneOfTwo; var ?endTimestamp:OneOfTwo; var ?largeImageKey:String; /* max 32 bytes */ var ?largeImageText:String; /* max 128 bytes */ var ?smallImageKey:String; /* max 32 bytes */ var ?smallImageText:String; /* max 128 bytes */ var ?partyId:String; /* max 128 bytes */ var ?partySize:Int; var ?partyMax:Int; var ?partyPrivacy:Int; var ?matchSecret:String; /* max 128 bytes */ var ?joinSecret:String; /* max 128 bytes */ var ?spectateSecret:String; /* max 128 bytes */ var ?instance:OneOfTwo; } typedef DEvents = { var ?ready:DUser->Void; var ?disconnected:(errorCode:Int, message:String)->Void; var ?errored:(errorCode:Int, message:String)->Void; var ?joinGame:String->Void; var ?spectateGame:String->Void; var ?joinRequest:DUser->Void; }