diff --git a/README.md b/README.md index c4439c44..604cd9c2 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,5 @@ In the future (when the engine won't be a WIP anymore) we're gonna also publish - Credits to [Ne_Eo](https://twitter.com/Ne_Eo_Twitch) and the [3D-HaxeFlixel](https://github.com/lunarcleint/3D-HaxeFlixel) repository for Away3D Flixel support - Credits to the [FlxAnimate](https://github.com/Dot-Stuff/flxanimate) team for the Animate Atlas support - Credits to Smokey555 for the backup Animate Atlas to spritesheet code -- Credits to MAJigsaw77 for [hxvlc](https://github.com/MAJigsaw77/hxvlc) for video cutscene/mp4 support -- Credits to Aidan63 for [discord-rpc](https://github.com/Aidan63/linc_discord-rpc) for discord rpc integration +- Credits to MAJigsaw77 for [hxvlc](https://github.com/MAJigsaw77/hxvlc) (video cutscene/mp4 support) and [hxdiscord_rpc](https://github.com/MAJigsaw77/hxdiscord_rpc) (discord rpc integration) diff --git a/assets/data/discord.hx b/assets/data/discord.hx new file mode 100644 index 00000000..9d5f0f6f --- /dev/null +++ b/assets/data/discord.hx @@ -0,0 +1,50 @@ +import funkin.backend.utils.DiscordUtil; + +function onGameOver() { + DiscordUtil.changePresence('Game Over', PlayState.SONG.meta.displayName + " (" + PlayState.difficulty + ")"); +} + +function onDiscordPresenceUpdate(e) { + var data = e.presence; + + if(data.button1Label == null) + data.button1Label = "Codename Engine Discord"; + if(data.button1Url == null) + data.button1Url = "https://discord.gg/2NTCdsQvx4"; +} + +function onPlayStateUpdate() { + DiscordUtil.changeSongPresence( + PlayState.instance.detailsText, + (PlayState.instance.paused ? "Paused - " : "") + PlayState.SONG.meta.displayName + " (" + PlayState.difficulty + ")", + PlayState.instance.inst, + PlayState.instance.getIconRPC() + ); +} + +function onMenuLoaded(name:String) { + // Name is either "Main Menu", "Freeplay", "Title Screen", "Options Menu", "Credits Menu", "Beta Warning", "Update Available Screen", "Update Screen" + DiscordUtil.changePresenceSince("In the Menus", null); +} + +function onEditorTreeLoaded(name:String) { + switch(name) { + case "Character Editor": + DiscordUtil.changePresenceSince("Choosing a Character", null); + case "Chart Editor": + DiscordUtil.changePresenceSince("Choosing a Chart", null); + case "Stage Editor": + DiscordUtil.changePresenceSince("Choosing a Stage", null); + } +} + +function onEditorLoaded(name:String, editingThing:String) { + switch(name) { + case "Character Editor": + DiscordUtil.changePresenceSince("Editing a Character", editingThing); + case "Chart Editor": + DiscordUtil.changePresenceSince("Editing a Chart", editingThing); + case "Stage Editor": + DiscordUtil.changePresenceSince("Editing a Stage", editingThing); + } +} \ No newline at end of file diff --git a/commandline/commands/Update.hx b/commandline/commands/Update.hx index 92ae18c7..6ac0f0f3 100644 --- a/commandline/commands/Update.hx +++ b/commandline/commands/Update.hx @@ -75,10 +75,12 @@ class Update { // vswhere.exe its used to find any visual studio related installations on the system, including full visual studio ide installations, visual studio build tools installations, and other related components - Nex if (Compiler.getBuildTarget().toLowerCase() == "windows" && new Process('"C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" -property catalog_productDisplayVersion').exitCode(true) == 1) { prettyPrint("Installing Microsoft Visual Studio Community (Dependency)"); + + // thanks to @crowplexus for these two lines! - Nex Sys.command("curl -# -O https://download.visualstudio.microsoft.com/download/pr/3105fcfe-e771-41d6-9a1c-fc971e7d03a7/8eb13958dc429a6e6f7e0d6704d43a55f18d02a253608351b6bf6723ffdaf24e/vs_Community.exe"); Sys.command("vs_Community.exe --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 --add Microsoft.VisualStudio.Component.Windows10SDK.19041 -p"); - FileSystem.deleteFile("vs_Community.exe"); + FileSystem.deleteFile("vs_Community.exe"); prettyPrint('If it didn\'t say it before: Because of this component if you want to compile you have to restart the device.'); Sys.print("Do you wish to do it now [y/n]? "); if(Sys.stdin().readLine().toLowerCase() == "y") Sys.command("shutdown /r /t 0 /f"); diff --git a/libs.xml b/libs.xml index 81618ce1..6463128d 100644 --- a/libs.xml +++ b/libs.xml @@ -1,8 +1,8 @@ - - + + @@ -10,13 +10,14 @@ - + + - + - \ No newline at end of file + diff --git a/project.xml b/project.xml index aee55825..77ae6af6 100644 --- a/project.xml +++ b/project.xml @@ -161,6 +161,13 @@ + + + + + + + @@ -170,11 +177,9 @@ - + - -
diff --git a/source/funkin/backend/scripting/GlobalScript.hx b/source/funkin/backend/scripting/GlobalScript.hx index 34da69b5..5c037aec 100644 --- a/source/funkin/backend/scripting/GlobalScript.hx +++ b/source/funkin/backend/scripting/GlobalScript.hx @@ -79,12 +79,14 @@ class GlobalScript { } public static function event(name:String, event:T):T { - if (scripts != null) scripts.call(name, [event]); + if (scripts != null) + scripts.event(name, event); return event; } public static function call(name:String, ?args:Array) { - if (scripts != null) scripts.call(name, args); + if (scripts != null) + scripts.call(name, args); } public static function onModSwitch(newMod:String) { call("destroy"); @@ -93,7 +95,8 @@ class GlobalScript { for (i in funkin.backend.assets.ModsFolder.getLoadedMods()) { var path = Paths.script('data/global/LIB_$i'); var script = Script.create(path); - if (script is DummyScript) continue; + if (script is DummyScript) + continue; script.remappedNames.set(script.fileName, '$i:${script.fileName}'); scripts.add(script); script.load(); diff --git a/source/funkin/backend/scripting/events/DiscordPresenceUpdateEvent.hx b/source/funkin/backend/scripting/events/DiscordPresenceUpdateEvent.hx index 782d70a0..c17cbb68 100644 --- a/source/funkin/backend/scripting/events/DiscordPresenceUpdateEvent.hx +++ b/source/funkin/backend/scripting/events/DiscordPresenceUpdateEvent.hx @@ -1,12 +1,8 @@ package funkin.backend.scripting.events; -#if DISCORD_RPC -import discord_rpc.DiscordRpc.DiscordPresenceOptions; -#end - final class DiscordPresenceUpdateEvent extends CancellableEvent { /** * Object containing all of the data for the presence. Can be altered. */ - public var presenceData:#if DISCORD_RPC DiscordPresenceOptions #else Dynamic #end; + public var presence:funkin.backend.utils.DiscordUtil.DPresence; } \ No newline at end of file diff --git a/source/funkin/backend/system/Main.hx b/source/funkin/backend/system/Main.hx index d04d4f3d..cad2266e 100644 --- a/source/funkin/backend/system/Main.hx +++ b/source/funkin/backend/system/Main.hx @@ -120,7 +120,6 @@ class Main extends Sprite ShaderResizeFix.init(); Logs.init(); Paths.init(); - DiscordUtil.init(); #if GLOBAL_SCRIPT funkin.backend.scripting.GlobalScript.init(); #end diff --git a/source/funkin/backend/system/MainState.hx b/source/funkin/backend/system/MainState.hx index e8192fc1..885da7b0 100644 --- a/source/funkin/backend/system/MainState.hx +++ b/source/funkin/backend/system/MainState.hx @@ -55,7 +55,7 @@ class MainState extends FlxState { Main.refreshAssets(); ModsFolder.onModSwitch.dispatch(ModsFolder.currentModFolder); - DiscordUtil.reloadJsonData(); + DiscordUtil.init(); EventsData.reloadEvents(); TitleState.initialized = false; diff --git a/source/funkin/backend/system/github/GitHub.hx b/source/funkin/backend/system/github/GitHub.hx index fa664112..27ba825b 100644 --- a/source/funkin/backend/system/github/GitHub.hx +++ b/source/funkin/backend/system/github/GitHub.hx @@ -3,7 +3,6 @@ package funkin.backend.system.github; #if GITHUB_API import haxe.Json; import haxe.Exception; -import haxe.Http; #end // TODO: Document further and perhaps make this a Haxelib. @@ -18,7 +17,7 @@ class GitHub { public static function getReleases(user:String, repository:String, ?onError:Exception->Void):Array { #if GITHUB_API try { - var data = Json.parse(__requestOnGitHubServers('https://api.github.com/repos/${user}/${repository}/releases')); + var data = Json.parse(HttpUtil.requestText('https://api.github.com/repos/${user}/${repository}/releases')); if (!(data is Array)) throw __parseGitHubException(data); @@ -41,7 +40,7 @@ class GitHub { public static function getContributors(user:String, repository:String, ?onError:Exception->Void):Array { #if GITHUB_API try { - var data = Json.parse(__requestOnGitHubServers('https://api.github.com/repos/${user}/${repository}/contributors')); + var data = Json.parse(HttpUtil.requestText('https://api.github.com/repos/${user}/${repository}/contributors')); if (!(data is Array)) throw __parseGitHubException(data); @@ -63,7 +62,7 @@ class GitHub { public static function getOrganization(org:String, ?onError:Exception->Void):GitHubOrganization { #if GITHUB_API try { - var data = Json.parse(__requestOnGitHubServers('https://api.github.com/orgs/$org')); + var data = Json.parse(HttpUtil.requestText('https://api.github.com/orgs/$org')); if (Reflect.hasField(data, "documentation_url")) throw __parseGitHubException(data); @@ -86,7 +85,7 @@ class GitHub { public static function getOrganizationMembers(org:String, ?onError:Exception->Void):Array { #if GITHUB_API try { - var data = Json.parse(__requestOnGitHubServers('https://api.github.com/orgs/$org/members')); + var data = Json.parse(HttpUtil.requestText('https://api.github.com/orgs/$org/members')); if (Reflect.hasField(data, "documentation_url")) throw __parseGitHubException(data); @@ -111,7 +110,7 @@ class GitHub { try { var url = 'https://api.github.com/users/$user'; - var data = Json.parse(__requestOnGitHubServers(url)); + var data = Json.parse(HttpUtil.requestText(url)); if (Reflect.hasField(data, "documentation_url")) throw __parseGitHubException(data); @@ -134,59 +133,6 @@ class GitHub { public static inline function filterReleases(releases:Array, keepPrereleases:Bool = true, keepDrafts:Bool = false) return #if GITHUB_API [for(release in releases) if (release != null && (!release.prerelease || (release.prerelease && keepPrereleases)) && (!release.draft || (release.draft && keepDrafts))) release] #else releases #end; - public static function __requestOnGitHubServers(url:String) { - var r = null; - #if GITHUB_API - var h = new Http(url); - h.setHeader("User-Agent", "request"); - - h.onStatus = function(s) { - if(isRedirect(s)) - r = __requestOnGitHubServers(h.responseHeaders.get("Location")); - }; - - h.onData = function(d) { - if(r == null) r = d; - } - h.onError = function(e) { - throw e; - } - - h.request(false); - #end - return r; - } - public static function __requestBytesOnGitHubServers(url:String) { - var r = null; - #if GITHUB_API - var h = new Http(url); - h.setHeader("User-Agent", "request"); - - h.onStatus = function(s) { - if(isRedirect(s)) - r = __requestBytesOnGitHubServers(h.responseHeaders.get("Location")); - }; - - h.onBytes = function(d) { - if(r == null) r = d; - } - h.onError = function(e) { - throw e; - } - - h.request(false); - #end - return r; - } - private static function isRedirect(status:Int):Bool { - switch (status) { - // 301: Moved Permanently, 302: Found (Moved Temporarily), 307: Temporary Redirect, 308: Permanent Redirect - Nex - case 301 | 302 | 307 | 308 : - trace("Redirected with status code: " + status); - return true; - } - return false; - } private static function __parseGitHubException(obj:Dynamic):GitHubException { #if GITHUB_API var msg:String = "(No message)"; diff --git a/source/funkin/backend/system/macros/Utils.hx b/source/funkin/backend/system/macros/Utils.hx index df25030c..1d4873f4 100644 --- a/source/funkin/backend/system/macros/Utils.hx +++ b/source/funkin/backend/system/macros/Utils.hx @@ -37,4 +37,21 @@ class Utils { return expr; #end } + + public static macro function safeSet(variable:Null, value:Null):Null { + #if macro + return macro if (${value} != null) ${variable} = ${value}; + #end + } + public static macro function safeSetWrapper(variable:Null, value:Null, wrapper:Null):Null { + #if macro + return macro if (${value} != null) ${variable} = ${wrapper}(${value}); + #end + } + + public static macro function safeReflection(variable:Null, value:Null, field:Null):Null { + #if macro + return macro if (Reflect.hasField(${value}, ${field})) ${variable} = Reflect.field(${value}, ${field}); + #end + } } \ No newline at end of file diff --git a/source/funkin/backend/system/updating/UpdateAvailableScreen.hx b/source/funkin/backend/system/updating/UpdateAvailableScreen.hx index 5b076058..d3a3869a 100644 --- a/source/funkin/backend/system/updating/UpdateAvailableScreen.hx +++ b/source/funkin/backend/system/updating/UpdateAvailableScreen.hx @@ -92,6 +92,8 @@ class UpdateAvailableScreen extends MusicBeatState { add(skipButton); oldPos = FlxG.mouse.getScreenPosition(); + + DiscordUtil.call("onMenuLoaded", ["Update Available Screen"]); } var destY:Float = 0; diff --git a/source/funkin/backend/system/updating/UpdateScreen.hx b/source/funkin/backend/system/updating/UpdateScreen.hx index 4fbe4cf8..323fda09 100644 --- a/source/funkin/backend/system/updating/UpdateScreen.hx +++ b/source/funkin/backend/system/updating/UpdateScreen.hx @@ -61,6 +61,8 @@ class UpdateScreen extends MusicBeatState { updater.execute(); FlxG.camera.addShader(rainbowShader = new CustomShader("engine/updaterShader")); + + DiscordUtil.call("onMenuLoaded", ["Update Screen"]); } diff --git a/source/funkin/backend/utils/DiscordUtil.hx b/source/funkin/backend/utils/DiscordUtil.hx index cdd24b6c..9df88b3e 100644 --- a/source/funkin/backend/utils/DiscordUtil.hx +++ b/source/funkin/backend/utils/DiscordUtil.hx @@ -1,62 +1,122 @@ package funkin.backend.utils; import funkin.backend.scripting.events.DiscordPresenceUpdateEvent; +import funkin.backend.scripting.events.CancellableEvent; +import funkin.backend.scripting.*; // lazy +import flixel.util.typeLimit.OneOfTwo; +import openfl.display.BitmapData; +import funkin.backend.system.macros.Utils; import haxe.Json; import flixel.sound.FlxSound; #if DISCORD_RPC -import discord_rpc.DiscordRpc; +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:String = null; +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; - public static var data:DiscordJson = null; + public static var initialized:Bool = false; + private static var stopThread:Bool = false; - public static function init() { + 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 config:#if DISCORD_RPC DiscordJson #else Dynamic #end = null; + + public static var script:Script; + + // Constants + public static var REPLY_NO:Int = Discord.REPLY_NO; + public static var REPLY_YES:Int = Discord.REPLY_YES; + public static var REPLY_IGNORE:Int = Discord.REPLY_IGNORE; + public static var PARTY_PRIVATE:Int = Discord.PARTY_PRIVATE; + public static var PARTY_PUBLIC:Int = Discord.PARTY_PUBLIC; + + public static function init() + { #if DISCORD_RPC reloadJsonData(); - discordThread = Thread.create(function() { + if (initialized) + return; + initialized = true; + + discordThread = Thread.create(function() + { while (true) { - while(!ready) { - Sys.sleep(1/60); + while (!stopThread) + { + #if DISCORD_DISABLE_IO_THREAD + Discord.UpdateConnection(); + #end + Discord.RunCallbacks(); + + Sys.sleep(2); } - trace("Processing Discord RPC..."); - DiscordRpc.process(); - Sys.sleep(2); + + Sys.sleep(1); // to reduce cpu } }); - Application.current.onExit.add(function(exitCode) { - shutdown(); - }); + Application.current.onExit.add(function(_) shutdown()); #end } - public static function reloadJsonData() { + public static function reloadJsonData() + { #if DISCORD_RPC - data = {}; var jsonPath = Paths.json("config/discord"); - if (Assets.exists(jsonPath)) { - try { - data = Json.parse(Assets.getText(jsonPath)); - } catch(e) { + if (Assets.exists(jsonPath)) + { + try + config = Json.parse(Assets.getText(jsonPath)) + catch (e) Logs.trace('Couldn\'t load Discord RPC configuration: ${e.toString()}', ERROR); - } } - data.setFieldDefault("clientID", "1027994136193810442"); - data.setFieldDefault("logoKey", "icon"); - data.setFieldDefault("logoText", Application.current.meta.get('title')); + else + { + config = {}; + } - changeClientID(data.clientID); + 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) { + public static function event(name:String, event:T):T + { + if (script != null) + script.call(name, [event]); + return event; + } + + public static function call(name:String, ?args:Array) + { + if (script != null) + script.call(name, args); + } + + public static function loadScript() + { + if (script != null) + { + call("destroy"); + script = FlxDestroyUtil.destroy(script); + } + script = Script.create(Paths.script('data/discord')); + // script.setParent(DiscordUtil); + script.load(); + } + + public static function changePresence(details:String, state:String, ?smallImageKey:String) + { #if DISCORD_RPC changePresenceAdvanced({ state: state, @@ -66,12 +126,14 @@ class DiscordUtil { #end } - public static function changeSongPresence(details:String, state:String, audio:FlxSound, ?smallImageKey : String) { + 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) { + if (audio != null && audio.playing) + { start = Date.now().getTime(); end = start + (audio.length - audio.time); } @@ -86,66 +148,335 @@ class DiscordUtil { #end } - public static function changePresenceAdvanced(data:#if DISCORD_RPC DiscordPresenceOptions #else Dynamic #end) { + public static function changePresenceSince(details:String, state:String, ?smallImageKey:String, ?time:Float) + { #if DISCORD_RPC - if (data == null) return; + if(time == null) + time = Date.now().getTime(); - if (data.largeImageKey == null) - data.largeImageKey = DiscordUtil.data.logoKey; - if (data.largeImageText == null) - data.largeImageText = DiscordUtil.data.logoText; - - #if GLOBAL_SCRIPT - var event = funkin.backend.scripting.GlobalScript.event("onDiscordPresenceUpdate", EventManager.get(DiscordPresenceUpdateEvent).recycle(data)); - if (event.cancelled) return; - #end - - DiscordRpc.presence(data); + changePresenceAdvanced({ + state: state, + details: details, + smallImageKey: smallImageKey, + startTimestamp: Std.int(time / 1000) + }); #end } - public static function changeClientID(id:String) { + + @: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 (currentID != null) { - DiscordRpc.shutdown(); + if (data == null) + return; + + // copy last presence + if (data.largeImageKey == null) + data.largeImageKey = config.logoKey; + if (data.largeImageText == null) + data.largeImageText = config.logoText; + + var evt = EventManager.get(DiscordPresenceUpdateEvent).recycle(data); + #if GLOBAL_SCRIPT + // kept for "backwards compat" + funkin.backend.scripting.GlobalScript.event("onDiscordPresenceUpdate", evt); + #end + event("onDiscordPresenceUpdate", evt); + if (evt.cancelled) + return; + data = evt.presence; + 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); + if (data.matchSecret == null && data.joinSecret == null && data.spectateSecret == null) + { + Utils.safeSetWrapper(dp.button1Label, data.button1Label, fixString); + Utils.safeSetWrapper(dp.button1Url, data.button1Url, fixString); + Utils.safeSetWrapper(dp.button2Label, data.button2Label, fixString); + Utils.safeSetWrapper(dp.button2Url, data.button2Url, fixString); } - ready = false; - - DiscordRpc.start({ - clientID: id, - onReady: function() { - Logs.trace('Discord RPC started'); - ready = true; - }, - onError: onError, - onDisconnected: onDisconnected - }); - currentID = id; - + Discord.UpdatePresence(cpp.RawConstPointer.addressOf(dp)); #end } - public static function shutdown() { + public static function clearPresence() { #if DISCORD_RPC - DiscordRpc.shutdown(); + Discord.ClearPresence(); #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; + + loadScript(); + #end + + return currentID = id; + } + + public static function shutdown() + { + ready = false; + stopThread = true; + #if DISCORD_RPC + Discord.Shutdown(); + #end + + call("destroy"); + script = FlxDestroyUtil.destroy(script); + } + + public static function respond(userId:String, reply:Int) + { + #if DISCORD_RPC + Discord.Respond(fixString(userId), reply); + #end + } // HANDLERS #if DISCORD_RPC - static function onError(_code:Int, _message:String) { - Logs.trace('Discord RPC Error: ${_message} (Code: $_code)', ERROR); + private static function onReady(request:cpp.RawConstPointer):Void + { + user = DUser.initRaw(request); + + Logs.traceColored([ + Logs.logText("[Discord] ", BLUE), + Logs.logText("Connected to User " + user.globalName + " ("), + Logs.logText(user.handle, GRAY), + Logs.logText(")") + ], INFO); + + ready = true; + + call("onReady", [user]); } - static function onDisconnected(_code:Int, _message:String) { - Logs.trace('Discord RPC Disconnected: ${_message} (Code: $_code)', WARNING); + 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); + + call("onReady", [errorCode, cast(finalMsg, String)]); + } + + 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 ($errorCode: $finalMsg)', RED) + ], ERROR); + + call("onError", [errorCode, cast(finalMsg, String)]); + } + + private static function onJoin(joinSecret:cpp.ConstCharStar):Void + { + Logs.traceColored([Logs.logText("[Discord] ", BLUE), Logs.logText("Someone has just joined", GREEN)], INFO); + + call("onJoinGame", [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); + + call("onSpectateGame", [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); + + var req:DUser = DUser.initRaw(request); + call("onJoinRequest", [req]); } #end } -typedef DiscordJson = { +typedef DiscordJson = +{ var ?clientID:String; var ?logoKey:String; var ?logoText:String; -} \ No newline at end of file +} + +@:noCustomClass +final class DUser +{ + /** + * The username + discriminator if they have it + **/ + public var handle: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; + + /** + * The user's display name + **/ + public var globalName:String; + + /** + * If the user is a bot or not + **/ + public var bot:Bool; + + /** + * Idk check discord docs + **/ + public var flags:Int; + + /** + * If the user has nitro + **/ + public var premiumType:NitroType; + + private function new() + { + } + + public static function initRaw(req:cpp.RawConstPointer) + { + return init(cpp.ConstPointer.fromRaw(req).ptr); + } + + public static function init(userData:cpp.Star) + { + var d = new DUser(); + d.userId = userData.userId; + d.username = userData.username; + d.discriminator = Std.parseInt(userData.discriminator); + d.avatar = userData.avatar; + d.globalName = userData.globalName; + d.bot = userData.bot; + d.flags = userData.flags; + d.premiumType = userData.premiumType; + + if (d.discriminator != 0) + d.handle = '${d.username}#${d.discriminator}'; + else + d.handle = '${d.username}'; + return d; + } + + /** + * 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')); +} + +enum abstract NitroType(Int) to Int from Int +{ + var NONE = 0; + var NITRO_CLASSIC = 1; + var NITRO = 2; + var NITRO_BASIC = 3; +} + +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; + var ?button1Label:String; /* max 32 bytes */ + var ?button1Url:String; /* max 512 bytes */ + var ?button2Label:String; /* max 32 bytes */ + var ?button2Url:String; /* max 512 bytes */ +} + +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; +} diff --git a/source/funkin/backend/utils/HttpUtil.hx b/source/funkin/backend/utils/HttpUtil.hx new file mode 100644 index 00000000..f2d3caf8 --- /dev/null +++ b/source/funkin/backend/utils/HttpUtil.hx @@ -0,0 +1,71 @@ +package funkin.backend.utils; + +import haxe.Http; + +class HttpUtil +{ + public static var userAgent:String = "request"; + public static function requestText(url:String) + { + var r = null; + var h = new Http(url); + h.setHeader("User-Agent", userAgent); + + h.onStatus = function(s) + { + if (isRedirect(s)) + r = requestText(h.responseHeaders.get("Location")); + }; + + h.onData = function(d) + { + if (r == null) + r = d; + } + h.onError = function(e) + { + throw e; + } + + h.request(false); + return r; + } + + public static function requestBytes(url:String) + { + var r = null; + var h = new Http(url); + h.setHeader("User-Agent", userAgent); + + h.onStatus = function(s) + { + if (isRedirect(s)) + r = requestBytes(h.responseHeaders.get("Location")); + }; + + h.onBytes = function(d) + { + if (r == null) + r = d; + } + h.onError = function(e) + { + throw e; + } + + h.request(false); + return r; + } + + private static function isRedirect(status:Int):Bool + { + switch (status) + { + // 301: Moved Permanently, 302: Found (Moved Temporarily), 307: Temporary Redirect, 308: Permanent Redirect - Nex + case 301 | 302 | 307 | 308: + Logs.traceColored([Logs.logText('[Connection Status] ', BLUE), Logs.logText('Redirected with status code: ', YELLOW), Logs.logText('$status', GREEN)], WARNING); + return true; + } + return false; + } +} diff --git a/source/funkin/editors/character/CharacterEditor.hx b/source/funkin/editors/character/CharacterEditor.hx index 8c5cf9c7..359ef24d 100644 --- a/source/funkin/editors/character/CharacterEditor.hx +++ b/source/funkin/editors/character/CharacterEditor.hx @@ -224,7 +224,7 @@ class CharacterEditor extends UIState { FlxG.cameras.add(uiCamera); FlxG.cameras.add(animsCamera); - character = new Character(0,0,__character, false, false); + character = new Character(0,0, __character, false, false); character.debugMode = true; character.cameras = [charCamera]; @@ -257,6 +257,8 @@ class CharacterEditor extends UIState { Framerate.memoryCounter.alpha = 0.4; Framerate.codenameBuildField.alpha = 0.4; } + + DiscordUtil.call("onEditorLoaded", ["Character Editor", __character]); } override function destroy() { diff --git a/source/funkin/editors/character/CharacterSelection.hx b/source/funkin/editors/character/CharacterSelection.hx index 9d1a60ed..817acf29 100644 --- a/source/funkin/editors/character/CharacterSelection.hx +++ b/source/funkin/editors/character/CharacterSelection.hx @@ -33,6 +33,8 @@ class CharacterSelection extends EditorTreeMenu })); main = new OptionsScreen("Character Editor", "Select a character to edit", list); + + DiscordUtil.call("onEditorTreeLoaded", ["Character Editor"]); } override function createPost() { diff --git a/source/funkin/editors/charter/Charter.hx b/source/funkin/editors/charter/Charter.hx index 926e12b8..a26c55c0 100644 --- a/source/funkin/editors/charter/Charter.hx +++ b/source/funkin/editors/charter/Charter.hx @@ -510,6 +510,8 @@ class Charter extends UIState { dataDisplay.updateHitbox(); dataDisplay.screenCenter(Y); dataDisplay.cameras = [charterCamera]; dataDisplay.x = -dataDisplay.width; add(dataDisplay);*/ + + DiscordUtil.call("onEditorLoaded", ["Chart Editor", __song + " (" + __diff + ")"]); } override function destroy() { @@ -548,7 +550,7 @@ class Charter extends UIState { notesGroup.autoSort = false; var noteCount:Int = 0; for (strL in PlayState.SONG.strumLines) - for (note in strL.notes) noteCount++; + noteCount += strL.notes.length; notesGroup.preallocate(noteCount); var notesCreated:Int = 0; diff --git a/source/funkin/editors/charter/CharterSelection.hx b/source/funkin/editors/charter/CharterSelection.hx index b0957997..6fd97bdc 100644 --- a/source/funkin/editors/charter/CharterSelection.hx +++ b/source/funkin/editors/charter/CharterSelection.hx @@ -46,6 +46,8 @@ class CharterSelection extends EditorTreeMenu { })); main = new OptionsScreen("Chart Editor", "Select a song to modify the charts from.", list); + + DiscordUtil.call("onEditorTreeLoaded", ["Chart Editor"]); } override function createPost() { diff --git a/source/funkin/game/GameOverSubstate.hx b/source/funkin/game/GameOverSubstate.hx index 3d119f02..4868b130 100644 --- a/source/funkin/game/GameOverSubstate.hx +++ b/source/funkin/game/GameOverSubstate.hx @@ -87,8 +87,7 @@ class GameOverSubstate extends MusicBeatSubstate lossSFX = FlxG.sound.play(Paths.sound(lossSFXName)); Conductor.changeBPM(gameOverSongBPM); - DiscordUtil.changePresence('Game Over', PlayState.SONG.meta.displayName + " (" + PlayState.difficulty + ")"); - + DiscordUtil.call("onGameOver", []); gameoverScript.call("postCreate"); } diff --git a/source/funkin/game/NoteGroup.hx b/source/funkin/game/NoteGroup.hx index 97b34da6..0d45a41f 100644 --- a/source/funkin/game/NoteGroup.hx +++ b/source/funkin/game/NoteGroup.hx @@ -106,6 +106,7 @@ class NoteGroup extends FlxTypedGroup { if (members == null) return null; + // TODO: maybe lastIndexOf var index:Int = members.indexOfFromLast(Object); if (index < 0) diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index 9a46aeb9..2ae26370 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -784,7 +784,7 @@ class PlayState extends MusicBeatState * This function is dynamic, which means you can do `updateDiscordPresence = function() {}` in scripts. */ public dynamic function updateDiscordPresence() - DiscordUtil.changeSongPresence(detailsText, (paused ? "Paused - " : "") + SONG.meta.displayName + " (" + difficulty + ")", inst, getIconRPC()); + DiscordUtil.call("onPlayStateUpdate", []); /** * Starts a cutscene. diff --git a/source/funkin/menus/BetaWarningState.hx b/source/funkin/menus/BetaWarningState.hx index 08d60f39..16201454 100644 --- a/source/funkin/menus/BetaWarningState.hx +++ b/source/funkin/menus/BetaWarningState.hx @@ -29,6 +29,8 @@ class BetaWarningState extends MusicBeatState { var off = Std.int((FlxG.height - (disclaimer.y + disclaimer.height)) / 2); disclaimer.y += off; titleAlphabet.y += off; + + DiscordUtil.call("onMenuLoaded", ["Beta Warning"]); } public override function update(elapsed:Float) { diff --git a/source/funkin/menus/FreeplayState.hx b/source/funkin/menus/FreeplayState.hx index 0a6f3493..7ca66b02 100644 --- a/source/funkin/menus/FreeplayState.hx +++ b/source/funkin/menus/FreeplayState.hx @@ -116,7 +116,7 @@ class FreeplayState extends MusicBeatState } } - DiscordUtil.changePresence("In the Menus", null); + DiscordUtil.call("onMenuLoaded", ["Freeplay"]); super.create(); diff --git a/source/funkin/menus/MainMenuState.hx b/source/funkin/menus/MainMenuState.hx index 03e3bd8b..b17e9db6 100644 --- a/source/funkin/menus/MainMenuState.hx +++ b/source/funkin/menus/MainMenuState.hx @@ -32,7 +32,7 @@ class MainMenuState extends MusicBeatState { super.create(); - DiscordUtil.changePresence("In the Menus", null); + DiscordUtil.call("onMenuLoaded", ["Main Menu"]); CoolUtil.playMenuSong(); diff --git a/source/funkin/menus/StoryMenuState.hx b/source/funkin/menus/StoryMenuState.hx index 6d9e27f2..384118e5 100644 --- a/source/funkin/menus/StoryMenuState.hx +++ b/source/funkin/menus/StoryMenuState.hx @@ -116,7 +116,7 @@ class StoryMenuState extends MusicBeatState { changeWeek(0, true); - DiscordUtil.changePresence("In the Menus", null); + DiscordUtil.call("onMenuLoaded", ["Story Menu"]); CoolUtil.playMenuSong(); } diff --git a/source/funkin/menus/TitleState.hx b/source/funkin/menus/TitleState.hx index 491bc65a..4aca30db 100644 --- a/source/funkin/menus/TitleState.hx +++ b/source/funkin/menus/TitleState.hx @@ -36,6 +36,8 @@ class TitleState extends MusicBeatState startIntro(); super.create(); + + DiscordUtil.call("onMenuLoaded", ["Title Screen"]); } var logoBl:FlxSprite; diff --git a/source/funkin/menus/credits/CreditsCodename.hx b/source/funkin/menus/credits/CreditsCodename.hx index 2a9dd42b..579360ed 100644 --- a/source/funkin/menus/credits/CreditsCodename.hx +++ b/source/funkin/menus/credits/CreditsCodename.hx @@ -1,5 +1,7 @@ package funkin.menus.credits; +import funkin.options.PlayerSettings; +import flixel.util.FlxColor; import funkin.backend.system.github.GitHub; import funkin.options.type.GithubIconOption; import flixel.text.FlxText; @@ -9,44 +11,74 @@ using StringTools; class CreditsCodename extends funkin.options.OptionsScreen { public var error:Bool = false; public var author:String = "FNF-CNE-Devs"; + public var mainDevCol:FlxColor = 0xFF9C35D5; + public var minContrCol:FlxColor = 0xFFB4A7DA; + public var totalContributions:Int = 0; public override function new() { super("Codename Engine", "All the contributors of the engine! - Press RESET to update the list (One reset per 2 minutes)."); - checkUpdate(); - displayList(); + tryUpdating(true); } + // blame the secondary threads if the code has to look this bad - Nex + private var _canReset:Bool = true; + private var _downloadingSteps:Int = 0; public override function update(elapsed:Float) { - if (funkin.options.PlayerSettings.solo.controls.RESET && checkUpdate()) { + super.update(elapsed); + + if (_downloadingSteps == 2) { + _downloadingSteps = 0; + _canReset = true; displayList(); + } else if (_downloadingSteps == 1) { + _downloadingSteps = 0; + _canReset = true; updateMenuDesc(); } - super.update(elapsed); + else if(_canReset && PlayerSettings.solo.controls.RESET) tryUpdating(); + } + + public function tryUpdating(forceDisplaying:Bool = false) { + updateMenuDesc("Downloading List..."); + _canReset = false; + Main.execAsync(function() { + if(checkUpdate() || forceDisplaying) _downloadingSteps = 2; + else _downloadingSteps = 1; + }); } public override function updateMenuDesc(?txt:String) { + if(!_canReset) return; super.updateMenuDesc(txt); - parent.treeParent.pathDesc.applyMarkup(parent.treeParent.pathDesc.text, [new FlxTextFormatMarkerPair(new FlxTextFormat(0xFF9C35D5), "*")]); + updateMarkup(); + } + + public function updateMarkup() { + if(parent == null || parent.treeParent == null) return; + var text:String = parent.treeParent.pathDesc.text; + parent.treeParent.pathDesc.text = ""; + parent.treeParent.pathDesc.applyMarkup(text, [ + new FlxTextFormatMarkerPair(new FlxTextFormat(mainDevCol), '*'), + new FlxTextFormatMarkerPair(new FlxTextFormat(FlxColor.interpolate(minContrCol, mainDevCol, Options.contributors[curSelected].contributions / totalContributions)), '~') + ]); } public function checkUpdate():Bool { var curTime:Float = Date.now().getTime(); - if(Options.lastUpdated != null && curTime < Options.lastUpdated + 120000) return false; // Fuck you Github rate limits + if(Options.lastUpdated != null && curTime < Options.lastUpdated + 120000) return false; // Fuck you Github rate limits - Nex Options.lastUpdated = curTime; error = false; - //Main.execAsync(function() { var idk = GitHub.getContributors(author, "CodenameEngine", function(e) { error = true; var errMsg:String = 'Error while trying to download contributors list:\n${CoolUtil.removeIP(e.message)}'; Logs.traceColored([Logs.logText(errMsg.replace('\n', ' '), RED)], ERROR); funkin.backend.utils.NativeAPI.showMessageBox("Codename Engine Warning", errMsg, MSG_WARNING); }); - //}); if(error) return false; Options.contributors = idk; - trace('List Updated!'); + trace('Contributors list Updated!'); var errorOnMain:Bool = false; var idk2 = GitHub.getOrganizationMembers(author, function(e) { @@ -55,7 +87,10 @@ class CreditsCodename extends funkin.options.OptionsScreen { Logs.traceColored([Logs.logText(errMsg.replace('\n', ' '), RED)], ERROR); funkin.backend.utils.NativeAPI.showMessageBox("Codename Engine Warning", errMsg, MSG_WARNING); }); - if(!errorOnMain) Options.mainDevs = [for(m in idk2) m.id]; + if(!errorOnMain) { + Options.mainDevs = [for(m in idk2) m.id]; + trace('Main Devs list Updated!'); + } return true; } @@ -70,13 +105,17 @@ class CreditsCodename extends funkin.options.OptionsScreen { remove(members[0], true); } - var totalContributions = 0; + totalContributions = 0; for(c in Options.contributors) totalContributions += c.contributions; for(c in Options.contributors) { - var desc:String = 'Total Contributions: ${c.contributions} / ${totalContributions} (${FlxMath.roundDecimal(c.contributions / totalContributions * 100, 2)}%) - Select to open GitHub account'; - if(Options.mainDevs.contains(c.id)) desc += " *- Public member of the main Devs!*"; - var opt:GithubIconOption = new GithubIconOption(c, desc); + var opt:GithubIconOption = new GithubIconOption(c, 'Total Contributions: ~${c.contributions}~ / *${totalContributions}* (~${FlxMath.roundDecimal(c.contributions / totalContributions * 100, 2)}%~) - Select to open GitHub account'); + if(Options.mainDevs.contains(c.id)) { + opt.desc += " *- Public member of the main Devs!*"; + @:privateAccess opt.__text.color = mainDevCol; + } add(opt); } + + updateMenuDesc(); } } \ No newline at end of file diff --git a/source/funkin/menus/credits/CreditsMain.hx b/source/funkin/menus/credits/CreditsMain.hx index 5dd16aff..56507274 100644 --- a/source/funkin/menus/credits/CreditsMain.hx +++ b/source/funkin/menus/credits/CreditsMain.hx @@ -43,6 +43,8 @@ class CreditsMain extends TreeMenu { main = new OptionsScreen('Credits', 'The people who made this possible!', selectables); super.create(); + + DiscordUtil.call("onMenuLoaded", ["Credits Menu"]); } /** diff --git a/source/funkin/options/OptionsMenu.hx b/source/funkin/options/OptionsMenu.hx index 5b786e10..f9232e13 100644 --- a/source/funkin/options/OptionsMenu.hx +++ b/source/funkin/options/OptionsMenu.hx @@ -37,6 +37,8 @@ class OptionsMenu extends TreeMenu { CoolUtil.playMenuSong(); + DiscordUtil.call("onMenuLoaded", ["Options Menu"]); + var bg:FlxSprite = new FlxSprite(-80).loadAnimatedGraphic(Paths.image('menus/menuBGBlue')); // bg.scrollFactor.set(); bg.scale.set(1.15, 1.15); diff --git a/source/funkin/options/OptionsScreen.hx b/source/funkin/options/OptionsScreen.hx index f21bdaaf..45580821 100644 --- a/source/funkin/options/OptionsScreen.hx +++ b/source/funkin/options/OptionsScreen.hx @@ -70,11 +70,8 @@ class OptionsScreen extends FlxTypedSpriteGroup { } public function updateMenuDesc(?customTxt:String) { - if (parent.treeParent == null) return; - - var text:String = members[curSelected].desc; - if (customTxt != null) text = customTxt; - parent.treeParent.updateDesc(text); + if (parent == null || parent.treeParent == null) return; + parent.treeParent.updateDesc(customTxt != null ? customTxt : members[curSelected].desc); } public dynamic function onClose(o:OptionsScreen) {} diff --git a/source/funkin/options/type/GithubIconOption.hx b/source/funkin/options/type/GithubIconOption.hx index cbbef1ed..5c780165 100644 --- a/source/funkin/options/type/GithubIconOption.hx +++ b/source/funkin/options/type/GithubIconOption.hx @@ -19,10 +19,10 @@ class GithubIconOption extends TextOption return usePortrait = value; } - public function new(user:Dynamic, desc:String, ?callback:Void->Void, ?customName:String, size:Int = 96, usePortrait:Bool = true) { + public function new(user:Dynamic, desc:String, ?callback:Void->Void, ?customName:String, size:Int = 96, usePortrait:Bool = true, waitUntilLoad:Float = 0.25) { super(customName == null ? user.login : customName, desc, callback == null ? function() CoolUtil.openURL(user.html_url) : callback); this.user = user; - this.icon = new GithubUserIcon(user, size); + this.icon = new GithubUserIcon(user, size, waitUntilLoad); this.usePortrait = usePortrait; add(icon); } @@ -30,22 +30,28 @@ class GithubIconOption extends TextOption class GithubUserIcon extends FlxSprite { - private var loading:Bool = false; + public var waitUntilLoad:Null; private var user:Dynamic; private var size:Int; - public override function new(user:Dynamic, size:Int = 96) { + public override function new(user:Dynamic, size:Int = 96, waitUntilLoad:Float = 0.25) { this.user = user; this.size = size; + this.waitUntilLoad = waitUntilLoad; super(); makeGraphic(size, size, FlxColor.TRANSPARENT); antialiasing = true; } + override function update(elapsed:Float) { + if(waitUntilLoad > 0) waitUntilLoad -= elapsed; + super.update(elapsed); + } + final mutex = new sys.thread.Mutex(); override function drawComplex(camera:FlxCamera):Void { // Making the image downlaod only if the player actually sees it on the screeeeen - Nex - if(!loading) { - loading = true; + if(waitUntilLoad <= 0) { + waitUntilLoad = null; Main.execAsync(function() { var key:String = 'GITHUB-USER:${user.login}'; var bmap:Dynamic = FlxG.bitmap.get(key); @@ -57,7 +63,7 @@ class GithubUserIcon extends FlxSprite var bytes = null; if(unfLink) { - try bytes = GitHub.__requestBytesOnGitHubServers('${user.avatar_url}?size=$size') + try bytes = HttpUtil.requestBytes('${user.avatar_url}?size=$size') catch(e) Logs.traceColored([Logs.logText('Failed to download github pfp for ${user.login}: ${CoolUtil.removeIP(e.message)} - (Retrying using the api..)', RED)], ERROR); if(bytes != null) { @@ -68,7 +74,7 @@ class GithubUserIcon extends FlxSprite if(planB) { if(unfLink) user = GitHub.getUser(user.login, function(e) Logs.traceColored([Logs.logText('Failed to download github user info for ${user.login}: ${CoolUtil.removeIP(e.message)}', RED)], ERROR)); // Api part - Nex - try bytes = GitHub.__requestBytesOnGitHubServers('${user.avatar_url}&size=$size') + try bytes = HttpUtil.requestBytes('${user.avatar_url}&size=$size') catch(e) Logs.traceColored([Logs.logText('Failed to download github pfp for ${user.login}: ${CoolUtil.removeIP(e.message)}', RED)], ERROR); if(bytes != null) bmap = BitmapData.fromBytes(bytes);