From c48d77b51d72f446ee4a0fa06980dab0fa5e2000 Mon Sep 17 00:00:00 2001 From: SplatterDash <95732227+SplatterDash@users.noreply.github.com> Date: Sun, 9 Aug 2026 23:21:43 -0400 Subject: [PATCH 1/3] gamejolt pr - cne prevents leaks! --- .gitignore | 1 + building/libs.xml | 1 + project.xml | 4 + source/funkin/backend/assets/ModsFolder.hx | 11 +- source/funkin/backend/system/Flags.hx | 6 + source/funkin/backend/system/MainState.hx | 8 + source/funkin/backend/utils/GJUtil.hx | 319 ++++++++++++++++ .../backend/utils/GameJoltSecurityPublic.hx | 341 ++++++++++++++++++ source/hscript/Config.hx | 2 +- 9 files changed, 691 insertions(+), 2 deletions(-) create mode 100644 source/funkin/backend/utils/GJUtil.hx create mode 100644 source/funkin/backend/utils/GameJoltSecurityPublic.hx diff --git a/.gitignore b/.gitignore index 8b905e1643..7401c22170 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ addons/* dev-libs/ *.sum .vs/ +source/funkin/backend/utils/GameJoltSecurity.hx \ No newline at end of file diff --git a/building/libs.xml b/building/libs.xml index f78849581d..9cc20032b4 100644 --- a/building/libs.xml +++ b/building/libs.xml @@ -17,6 +17,7 @@ + diff --git a/project.xml b/project.xml index 3bcdd92c26..45a14243cd 100644 --- a/project.xml +++ b/project.xml @@ -66,6 +66,9 @@ + + + @@ -131,6 +134,7 @@ + diff --git a/source/funkin/backend/assets/ModsFolder.hx b/source/funkin/backend/assets/ModsFolder.hx index 0e33f999ed..e1db271b7b 100644 --- a/source/funkin/backend/assets/ModsFolder.hx +++ b/source/funkin/backend/assets/ModsFolder.hx @@ -3,6 +3,7 @@ package funkin.backend.assets; import flixel.util.FlxSignal.FlxTypedSignal; import funkin.backend.system.MainState; import funkin.backend.utils.CoolUtil; +import funkin.backend.utils.GJUtil; import haxe.ds.StringMap; import haxe.io.Path; import lime.text.Font; @@ -75,9 +76,17 @@ class ModsFolder { } public static function reloadMods() { - if (!__firstTime) + if (!__firstTime) { + #if GAMEJOLT_API + if (GJUtil.active) + GJUtil.logout(); + #end + FlxG.switchState(new MainState()); + } __firstTime = false; + + } /** diff --git a/source/funkin/backend/system/Flags.hx b/source/funkin/backend/system/Flags.hx index 2e61eec20c..f2a6cef704 100644 --- a/source/funkin/backend/system/Flags.hx +++ b/source/funkin/backend/system/Flags.hx @@ -28,6 +28,7 @@ class Flags { public static var MOD_NAME:String = ""; public static var MOD_DESCRIPTION:String = ""; public static var MOD_AUTHOR:String = ""; + public static var MOD_VERSION:String = ""; @:lazy public static var MOD_API_VERSION:Null = null; public static var MOD_DOWNLOAD_LINK:String = ""; public static var MOD_DEPENDENCIES:Array = []; @@ -43,6 +44,11 @@ class Flags { public static var MOD_REDIRECT_STATES:Map = []; + @:also(funkin.backend.utils.GameJoltSecurity.gameId) + public static var MOD_GAMEJOLT_GAME_ID:String = ''; + @:also(funkin.backend.utils.GameJoltSecurity.encryptedGameToken) + public static var MOD_GAMEJOLT_TOKEN:String = ''; + // -- Codename's Default Flags -- @:lazy public static var SAVE_PATH:String = haxe.macro.Compiler.getDefine("SAVE_PATH"); @:lazy public static var SAVE_NAME:String = haxe.macro.Compiler.getDefine("SAVE_NAME"); diff --git a/source/funkin/backend/system/MainState.hx b/source/funkin/backend/system/MainState.hx index 14988dddc4..205cebcaad 100644 --- a/source/funkin/backend/system/MainState.hx +++ b/source/funkin/backend/system/MainState.hx @@ -10,6 +10,7 @@ import funkin.backend.assets.ModsFolderLibrary; import funkin.backend.assets.ZipFolderLibrary; import funkin.backend.chart.EventsData; import funkin.backend.system.framerate.Framerate; +import funkin.backend.utils.GJUtil; import funkin.editors.ModConfigWarning; import funkin.menus.TitleState; import haxe.io.Path; @@ -162,6 +163,13 @@ class MainState extends FlxState { if (cast(lib, ZipFolderLibrary).PRELOAD_VIDEOS) cast(lib, ZipFolderLibrary).precacheVideos(); } + #if GAMEJOLT_API + if (FlxG.save.data.gameJoltArray != null) { + var gjDat:Array = FlxG.save.data.gameJoltArray; + GJUtil.attemptLogin(gjDat[0], gjDat[1]); + } + #end + var startState:Class = Flags.DISABLE_WARNING_SCREEN ? TitleState : funkin.menus.WarningState; // In this case if the mod we just loaded a compressed modpack, we can't edit or modify files without decompressing it. diff --git a/source/funkin/backend/utils/GJUtil.hx b/source/funkin/backend/utils/GJUtil.hx new file mode 100644 index 0000000000..56fa88050b --- /dev/null +++ b/source/funkin/backend/utils/GJUtil.hx @@ -0,0 +1,319 @@ +package funkin.backend.utils; + +/** + * This is how GameJolt API responses are formatted like. + */ +typedef Response = { + // General + success:Bool, + ?message:String, + // User Fetching + ?users:Array, + // Trophies Fetching + ?trophies:Array, + // Scores Fetching + ?scores:Array, + ?tables:Array, + ?rank:Int, + // Friends Fetching + ?friends:Array<{friend_id:Int}>, + // Data Store Fetching + ?keys:Array<{key:String}>, + ?data:String, + // Time Fetching + ?timestamp:Int, + ?timezone:String, + ?year:Int, + ?month:Int, + ?day:Int, + ?hour:Int, + ?minute:Int, + ?second:Int, + // Batch Reception + ?responses:Array +} + +/** + * The way the scores are fetched from your game API. + * + * @param score The display text of the Score. + * @param sort The Score value. + * @param extra_data If some extra data is attached to this Score, it'll be shown here. + * @param user The username of the User who achieved this Score, if it's a registered User. + * @param user_id The user ID of the User who achieved this Score, if it's a registered User. + * @param guest The name of the user who achieved this Score, if it's a guest user. + * @param stored A short description about when the Score was achieved by the User or Guest. + * @param stored_timestamp A long time stamp (in seconds) of when the Score was achieved by the User or Guest. + */ +typedef Score = { + score:String, + sort:Int, + extra_data:String, + user:String, + user_id:Int, + guest:String, + stored:String, + stored_timestamp:Int +} + +/** + * The way the score tables are fetched from your game API. + * + * @param id The ID of the Score Table. + * @param name The name of the Score Table. + * @param description The description of the Score Table. + * @param primary Whether if this is the Primary Score Table in your game (1) or not (0). + */ +typedef ScoreTable = { + id:Int, + name:String, + description:String, + primary:Bool +} + +/** + * The way the trophies are fetched from your game API. + * + * @param id The ID of the Trophy. + * @param title The title of the Trophy. + * @param description The description of the Trophy. + * @param difficulty The difficulty rank of the Trophy. + * @param image_url The link of the image that represents the Trophy. + * @param achieved Whether this Trophy was achieved or not, it can be a string if it was (with info about how much time ago it was achieved) or bool if not (false). + */ +typedef Trophy = { + id:Int, + title:String, + description:String, + difficulty:String, + image_url:String, + achieved:String +} + +/** + * The way the user data is fetched from the GameJolt API. + * + * @param id The ID of the User. + * @param type The cathegory the User is cataloged like in GameJolt. + * @param username The username of the User. (Also available for guests). + * @param avatar_url The link of the avatar of the User. + * @param signed_up A short description about how long the User have been in GameJolt. + * @param signed_up_timestamp A long time stamp (in seconds) of when the User signed up. + * @param last_logged_in A short description about the last time the User was found active in GameJolt. + * @param last_logged_in_timestamp A long time stamp (in seconds) of the last time the User logged in GameJolt. + * @param status The actual status of the User. + * @param developer_name The display name of the User. (Also available for guests). + * @param developer_website The website of the User. + * @param developer_description The description of the User. + */ +typedef User = { + id:Int, + type:String, + username:String, + avatar_url:String, + signed_up:String, + signed_up_timestamp:Int, + last_logged_in:String, + last_logged_in_timestamp:Int, + status:String, + developer_name:String, + developer_website:String, + developer_description:String +} + +/** + * An enum class to clasify Data Store update functions. + */ +enum DataUpdateType { + Add(n:Int); + Substract(n:Int); + Multiply(n:Int); + Divide(n:Int); + Append(t:String); + Prepend(t:String); +} + +/** + * An enum of every single command currently available to request to GameJolt API. + */ +enum RequestType { + BATCH(parallel:Bool, breakOnError:Bool, requests:Array); + DATA_FETCH(key:String, fromUser:Bool); + DATA_GETKEYS(fromUser:Bool, ?pattern:String); + DATA_REMOVE(key:String, fromUser:Bool); + DATA_SET(key:String, data:String, toUser:Bool); + DATA_UPDATE(key:String, operation:DataUpdateType, toUser:Bool); + FRIENDS; + TIME; + USER_AUTH; + USER_FETCH(userOrID:String); + SESSION_OPEN; + SESSION_PING(active:Bool); + SESSION_CHECK; + SESSION_CLOSE; + SCORES_ADD(score:String, sort:Int, ?extra_data:String, ?table_id:Int); + SCORES_GETRANK(sort:Int, ?table_id:Int); + SCORES_FETCH(fromUser:Bool, ?table_id:Int, ?limit:Int, ?betterThan:Int); + SCORES_TABLES; + TROPHIES_FETCH(?achieved:Bool, ?trophy_id:Int); + TROPHIES_ADD(trophy_id:Int); + TROPHIES_REMOVE(trophy_id:Int); +} + +/** + * GameJolt utility to help with GameJolt functionality. Use this class to determine if your player is logged into GameJolt. + * Will not do anything if there is no provided GameJolt token. + * + * # IMPORTANT + * If you wish to use this utility, please run your GameJolt game's security code through the Codename Engine + * encryption tool on Codename's website. + * Place the output of that into your modpack.ini under the flag `MOD_GAMEJOLT_TOKEN`. + * + * ## DO NOT PLACE YOUR SECURITY KEY RIGHT INTO THE MODPACK.INI!!!! THAT IS A SECURITY ISSUE!!!! + */ +class GJUtil +{ + /** + * Boolean to determine if our player logged in. + */ + public static var loggedIn:Bool = false; + + /** + * The username of the logged in user. + */ + public static var userName(default, set):String; + + /** + * Whether or not the GameJolt utility is operational. + * This cannot be set other than load operations. + */ + public static var active(default, null):Bool = false; + + /** + * Whether or not the utility is executing a call. + */ + static var executing:Bool = false; + + /** + * Helper function in case the session is lost in the middle of the game. + */ + public static var onLostSession:NullVoid> = null; + + /** + * Helper function to simplify the login process. + * @param name Username of user attempting to login. + * @param token User token of user attempting to login. + * @return Bool Whether the attempt was successfull or not. + */ + public static function attemptLogin(name:String, token:String):Bool + { + if(Flags.MOD_GAMEJOLT_GAME_ID != '' && Flags.MOD_GAMEJOLT_TOKEN != '') + active = true + else + return false; + + var ret:Bool = false; + userName = name; + GameJoltSecurity.user_token = token; + send(RequestType.SESSION_OPEN, false, function(err) { + userName = null; + GameJoltSecurity.user_token = null; + }, function(resp) { + trace('GameJolt logged in as ${userName}'); + ret = true; + openfl.Lib.application.onExit.add(onExitApp); + FlxG.signals.postUpdate.add(pingTimer); + }); + return ret; + } + + static function onExitApp(i:Int) + { + logout(); + } + + static var pingTime:Int = 0; + public static function pingTimer() + { + pingTime += 1; + if (pingTime < 10000) return; + pingTime -= 10000; + pingSession(); + } + + public static function pingSession() + { + send(RequestType.SESSION_PING(true), true, (str) -> { + trace('GameJolt session lost.'); + if (onLostSession != null) onLostSession(); + shutdownFunctions(); + active = false; + }); + } + + public static function logout() + { + if (!active) + return; + + shutdownFunctions(); + send(RequestType.SESSION_CLOSE, false, null, function(resp) { + trace('GameJolt account ${userName} logged out successfully.'); + userName = null; + GameJoltSecurity.user_token = null; + }); + } + + static function shutdownFunctions() + { + FlxG.signals.postUpdate.remove(pingTimer); + openfl.Lib.application.onExit.remove(onExitApp); + onLostSession = null; + } + + public static function send(call:RequestType, async:Bool = false, ?onError:String->Void, ?onComplete:Response->Void, ?onProgress:Array->Void) + { + if (executing || !active) + return; + executing = true; + + @:privateAccess + var resp:Response = GameJoltSecurity.handleRequest(async, call, onProgress); + executing = false; + if (resp.message != null && onError != null) + onError(resp.message); + else if (resp.message == null && onComplete != null) + onComplete(formatImages(resp)); + } + + static function formatImages(res:Response):Response { + if (res.users != null) + for (u in res.users) u.avatar_url = '${u.avatar_url.substring(0, 32)}1000${u.avatar_url.substr(34)}'.replace(".jpg", ".png") + .replace(".webp", ".png"); + if (res.trophies != null) for (t in res.trophies) { + var newUrl:String = ""; + if (t.image_url.startsWith('https://m.')) + newUrl = '${t.image_url.substring(0, 37)}1000${t.image_url.substr(40)}'.replace(".jpg", ".png").replace(".webp", ".png"); + else { + newUrl = "https://s.gjcdn.net/assets/"; + newUrl += switch (t.image_url.substring(24).replace(".jpg", "").replace(".webp", "")) { + case "trophy-bronze-1": "9c2c91d0"; + case "trophy-silver-1": "b46e352e"; + case "trophy-gold-1": "363ce2dc"; + case "trophy-platinum-1": "92e5330d"; + default: ""; + }; + newUrl += ".png"; + } + t.image_url = newUrl; + }; + if (res.responses != null) for (res2 in res.responses) res2 = formatImages(res2); + return res; + } + + static function set_userName(name:String):String + { + loggedIn = (name != null && name != ''); + return userName = name; + } +} \ No newline at end of file diff --git a/source/funkin/backend/utils/GameJoltSecurityPublic.hx b/source/funkin/backend/utils/GameJoltSecurityPublic.hx new file mode 100644 index 0000000000..3a51d6f1f2 --- /dev/null +++ b/source/funkin/backend/utils/GameJoltSecurityPublic.hx @@ -0,0 +1,341 @@ +package funkin.backend.utils; + +import hscript.IHScriptCustomBehaviour; +import funkin.backend.utils.GJUtil; +import funkin.backend.utils.GJUtil.RequestType; +import haxe.crypto.Md5; +import funkin.backend.utils.GJUtil.*; +import haxe.Http; +import haxe.Json; +import openfl.events.*; + +/** + * # A BIG MOTHERFUCKING WARNING + * + * This class handles the raw game keys for GameJolt keys. + * If the raw game keys are made public, people can mess with leaderboards, data, achievements, + * or whatever else is on the game page. + * + * As such, Codename Engine requires players to encrypt keys, and the method of + * encryption is non-disclosable for security reasons, so the original file + * used in distributed CNE builds cannot be shared. + * Instead, we provide this public-facing file for hardcoding purposes. + * + * To use this file, make a copy and rename the copy's filename and class name to + * `GameJoltSecurity.hx`. To modify the encryption method, go to the function + * `set_encryptedGameToken` and modify the code in the first half of the null + * check (`if (tok != null) {}`). + * + * **WE HIGHLY ENCOURAGE YOU TO COME UP WITH AN ENCRYPTION METHOD FOR GAMEJOLT KEYS.** + * HaxeFoundation's crypto package is installed with Codename, you can see the methods + * you can use (as well as the documentation) [here](https://github.com/HaxeFoundation/crypto). + * + * # ***DO NOT LET YOUR PLAYERS PUT THE RAW KEYS IN ANY SOFTCODED FILES!!! WE ARE NOT*** + * ***RESPONSIBLE IF YOU DON'T MAKE YOUR PLAYERS ENCRYPT THEIR KEYS AND THEIR STUFF*** + * ***GETS HACKED!!!!*** + * + * Also for security purposes, this class is unattainable via HScript. + * + * ~ SplatterDash + */ +@:noCustomClass +@:dox(hide) +abstract class GameJoltSecurityPublic implements IHScriptCustomBehaviour +{ + /** + * Token for the user if they're logged in. + */ + public static var user_token:String = ''; + + /** + * ID number for the current mod. + */ + public static var gameId:String = ''; + + /** + * The encrypted game token. Set using GAMEJOLT_ENCRYPTED_TOKEN in ini file. + */ + public static var encryptedGameToken(default, set):String; + + /** + * The unencrypted game token. It's insanely hard to get this variable. + */ + @:noPrivateAccess static var revealedGameToken:String; + + /** + * URL sent to GameJolt per request. + */ + @:noPrivateAccess static var url(get, never):String; + + /** + * The previous response created by the API client. Usually for just storage purposes. + */ + static var lastResponse:Response = {success: false, message: "No response yet."}; + + /** + * The current call being processed. + */ + static var curCall:Null = null; + + // hscript - thanks LJ :D + public function hget(name:String):Dynamic + { + return null; + } + + public function hset(name:String, val:Dynamic):Dynamic + { + return null; + } + + static function get_url():String + { + return sign('https://api.gamejolt.com/api/game/v1_2${parseType(curCall)}'); + } + + static function handleRequest(async:Bool = false, data:RequestType, ?onProgress:Array->Void):Response + { + if (encryptedGameToken == null || gameId == null) { + lastResponse = {success: false, message: 'Missing game token and/or game ID.'}; + curCall = null; + return lastResponse; + } + + curCall = data; + + if (async) { + var loader = new openfl.net.URLLoader(); + loader.addEventListener(Event.COMPLETE, function(complete) { + lastResponse = Json.parse(cast(loader.data, String)).response; + if (lastResponse.message != null) { + trace('Response Error: ${lastResponse.message}'); + } + + }); + loader.addEventListener(ProgressEvent.PROGRESS, progress -> { if (onProgress != null) onProgress([progress.bytesLoaded, progress.bytesTotal]);}); + loader.addEventListener(IOErrorEvent.IO_ERROR, function(ioError) { + lastResponse = {success: false, message: 'IO Error: ${ioError.text}'}; + }); + loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, (securityError) -> { + lastResponse = {success: false, message: 'Security Error: ${securityError.text}'}; + }); + loader.load(new openfl.net.URLRequest(url)); + return {success: false, message: "No response yet."}; + } else { + var loader:Http = new Http(url); + loader.onData = function(data) { + lastResponse = Json.parse(data).response; + if (lastResponse.message != null) + trace('Response Error: ${lastResponse.message}'); + }; + loader.onError = function(error) { + lastResponse = {success: false, message: 'Request Error: ${error}'}; + }; + loader.request(false); + } + curCall = null; + return lastResponse; + } + + static function parseType(request:RequestType, signed:Bool = false):String { + var command:String = ""; + var action:String = ""; + var params:Array<{name:String, value:String}> = []; + + switch (request) { + case BATCH(parallel, breakOnError, requests): + command = "batch"; + params.push({name: "parallel", value: '$parallel'}); + params.push({name: "break_on_error", value: '$breakOnError'}); + for (req in requests) params.push({name: "requests[]", value: parseType(req, true)}); + case DATA_FETCH(key, fromUser): + command = "data-store"; + params.push({name: "key", value: key.urlEncode()}); + if (fromUser) { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } + case DATA_GETKEYS(fromUser, pattern): + command = "data-store"; + action = "get-keys"; + if (pattern != null && pattern != "") + params.push({name: "pattern", value: pattern.urlEncode()}); + if (fromUser) { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } + case DATA_REMOVE(key, fromUser): + command = "data-store"; + action = "remove"; + params.push({name: "key", value: key.urlEncode()}); + if (fromUser) { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } + case DATA_SET(key, data, toUser): + command = "data-store"; + action = "set"; + params.push({name: "key", value: key.urlEncode()}); + params.push({name: "data", value: data.urlEncode()}); + if (toUser) { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } + case DATA_UPDATE(key, operation, toUser): + command = "data-store"; + action = "update"; + params.push({name: "key", value: key.urlEncode()}); + if (toUser) { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } + switch (operation) { + case Add(n): + params.push({name: 'operation', value: 'add'}); + params.push({name: 'value', value: '$n'}); + case Substract(n): + params.push({name: 'operation', value: 'substract'}); + params.push({name: 'value', value: '$n'}); + case Multiply(n): + params.push({name: 'operation', value: 'multiply'}); + params.push({name: 'value', value: '$n'}); + case Divide(n): + params.push({name: 'operation', value: 'divide'}); + params.push({name: 'value', value: '$n'}); + case Append(t): + params.push({name: 'operation', value: 'append'}); + params.push({name: 'value', value: t.urlEncode()}); + case Prepend(t): + params.push({name: 'operation', value: 'prepend'}); + params.push({name: 'value', value: t.urlEncode()}); + } + case FRIENDS: + command = "friends"; + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case TIME: + command = "time"; + case USER_AUTH: + command = "users"; + action = "auth"; + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case USER_FETCH(userOrID): + command = "users"; + var letters:Array = "ABCDEFGHIJKLMNÑOPQRSTUVWXYZ_-".split(""); + if (letters.filter(l -> userOrID.contains(l.toUpperCase()) || userOrID.contains(l.toLowerCase())).length > 0) + params.push({name: "username", value: userOrID}); + else + params.push({name: "user_id", value: userOrID.replace(",", "%2C")}); + case SESSION_OPEN: + command = "sessions"; + action = "open"; + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case SESSION_PING(active): + command = "sessions"; + action = "ping"; + params.push({name: "status", value: active ? "active" : "idle"}); + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case SESSION_CHECK: + command = "sessions"; + action = "check"; + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case SESSION_CLOSE: + command = "sessions"; + action = "close"; + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case SCORES_ADD(score, sort, extra_data, table_id): + command = "scores"; + action = "add"; + params.push({name: "score", value: score}); + params.push({name: "sort", value: '$sort'}); + if (extra_data != null && extra_data != "") + params.push({name: "extra_data", value: extra_data.urlEncode()}); + if (table_id != null) + params.push({name: "table_id", value: '$table_id'}); + if (user_token != "") { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } else + params.push({name: "guest", value: GJUtil.userName}); + case SCORES_GETRANK(sort, table_id): + command = "scores"; + action = "get-rank"; + params.push({name: "sort", value: '$sort'}); + if (table_id != null) + params.push({name: "table_id", value: '$table_id'}); + case SCORES_FETCH(fromUser, table_id, limit, betterThan): + command = "scores"; + if (table_id != null) + params.push({name: "table_id", value: '$table_id'}); + if (limit != null) + params.push({name: "limit", value: '$limit'}); + if (betterThan != null) + params.push({name: betterThan < 0 ? "worse_than" : "better_than", value: '${Math.abs(betterThan)}'}); + if (fromUser) { + if (user_token != "") { + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } else + params.push({name: "guest", value: GJUtil.userName}); + } + case SCORES_TABLES: + command = "scores"; + action = "tables"; + case TROPHIES_FETCH(achieved, trophy_id): + command = "trophies"; + if (achieved != null) + params.push({name: "achieved", value: '$achieved'}); + if (trophy_id != null) + params.push({name: "trophy_id", value: '$trophy_id'}); + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case TROPHIES_ADD(trophy_id): + command = "trophies"; + action = "add-achieved"; + params.push({name: "trophy_id", value: '$trophy_id'}); + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + case TROPHIES_REMOVE(trophy_id): + command = "trophies"; + action = "remove-achieved"; + params.push({name: "trophy_id", value: '$trophy_id'}); + params.push({name: "username", value: GJUtil.userName}); + params.push({name: "user_token", value: user_token}); + } + + var urlSection:String = '/$command${action != "" ? '/$action' : ""}?game_id=${gameId}${[for (p in params) '&${p.name}=${p.value}'].join("")}'; + if (signed) + urlSection = sign(urlSection).urlEncode(); + return urlSection; + } + + /** + * Setter function for encrypted game token. Also sets revealed game token. + * @param tok + */ + static function set_encryptedGameToken(tok:String) + { + if (tok != null) { + // Encryption method goes here. + revealedGameToken = tok; + } else { + revealedGameToken = null; + } + return encryptedGameToken = tok; + } + + /** + * Signs a piece of URL with Md5. + * @param daUrl The old URL piece. + * @return The new URL piece. + */ + static function sign(daUrl:String):String { + var urlToEncode:String = daUrl + revealedGameToken; + return '$daUrl&signature=${Md5.encode(urlToEncode)}'; + } +} \ No newline at end of file diff --git a/source/hscript/Config.hx b/source/hscript/Config.hx index 3acd554b16..7c8e03f105 100644 --- a/source/hscript/Config.hx +++ b/source/hscript/Config.hx @@ -29,7 +29,7 @@ class Config { // Incase any of your files fail // These are the module names public static final DISALLOW_CUSTOM_CLASSES = [ - + "funkin.backend.utils.GameJoltSecurity", // don't want people getting those gamejolt keys! ]; public static final DISALLOW_ABSTRACT_AND_ENUM = [ From 7f0f6f2c00d9ac35fdeac7c95c1c49fa7c3c916b Mon Sep 17 00:00:00 2001 From: SplatterDash <95732227+SplatterDash@users.noreply.github.com> Date: Sun, 9 Aug 2026 23:24:52 -0400 Subject: [PATCH 2/3] quick save data patch saves login information to save file (with options) when successfully logged in --- source/funkin/backend/utils/GJUtil.hx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/funkin/backend/utils/GJUtil.hx b/source/funkin/backend/utils/GJUtil.hx index 56fa88050b..cc8ee81e37 100644 --- a/source/funkin/backend/utils/GJUtil.hx +++ b/source/funkin/backend/utils/GJUtil.hx @@ -223,6 +223,8 @@ class GJUtil ret = true; openfl.Lib.application.onExit.add(onExitApp); FlxG.signals.postUpdate.add(pingTimer); + FlxG.save.data.gameJoltArray = [userName, token]; + FlxG.save.flush(); }); return ret; } From 773458e314fddb4d50db5116c962ede0af9d0e75 Mon Sep 17 00:00:00 2001 From: SplatterDash <95732227+SplatterDash@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:51:00 -0400 Subject: [PATCH 3/3] checkpoint commit! - tested/modified settings to ensure both GameJoltSecurity and NEW GameJoltData are inaccessible via Hscript - NEW SecretMacro - reads .env file if provided, and generates new one if missing (alongside new randomly-generated AES key if missing) - new method of initializing GameJolt for mods! > place Game ID in modpack.ini for GAMEJOLT_GAME_ID > place owner username/user token, raw game token (if you're worried keep reading), trophies, leaderboards, and data items in gamejolt.xml in data/config folder > load mod in-game - owner/game token info will be wiped from xml, game gives xml key, and game sets everything in xml (except game token and user token) in global data store for game > running a mod with gamejolt relies on global data store items for trophies, leaderboards, etc - only page owner can update this global key > for those worried about keys - each key can hold 16 MB, and in testing a global key with 6 trophies, a leaderboard, one custom data setting, and owner items takes up around 0.7 KB - some starts on GameJolt menus - WIP at time of commit encrypting/protecting game security keys is complete! part two now is limiting the attack surface for API calls - most of the calls that can be made now for softcoding are fetch calls and a planned GJUtil function for custom trophies (there are some that can be inputted for "defined" events - loading the mod for the first time, getting a first FC, completing all songs, completing a week - but these are inaccessible from softcode). still a bunch planned, especially with menus - but it's a fun little commit hehe --- .gitignore | 2 +- assets/data/config/menuItems.txt | 3 +- assets/images/menus/gamejolt-icon.png | Bin 0 -> 7063 bytes source/funkin/backend/system/Flags.hx | 6 +- source/funkin/backend/system/MainState.hx | 8 +- .../backend/system/gamejolt/GameJoltData.hx | 386 ++++++++++++++++++ .../gamejolt/GameJoltSecurity.hx} | 124 ++++-- .../backend/system/macros/SecretMacro.hx | 104 +++++ source/funkin/backend/utils/GJUtil.hx | 150 +++++-- source/funkin/menus/MainMenuState.hx | 15 + .../menus/gamejolt/GameJoltCompleteScreen.hx | 57 +++ .../menus/gamejolt/GameJoltLoginSuccess.hx | 23 ++ .../menus/gamejolt/GameJoltLoginWindow.hx | 52 +++ source/funkin/menus/gamejolt/GameJoltMenu.hx | 47 +++ .../menus/gamejolt/GameJoltTokenInfoWindow.hx | 32 ++ source/hscript/Config.hx | 5 +- 16 files changed, 937 insertions(+), 77 deletions(-) create mode 100644 assets/images/menus/gamejolt-icon.png create mode 100644 source/funkin/backend/system/gamejolt/GameJoltData.hx rename source/funkin/backend/{utils/GameJoltSecurityPublic.hx => system/gamejolt/GameJoltSecurity.hx} (78%) create mode 100644 source/funkin/backend/system/macros/SecretMacro.hx create mode 100644 source/funkin/menus/gamejolt/GameJoltCompleteScreen.hx create mode 100644 source/funkin/menus/gamejolt/GameJoltLoginSuccess.hx create mode 100644 source/funkin/menus/gamejolt/GameJoltLoginWindow.hx create mode 100644 source/funkin/menus/gamejolt/GameJoltMenu.hx create mode 100644 source/funkin/menus/gamejolt/GameJoltTokenInfoWindow.hx diff --git a/.gitignore b/.gitignore index 7401c22170..3138e6d0ed 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ addons/* dev-libs/ *.sum .vs/ -source/funkin/backend/utils/GameJoltSecurity.hx \ No newline at end of file +.env \ No newline at end of file diff --git a/assets/data/config/menuItems.txt b/assets/data/config/menuItems.txt index a8ec55b16e..677cc54593 100644 --- a/assets/data/config/menuItems.txt +++ b/assets/data/config/menuItems.txt @@ -1,4 +1,5 @@ story mode freeplay options -credits \ No newline at end of file +credits +gamejolt \ No newline at end of file diff --git a/assets/images/menus/gamejolt-icon.png b/assets/images/menus/gamejolt-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..70b5055600fa1aa287c13eb1a1d5fa50425cac2e GIT binary patch literal 7063 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 = []; - @:also(funkin.backend.utils.GameJoltSecurity.gameId) + @:also(funkin.backend.system.gamejolt.GameJoltSecurity.gameId) public static var MOD_GAMEJOLT_GAME_ID:String = ''; - @:also(funkin.backend.utils.GameJoltSecurity.encryptedGameToken) - public static var MOD_GAMEJOLT_TOKEN:String = ''; + @:also(funkin.backend.system.gamejolt.GameJoltSecurity.encryptedGameToken) + public static var MOD_GAMEJOLT_ENCRYPTED_TOKEN:String = ''; // -- Codename's Default Flags -- @:lazy public static var SAVE_PATH:String = haxe.macro.Compiler.getDefine("SAVE_PATH"); diff --git a/source/funkin/backend/system/MainState.hx b/source/funkin/backend/system/MainState.hx index 205cebcaad..5d920352b3 100644 --- a/source/funkin/backend/system/MainState.hx +++ b/source/funkin/backend/system/MainState.hx @@ -13,6 +13,7 @@ import funkin.backend.system.framerate.Framerate; import funkin.backend.utils.GJUtil; import funkin.editors.ModConfigWarning; import funkin.menus.TitleState; +import funkin.menus.gamejolt.GameJoltCompleteScreen; import haxe.io.Path; @@ -164,13 +165,10 @@ class MainState extends FlxState { } #if GAMEJOLT_API - if (FlxG.save.data.gameJoltArray != null) { - var gjDat:Array = FlxG.save.data.gameJoltArray; - GJUtil.attemptLogin(gjDat[0], gjDat[1]); - } + GJUtil.init(); #end - var startState:Class = Flags.DISABLE_WARNING_SCREEN ? TitleState : funkin.menus.WarningState; + var startState:Class = /**GameJoltData.freshStart ?**/ GameJoltCompleteScreen /**: (Flags.DISABLE_WARNING_SCREEN ? TitleState : funkin.menus.WarningState)**/; // In this case if the mod we just loaded a compressed modpack, we can't edit or modify files without decompressing it. if (Options.devMode && Options.allowConfigWarning && !isZipMod) { diff --git a/source/funkin/backend/system/gamejolt/GameJoltData.hx b/source/funkin/backend/system/gamejolt/GameJoltData.hx new file mode 100644 index 0000000000..7c87d8a3db --- /dev/null +++ b/source/funkin/backend/system/gamejolt/GameJoltData.hx @@ -0,0 +1,386 @@ +package funkin.backend.system.gamejolt; + +import haxe.xml.Access; +import haxe.io.Bytes; +import haxe.crypto.Aes; +import haxe.crypto.mode.Mode; +import haxe.crypto.padding.Padding; +import haxe.Json; +import sys.io.File; +import sys.FileSystem; +import flixel.input.keyboard.FlxKey; +import funkin.backend.utils.GJUtil; +import funkin.backend.utils.GJUtil.ScoreTable; +import funkin.backend.system.Controls.Control; +import funkin.backend.system.gamejolt.GameJoltSecurity; +import funkin.backend.assets.AssetSource; + + +typedef CNEGameJoltData = { + ownerU:String, + ownerI:Int, + defTrophies:Map, + cusTrophies:Map, + leaderboards:Map, + addlData:Map>, +} + +typedef GameJoltUserData = { + controls:Map>, + options:Map, + scores:Dynamic, + misc:Map +} + +typedef GJTrophyData = { + id:Int, + ?require:Int, + ?except:Array, + ?hidden:Bool, + ?weekName:String, +} + +class GameJoltData +{ + public static var freshStart(default, null):Bool = false; + + public static var ownerUsername:Null = null; + + public static var ownerUserId:Null = null; + + public static var leaderboards:Map = []; + + public static var definedTrophies:Map = []; + + public static var definitions:Array = ['open-first', 'friday-night', 'week', 'complete-all', 'fc-first', 'fc-all']; + + /** + * Custom trophies that mods may want to implement outside of the usual suspects. + * Unfortunately, these could be easy to cheese. + */ + public static var customTrophies(default, null):Map = []; + + public static var earnedTrophies:Map = []; + + public static var dataToInclude:Map> = []; + + static var xmlPath:String = Paths.xml('config/gamejolt'); + + static var helpText:String = ''; + + #if GAMEJOLT_API + public static function loadAdminData() + { + Logs.trace(Paths.assetsTree.getSpecificAsset(xmlPath, "TEXT", AssetSource.MODS)); + // get xml + var access = getGJX(); + + if (access == null) + return; + + // return if owner and token nodes are absent + if (!access.hasNode.owner || !access.hasNode.token) { + Logs.trace('Missing owner or token node from gamejolt.xml!', ERROR, LIGHTGRAY, 'GameJolt'); + return; + } + + if (!access.node.owner.has.username || !access.node.owner.has.token) { + Logs.trace('Missing username or user token in gamejolt.xml owner node!', ERROR, LIGHTGRAY, 'GameJolt'); + return; + } + + // encrypt token + encryptToken(access.node.token.innerData); + + // + if (!GJUtil.attemptLogin(access.node.owner.att.username, access.node.owner.att.token, true, true)) { + Logs.traceColored([ + Logs.getPrefix("GameJolt"), + Logs.logText("Unable to log in user "), + Logs.logText(access.node.owner.att.username, GREEN), + Logs.logText(' for data upload.') + ], ERROR); + return; + } + + ownerUsername = access.node.owner.att.username; + GameJoltSecurity.sendTrusted(USER_FETCH(ownerUsername), false, function(err) { + Logs.traceColored([ + Logs.getPrefix("GameJolt"), + Logs.logText("Unable to obtain mandatory user ID data from user "), + Logs.logText(ownerUsername, GREEN), + Logs.logText(' for data upload: ${err}') + ], ERROR); + ownerUsername = null; + GJUtil.logout(false, true); + }, function(resp) { + ownerUserId = resp.users[0].id; + }); + + if (ownerUserId == null) + return; + + setGlobalData(true, () -> { + GJUtil.logout(false, true); + var overwriteXml:Xml = access.x; + for (own in access.x.elements()) { + if (own.nodeName == "owner" || own.nodeName == "token") + overwriteXml.removeChild(own); + } + + try { + // FileSystem.deleteFile(Paths.assetsTree.getSpecificPath(xmlPath, AssetSource.MODS)); + File.saveContent(Paths.assetsTree.getSpecificPath(xmlPath, AssetSource.MODS), helpText + '\n' + overwriteXml.toString()); + } catch(e) { + Logs.trace('Error creating new XML file: ${e}', ERROR, LIGHTGRAY, "GameJolt"); + } + freshStart = true; + }, access); + } + + public static function loadGlobalData():Bool + { + var ret:Bool = false; + GameJoltSecurity.sendTrusted(DATA_FETCH('CNE_DATASTORE', false), true, function(err) { + Logs.trace('Unable to obtain global mod data. GameJolt API turning off automatically.', ERROR, LIGHTGRAY, 'GameJolt'); + GJUtil.logout(false, true); + }, function(resp) { + var daDat:CNEGameJoltData = cast Json.parse(resp.data); + ownerUsername = daDat.ownerU; + ownerUserId = daDat.ownerI; + definedTrophies = daDat.defTrophies; + customTrophies = daDat.cusTrophies; + leaderboards = daDat.leaderboards; + dataToInclude = daDat.addlData; + ret = true; + }); + return ret; + } + + //region Set CNE Glob'ls + public static function setGlobalData(cleanSet:Bool = false, ?callback:Void->Void, ?data:Access) + { + if (data == null) { + data = getGJX(); + if (data == null) { + Logs.traceColored([ + Logs.getPrefix("GameJolt"), + Logs.logText("Unable to locate "), + Logs.logText("gamejolt.xml", GREEN), + Logs.logText(' in '), + Logs.logText("data/config", GREEN), + Logs.logText(' folder!'), + ], ERROR); + return; + } + } + + // With all of these nodes, it's important to make sure that + // what we're importing a) exists, and b) isn't just a blank string. + + for (trophyDef in data.node.trophies.nodes.defined) { + if (!trophyDef.has.def || !trophyDef.has.id || trophyDef.att.def == '' || trophyDef.att.id == '') + continue; + + if (!definitions.contains(trophyDef.att.def)) + customTrophies.set(trophyDef.att.def, { + id: Std.parseInt(trophyDef.att.id), + require: (trophyDef.has.require && trophyDef.att.require != '') ? Std.parseInt(trophyDef.att.require) : null, + except: (trophyDef.has.except && trophyDef.att.except != '') ? trophyDef.att.except.split('//') : null, + hidden: (trophyDef.has.hidden && trophyDef.att.hidden != '') ? (trophyDef.att.hidden == 'true') : null, + weekName: null, + }); + + if (trophyDef.att.def == 'week' && (!trophyDef.has.weekName || trophyDef.att.weekName == '')) + continue; + + definedTrophies.set(trophyDef.att.def + (trophyDef.att.def == 'week' ? '-${trophyDef.att.weekName}': ''), { + id: Std.parseInt(trophyDef.att.id), + require: (trophyDef.has.require && trophyDef.att.require != '') ? Std.parseInt(trophyDef.att.require) : null, + except: (trophyDef.has.except && trophyDef.att.except != '') ? trophyDef.att.except.split('//') : null, + hidden: (trophyDef.has.hidden && trophyDef.att.hidden != '') ? (trophyDef.att.hidden == 'true') : null, + weekName: (trophyDef.att.def == 'week' && trophyDef.has.weekName && trophyDef.att.weekName != '') ? trophyDef.att.weekName : null, + }); + } + + for (trophyDef in data.node.trophies.nodes.custom) { + if (!trophyDef.has.def || !trophyDef.has.id) + continue; + + customTrophies.set(trophyDef.att.def, { + id: Std.parseInt(trophyDef.att.def), + require: (trophyDef.has.require && trophyDef.att.require != '') ? Std.parseInt(trophyDef.att.require) : null, + except: (trophyDef.has.except && trophyDef.att.except != '') ? trophyDef.att.except.split('//') : null, + hidden: (trophyDef.has.hidden && trophyDef.att.hidden != '') ? (trophyDef.att.hidden == 'true') : null, + weekName: null, + }); + } + + for (leaderboard in data.node.leaderboards.nodes.song) { + if (!leaderboard.has.id || !leaderboard.has.name || leaderboard.att.name == '' || leaderboard.att.id == '') + continue; + + leaderboards.set(leaderboard.att.name + (leaderboard.has.diff ? '-${leaderboard.att.diff}' : '') + ' (${leaderboard.has.vari ? leaderboard.att.vari : 'Default'})', Std.parseInt(leaderboard.att.id)); + } + + for (dat in data.node.data.nodes.value) { + if (!dat.has.name || dat.att.name == '') + continue; + + var location:String = (dat.has.inSave && dat.att.inSave != '') ? dat.att.inSave : "FlxG"; + var curVars:Array = dataToInclude.exists(location) ? dataToInclude.get(location) : []; + curVars.push(dat.att.name); + dataToInclude.set(location, curVars); + } + + var sendOut:CNEGameJoltData = { + ownerU: ownerUsername, + ownerI: ownerUserId, + defTrophies: definedTrophies, + cusTrophies: customTrophies, + leaderboards: leaderboards, + addlData: dataToInclude, + }; + + Logs.trace('Sending global data...', INFO, LIGHTGRAY, "GameJolt"); + + var success:Bool = true; + GameJoltSecurity.sendTrusted(DATA_SET('CNE_DATASTORE', Json.stringify(sendOut), false), false, function(err) { + Logs.trace('Unable to upload global GameJolt data: ${err}', ERROR, LIGHTGRAY, 'GameJolt'); + success = false; + if (cleanSet) { + reset(); + GJUtil.logout(false, true); + } + }); + + if (!success) + return; + + Logs.trace('Global data set successfully!', SUCCESS, LIGHTGRAY, "GameJolt"); + + if (callback != null) callback(); + } + //endregion + + public static function setUserData() + { + + } + + public static function loadUserData() + { + + } + + static function getGJX():Null + { + if (!Paths.assetsTree.existsSpecific(xmlPath, "TEXT", AssetSource.MODS)) + return null; + + var access:Access = null; + try { + access = new Access(Xml.parse(Paths.assetsTree.getSpecificAsset(xmlPath, "TEXT", AssetSource.MODS)).firstElement()); + } catch(e) { + Logs.trace('Error while parsing gamejolt.xml: ${Std.string(e)}', ERROR, LIGHTGRAY, 'GameJolt'); + } + return access; + } + + static function encryptToken(token:String) + { + var validHex:String = "0123456789abcdef"; + + var dateString:String = Date.now().toString(); + + var hexString:String = ''; + + for (i in 0...dateString.length - 1) { + if (dateString.charAt(i) == ' ') continue; + if (hexString.length == 32) break; + var charCode:Int = StringTools.fastCodeAt(dateString, i); + var charString:String = StringTools.hex(charCode); + hexString += charString.substr(0, Std.int(Math.min(charString.length, 32 - hexString.length))); + } + + for (i in 0...(32 - hexString.length)) { + if (FlxG.random.bool()) { + hexString += validHex.charAt(FlxG.random.int(0, validHex.length - 1)); + } else { + hexString = validHex.charAt(FlxG.random.int(0, validHex.length - 1)) + hexString; + } + } + var iv:Bytes = Bytes.ofHex(hexString); + + @:privateAccess + var aes:Aes = new Aes(Bytes.ofHex(GameJoltSecurity.CODENAME_AES_KEY), iv); + var encryp:Bytes = aes.encrypt(Mode.OFB, Bytes.ofString(token), Padding.NoPadding); + + GameJoltSecurity.encryptedGameToken = Flags.MOD_GAMEJOLT_ENCRYPTED_TOKEN = hexString.toLowerCase() + encryp.toHex(); + } + + public static function reset(fullWipe:Bool = false) + { + earnedTrophies = []; + if (fullWipe) { + ownerUsername = null; + ownerUserId = null; + leaderboards = []; + definedTrophies = []; + customTrophies = []; + dataToInclude = []; + freshStart = false; + } + } + #else + public static function loadAdminData() + { + Logs.trace('GameJolt API not set in Project.xml!', ERROR, LIGHTGRAY, 'GameJolt'); + return; + } + + public static function loadGlobalData() + { + Logs.trace('GameJolt API not set in Project.xml!', ERROR, LIGHTGRAY, 'GameJolt'); + return; + } + + public static function setGlobalData(cleanSet:Bool = false, ?callback:Void->Void, ?data:Access) + { + Logs.trace('GameJolt API not set in Project.xml!', ERROR, LIGHTGRAY, 'GameJolt'); + return; + } + + public static function setUserData() + { + Logs.trace('GameJolt API not set in Project.xml!', ERROR, LIGHTGRAY, 'GameJolt'); + return; + } + + public static function loadUserData() + { + Logs.trace('GameJolt API not set in Project.xml!', ERROR, LIGHTGRAY, 'GameJolt'); + return; + } + + public static function reset(fullWipe:Bool = false) + { + Logs.trace('GameJolt API not set in Project.xml!', ERROR, LIGHTGRAY, 'GameJolt'); + return; + } + #end +} \ No newline at end of file diff --git a/source/funkin/backend/utils/GameJoltSecurityPublic.hx b/source/funkin/backend/system/gamejolt/GameJoltSecurity.hx similarity index 78% rename from source/funkin/backend/utils/GameJoltSecurityPublic.hx rename to source/funkin/backend/system/gamejolt/GameJoltSecurity.hx index 3a51d6f1f2..217e05bccd 100644 --- a/source/funkin/backend/utils/GameJoltSecurityPublic.hx +++ b/source/funkin/backend/system/gamejolt/GameJoltSecurity.hx @@ -1,9 +1,12 @@ -package funkin.backend.utils; +package funkin.backend.system.gamejolt; -import hscript.IHScriptCustomBehaviour; +import hscript.IHScriptCustomAccessBehaviour; import funkin.backend.utils.GJUtil; import funkin.backend.utils.GJUtil.RequestType; -import haxe.crypto.Md5; +import haxe.crypto.*; +import haxe.crypto.mode.Mode; +import haxe.crypto.padding.Padding; +import haxe.io.Bytes; import funkin.backend.utils.GJUtil.*; import haxe.Http; import haxe.Json; @@ -16,31 +19,16 @@ import openfl.events.*; * If the raw game keys are made public, people can mess with leaderboards, data, achievements, * or whatever else is on the game page. * - * As such, Codename Engine requires players to encrypt keys, and the method of - * encryption is non-disclosable for security reasons, so the original file - * used in distributed CNE builds cannot be shared. - * Instead, we provide this public-facing file for hardcoding purposes. - * - * To use this file, make a copy and rename the copy's filename and class name to - * `GameJoltSecurity.hx`. To modify the encryption method, go to the function - * `set_encryptedGameToken` and modify the code in the first half of the null - * check (`if (tok != null) {}`). - * - * **WE HIGHLY ENCOURAGE YOU TO COME UP WITH AN ENCRYPTION METHOD FOR GAMEJOLT KEYS.** - * HaxeFoundation's crypto package is installed with Codename, you can see the methods - * you can use (as well as the documentation) [here](https://github.com/HaxeFoundation/crypto). - * - * # ***DO NOT LET YOUR PLAYERS PUT THE RAW KEYS IN ANY SOFTCODED FILES!!! WE ARE NOT*** - * ***RESPONSIBLE IF YOU DON'T MAKE YOUR PLAYERS ENCRYPT THEIR KEYS AND THEIR STUFF*** - * ***GETS HACKED!!!!*** + * As such, Codename Engine requires players to encrypt keys using the AES protocol, and + * the key to decrypt such keys is non-disclosable (hence why it's in a .env file). * * Also for security purposes, this class is unattainable via HScript. * * ~ SplatterDash */ -@:noCustomClass -@:dox(hide) -abstract class GameJoltSecurityPublic implements IHScriptCustomBehaviour + +@:noCustomClass @:build(funkin.backend.system.macros.SecretMacro.build()) +class GameJoltSecurity implements IHScriptCustomAccessBehaviour { /** * Token for the user if they're logged in. @@ -52,6 +40,17 @@ abstract class GameJoltSecurityPublic implements IHScriptCustomBehaviour */ public static var gameId:String = ''; + /** + * Map of all trophies defined using specific names. + * Specifically stored here so that it's harder to cheese song, + * FC or other specific trophies. + */ + public static var definedTrophies(default, null):Map = []; + + /** + * + */ + public static var definedLeaderboards(default, null):Map = []; /** * The encrypted game token. Set using GAMEJOLT_ENCRYPTED_TOKEN in ini file. */ @@ -60,39 +59,80 @@ abstract class GameJoltSecurityPublic implements IHScriptCustomBehaviour /** * The unencrypted game token. It's insanely hard to get this variable. */ - @:noPrivateAccess static var revealedGameToken:String; + @:noPrivateAccess private static var revealedGameToken:String; /** * URL sent to GameJolt per request. */ - @:noPrivateAccess static var url(get, never):String; + @:noPrivateAccess private static var url(get, never):String; /** * The previous response created by the API client. Usually for just storage purposes. */ - static var lastResponse:Response = {success: false, message: "No response yet."}; + private static var lastResponse:Response = {success: false, message: "No response yet."}; /** * The current call being processed. */ - static var curCall:Null = null; + private static var curCall:Null = null; + + /** + * The secret key to decrypt GameJolt keys using AES. This cannot be traced or located in any way. + */ + @:envField + private static final CODENAME_AES_KEY:Null; // hscript - thanks LJ :D + + //region IHScriptCustomAccessBehaviour implementation + public var __allowSetGet:Bool = false; + public function hget(name:String):Dynamic - { return null; - } public function hset(name:String, val:Dynamic):Dynamic - { return null; - } + + public function __callGetter(name:String):Dynamic + return null; + + public function __callSetter(name:String, val:Dynamic):Dynamic + return null; + //endregion static function get_url():String { return sign('https://api.gamejolt.com/api/game/v1_2${parseType(curCall)}'); } + /** + * This is a copy of GJUtil's "send" request, but kept here since Hscript can't get here. + * Any calls here are guaranteed to be from hardcoding. + * @param call + * @param async + * @param onError + * @param onComplete + * @param onProgress + */ + public static function sendTrusted(call:RequestType, async:Bool = false, ?onError:String->Void, ?onComplete:Response->Void, ?onProgress:Array->Void) + { + @:privateAccess { + if (GJUtil.executing || !GJUtil.active) + return; + GJUtil.executing = true; + } + + var resp:Response = handleRequest(async, call, onProgress); + @:privateAccess + GJUtil.executing = false; + if (resp.message != null && onError != null) + onError(resp.message); + else if (resp.message == null && onComplete != null) { + @:privateAccess + onComplete(GJUtil.formatImages(resp)); + } + } + static function handleRequest(async:Bool = false, data:RequestType, ?onProgress:Array->Void):Response { if (encryptedGameToken == null || gameId == null) { @@ -108,7 +148,10 @@ abstract class GameJoltSecurityPublic implements IHScriptCustomBehaviour loader.addEventListener(Event.COMPLETE, function(complete) { lastResponse = Json.parse(cast(loader.data, String)).response; if (lastResponse.message != null) { - trace('Response Error: ${lastResponse.message}'); + Logs.traceColored([ + Logs.getPrefix("GameJolt"), + Logs.logText('Response Error: ${lastResponse.message}') + ], ERROR); } }); @@ -126,7 +169,10 @@ abstract class GameJoltSecurityPublic implements IHScriptCustomBehaviour loader.onData = function(data) { lastResponse = Json.parse(data).response; if (lastResponse.message != null) - trace('Response Error: ${lastResponse.message}'); + Logs.traceColored([ + Logs.getPrefix("GameJolt"), + Logs.logText('Response Error: ${lastResponse.message}') + ], ERROR); }; loader.onError = function(error) { lastResponse = {success: false, message: 'Request Error: ${error}'}; @@ -320,9 +366,15 @@ abstract class GameJoltSecurityPublic implements IHScriptCustomBehaviour */ static function set_encryptedGameToken(tok:String) { - if (tok != null) { - // Encryption method goes here. - revealedGameToken = tok; + if (tok != null && CODENAME_AES_KEY != null) { + var iv:String = tok.substr(0, 32); + var theK:String = tok.substr(32); + + var aes:Aes = new Aes(Bytes.ofHex(CODENAME_AES_KEY), Bytes.ofHex(iv.toUpperCase())); + + var dat:String = aes.decrypt(Mode.OFB, Bytes.ofHex(theK), Padding.NoPadding).toString(); + + revealedGameToken = dat; } else { revealedGameToken = null; } diff --git a/source/funkin/backend/system/macros/SecretMacro.hx b/source/funkin/backend/system/macros/SecretMacro.hx new file mode 100644 index 0000000000..3385db3735 --- /dev/null +++ b/source/funkin/backend/system/macros/SecretMacro.hx @@ -0,0 +1,104 @@ +package funkin.backend.system.macros; + +#if macro +import sys.io.File; +import sys.FileSystem; +import Sys; +import haxe.macro.Expr; +import haxe.macro.Expr.Field; +import haxe.macro.Context; +#end + +@:dox(hide) class SecretMacro { + static var envPath:String = '.env'; + // A little bit taken from Funkin's base code. Not too much though. + public static macro function build():Array + { + var contents:Null = !FileSystem.exists(envPath) ? buildEnv() : File.getContent(envPath); + + if (contents == null) + contents = buildEnv(); + + final indivVars:Array = contents.split('\n'); + + final envMap:Map = []; + + for (varE in indivVars) { + // If there's no equal sign, or multiple equal signs, we don't process this variable due to errors. + if (varE.indexOf('=') == -1 || varE.indexOf('=') != varE.lastIndexOf('=')) + continue; + + var titAndVal:Array = varE.split('='); + envMap.set(titAndVal[0], titAndVal[1]); + } + + // Now we look at the actual fields to try and match things up. + final buildFields:Array = Context.getBuildFields(); + + for (fld in buildFields) { + if (fld.access.contains(AStatic)) switch(fld.kind) { + case FVar(t, e): + for (meta in fld.meta) { + if (meta.name != ':envField') + continue; + + // Not gonna do the 'mandatoryIfDefined' stuff because that's not necessary! + // But because we're not fixing what isn't broken, time for null string checks. + var isNullString:Bool = false; + switch (t) { + case TPath(tp): + if (tp.name == 'Null' && tp.params != null && tp.params.length == 1) + switch (tp.params[0]) { + case TPType(TPath(tptp)): + if (tptp.name == 'String') + { + isNullString = true; + } + case _: + } + case _: + } + + if (!isNullString) + Context.fatalError('Field ${fld.name} must be of type Null to use :envField', fld.pos) + else { + if (envMap.exists(fld.name)) + buildFields[buildFields.indexOf(fld)].kind = FVar(t, macro $v{envMap.get(fld.name)}); + else + Sys.println('WARNING: Value for environment variable "${fld.name}" not found.'); + } + } + case _: + // nothing lol + } + } + return buildFields; + } + + private static function buildEnv():Null + { + if (sys.FileSystem.exists(envPath)) + return sys.io.File.getContent(envPath); + + // For future peoples: to append an item to .env, just add it as a new item in this array. + // Use the format 'NAME_ALL_CAPS=value' - like an ini file! + var items:Array = ['CODENAME_AES_KEY=${generateKey()}']; + var finalProd:String = items.join('\n'); + + sys.io.File.saveContent(Sys.getCwd() + '\\.env', finalProd); + + return finalProd; + } + + private static function generateKey():String + { + var validChars:String = "0123456789ABCDEF"; + var outputStr:String = ''; + + for (i in 0...64) + { + outputStr += validChars.charAt(Math.round(Math.random() * 15)); + } + return outputStr; + } +} \ No newline at end of file diff --git a/source/funkin/backend/utils/GJUtil.hx b/source/funkin/backend/utils/GJUtil.hx index cc8ee81e37..e05569ed9a 100644 --- a/source/funkin/backend/utils/GJUtil.hx +++ b/source/funkin/backend/utils/GJUtil.hx @@ -1,5 +1,8 @@ package funkin.backend.utils; +import funkin.backend.system.gamejolt.GameJoltData; +import funkin.backend.system.gamejolt.GameJoltSecurity; + /** * This is how GameJolt API responses are formatted like. */ @@ -94,7 +97,7 @@ typedef Trophy = { * The way the user data is fetched from the GameJolt API. * * @param id The ID of the User. - * @param type The cathegory the User is cataloged like in GameJolt. + * @param type The category the User is cataloged like in GameJolt. * @param username The username of the User. (Also available for guests). * @param avatar_url The link of the avatar of the User. * @param signed_up A short description about how long the User have been in GameJolt. @@ -183,6 +186,11 @@ class GJUtil */ public static var userName(default, set):String; + /** + * The avatar of the logged in user. + */ + public static var userAvatarUrl(default, null):String; + /** * Whether or not the GameJolt utility is operational. * This cannot be set other than load operations. @@ -190,14 +198,36 @@ class GJUtil public static var active(default, null):Bool = false; /** - * Whether or not the utility is executing a call. + * Helper function in case the session is lost in the middle of the game. */ - static var executing:Bool = false; + public static var onLostSession:NullVoid> = null; /** - * Helper function in case the session is lost in the middle of the game. + * Helper function that runs when an achievement is unlocked. + * Can be useful for notifications. */ - public static var onLostSession:NullVoid> = null; + public static var onTrophyUnlock:Null->Void> = null; + + /** + * Whether or not the utility is executing a call. + */ + static var executing:Bool = false; + + #if GAMEJOLT_API + public static function init() + { + if (Flags.MOD_GAMEJOLT_GAME_ID == '') + return; + + if (Flags.MOD_GAMEJOLT_ENCRYPTED_TOKEN == '') { + GameJoltData.loadAdminData(); + if (Flags.MOD_GAMEJOLT_ENCRYPTED_TOKEN == '') + return; + } else if (FlxG.save.data.gameJoltArray != null) { + var gjDat:Array = FlxG.save.data.gameJoltArray; + GJUtil.attemptLogin(gjDat[0], gjDat[1]); + } + } /** * Helper function to simplify the login process. @@ -205,9 +235,9 @@ class GJUtil * @param token User token of user attempting to login. * @return Bool Whether the attempt was successfull or not. */ - public static function attemptLogin(name:String, token:String):Bool + public static function attemptLogin(name:String, token:String, checkCreds:Bool = false, tempLogin:Bool = false):Bool { - if(Flags.MOD_GAMEJOLT_GAME_ID != '' && Flags.MOD_GAMEJOLT_TOKEN != '') + if(Flags.MOD_GAMEJOLT_GAME_ID != '' && Flags.MOD_GAMEJOLT_ENCRYPTED_TOKEN != '') active = true else return false; @@ -215,27 +245,88 @@ class GJUtil var ret:Bool = false; userName = name; GameJoltSecurity.user_token = token; - send(RequestType.SESSION_OPEN, false, function(err) { + var batchCalls:Array = [RequestType.SESSION_OPEN]; + if (checkCreds) batchCalls.unshift(RequestType.USER_AUTH); + send(RequestType.BATCH(true, false, batchCalls), false, function(err) { userName = null; GameJoltSecurity.user_token = null; }, function(resp) { - trace('GameJolt logged in as ${userName}'); ret = true; - openfl.Lib.application.onExit.add(onExitApp); - FlxG.signals.postUpdate.add(pingTimer); - FlxG.save.data.gameJoltArray = [userName, token]; - FlxG.save.flush(); + if (!tempLogin) { + Logs.traceColored([ + Logs.getPrefix("GameJolt"), + Logs.logText("Successfully logged in user "), + Logs.logText(userName, GREEN), + Logs.logText('!') + ], SUCCESS); + openfl.Lib.application.onExit.add(onExitApp); + FlxG.signals.postUpdate.add(pingTimer); + if (checkCreds) { + FlxG.save.data.gameJoltArray = [userName, token]; + FlxG.save.flush(); + } + } }); return ret; } - static function onExitApp(i:Int) + public static function logout(wipeSave:Bool = false, tempLogin:Bool = false) { - logout(); + if (!active) + return; + if (!tempLogin) + shutdownFunctions(); + send(RequestType.SESSION_CLOSE, true, null, function(resp) { + if (!tempLogin) + Logs.traceColored([ + Logs.getPrefix("GameJolt"), + Logs.logText("User "), + Logs.logText(userName, GREEN), + Logs.logText(' logged out successfully.') + ], VERBOSE); + userName = null; + GameJoltSecurity.user_token = null; + if (wipeSave) { + FlxG.save.data.gameJoltArray = null; + FlxG.save.flush(); + } + }); + } + + public static function makeCall(call:RequestType, async:Bool = false, ?onError:String->Void, ?onComplete:Response->Void, ?onProgress:Array->Void) + { + switch(call) { + case BATCH(parallel, breakOnError, requests): + return; + + case DATA_GETKEYS(fromUser, pattern): + return; + + case DATA_REMOVE(key, fromUser): + return; + + case DATA_SET(key, data, toUser): + return; + + case DATA_UPDATE(key, operation, toUser): + return; + + case USER_AUTH: + return; + + case SCORES_ADD(score, sort, extra_data, table_id): + return; + + case TROPHIES_ADD(trophy_id): + return; + + case _: + send(call, async, onError, onComplete, onProgress); + } } static var pingTime:Int = 0; - public static function pingTimer() + static function pingTimer() { pingTime += 1; if (pingTime < 10000) return; @@ -243,37 +334,35 @@ class GJUtil pingSession(); } - public static function pingSession() + static function pingSession() { send(RequestType.SESSION_PING(true), true, (str) -> { - trace('GameJolt session lost.'); if (onLostSession != null) onLostSession(); shutdownFunctions(); + Logs.trace("Session lost; GJUtil shut down successfully.", WARNING, LIGHTGRAY, 'GameJolt'); active = false; }); } - public static function logout() + static function onExitApp(i:Int) { - if (!active) - return; - - shutdownFunctions(); - send(RequestType.SESSION_CLOSE, false, null, function(resp) { - trace('GameJolt account ${userName} logged out successfully.'); - userName = null; - GameJoltSecurity.user_token = null; - }); + logout(); } static function shutdownFunctions() { + Logs.traceColored([ + Logs.getPrefix("GameJolt"), + Logs.logText("Logging out user "), + Logs.logText(userName, GREEN), + Logs.logText('...') + ], INFO); FlxG.signals.postUpdate.remove(pingTimer); openfl.Lib.application.onExit.remove(onExitApp); onLostSession = null; } - public static function send(call:RequestType, async:Bool = false, ?onError:String->Void, ?onComplete:Response->Void, ?onProgress:Array->Void) + @:noPrivateAccess static function send(call:RequestType, async:Bool = false, ?onError:String->Void, ?onComplete:Response->Void, ?onProgress:Array->Void) { if (executing || !active) return; @@ -318,4 +407,7 @@ class GJUtil loggedIn = (name != null && name != ''); return userName = name; } + #else + + #end } \ No newline at end of file diff --git a/source/funkin/menus/MainMenuState.hx b/source/funkin/menus/MainMenuState.hx index 43f33ebb53..ae44f4b5ce 100644 --- a/source/funkin/menus/MainMenuState.hx +++ b/source/funkin/menus/MainMenuState.hx @@ -7,6 +7,7 @@ import funkin.backend.FunkinText; import funkin.backend.scripting.events.menu.MenuChangeEvent; import funkin.backend.scripting.events.NameEvent; import funkin.menus.credits.CreditsMain; +import funkin.menus.gamejolt.GameJoltMenu; import funkin.options.OptionsMenu; import lime.app.Application; @@ -62,6 +63,8 @@ class MainMenuState extends MusicBeatState for (i=>option in optionShit) { + if (option == 'gamejolt') + continue; var menuItem:FlxSprite = new FlxSprite(0, 60 + (i * 160)); menuItem.frames = Paths.getFrames('menus/mainmenu/${option}'); menuItem.animation.addByPrefix('idle', option + " basic", 24); @@ -74,6 +77,17 @@ class MainMenuState extends MusicBeatState menuItem.antialiasing = true; } + var menuItem:FlxSprite = new FlxSprite(FlxG.width - 138, FlxG.height - 138).loadGraphic(Paths.image('menus/gamejolt-icon')); + menuItem.setGraphicSize(128); + menuItem.updateHitbox(); + menuItem.animation.add('idle', [0], 1, false); + menuItem.animation.add('selected', [0], 1, false); + menuItem.animation.play('idle'); + menuItem.ID = menuItems.length; + menuItems.add(menuItem); + menuItem.scrollFactor.set(); + menuItem.antialiasing = true; + FlxG.camera.follow(camFollow, null, 0.06); versionText = new FunkinText(5, FlxG.height - 2, 0, [ @@ -190,6 +204,7 @@ class MainMenuState extends MusicBeatState case 'freeplay': FlxG.switchState(new FreeplayState()); case 'donate', 'credits': FlxG.switchState(new CreditsMain()); // kept donate for not breaking scripts, if you don't want donate to bring you to the credits menu, thats easy softcodable - Nex case 'options': FlxG.switchState(new OptionsMenu()); + case 'gamejolt': FlxG.switchState(new GameJoltMenu()); } }); } diff --git a/source/funkin/menus/gamejolt/GameJoltCompleteScreen.hx b/source/funkin/menus/gamejolt/GameJoltCompleteScreen.hx new file mode 100644 index 0000000000..554ec816ac --- /dev/null +++ b/source/funkin/menus/gamejolt/GameJoltCompleteScreen.hx @@ -0,0 +1,57 @@ +package funkin.menus.gamejolt; + +import funkin.editors.ui.UIState; +import funkin.editors.ui.UIButton; +import funkin.editors.ui.UIText; +import openfl.desktop.Clipboard; +import flixel.addons.display.FlxBackdrop; +import funkin.backend.system.gamejolt.GameJoltData; +import funkin.menus.TitleState; +import funkin.backend.system.Controls; + +class GameJoltCompleteScreen extends UIState +{ + var mainText:UIText; + var copyText:UIText; + var copyButton:UIButton; + var continueText:UIText; + + override public function create() + { + super.create(); + + add(mainText = new UIText(0, 50, FlxG.width, + 'Hey there funkhead! + Your GameJolt data was recognized and registered to your game\'s data store successfully. + From here on, only ${GameJoltData.ownerUsername} will be able to change the global data. As for the XML - for security purposes, we took out the login info and game token. Feel free to discard the XML entirely, or keep it as a souvenir - your globals load from your game\'s data store now! + Before you go: it\'s important to press the "Copy" button below and copy the following text into your mod\'s modpack.ini file. This is your game\'s security key encrypted uniquely for this build. Paste it in the "Common" section.', + 30)); + + mainText.alignment = CENTER; + mainText.antialiasing = true; + + add(copyText = new UIText(0, mainText.height + 60, FlxG.width, 'GAMEJOLT_ENCRYPTED_TOKEN=\'${Flags.MOD_GAMEJOLT_ENCRYPTED_TOKEN}\'', 16)); + copyText.alignment = CENTER; + copyText.antialiasing = true; + + add(copyButton = new UIButton(0, copyText.y + copyText.height + 10, "Copy", () -> { + Clipboard.generalClipboard.setData(TEXT_FORMAT, copyText.text); + })); + copyButton.color = 0xFF3F3FFF; + copyButton.x = (FlxG.width / 2) - (copyButton.bWidth / 2); + + add(continueText = new UIText(0, copyButton.y + copyButton.bHeight + 30, FlxG.width, '~ Press ${controls.getKeyName(ACCEPT)} to continue ~')); + continueText.alignment = CENTER; + + CoolUtil.playMusic(Paths.music('breakfast')); + } + + override public function update(elapsed:Float) + { + super.update(elapsed); + if (controls.ACCEPT) { + CoolUtil.playMenuSFX(CONFIRM); + FlxG.switchState(new TitleState()); + } + } +} \ No newline at end of file diff --git a/source/funkin/menus/gamejolt/GameJoltLoginSuccess.hx b/source/funkin/menus/gamejolt/GameJoltLoginSuccess.hx new file mode 100644 index 0000000000..5e27a0c4c6 --- /dev/null +++ b/source/funkin/menus/gamejolt/GameJoltLoginSuccess.hx @@ -0,0 +1,23 @@ +package funkin.menus.gamejolt; + +import funkin.editors.ui.UIButton; +import funkin.editors.ui.UISubstateWindow; + +class GameJoltLoginSuccess extends UISubstateWindow +{ + var confirmButton:UIButton; + + public override function create() + { + winTitle = "Login Success!"; + + messageSpr.text = 'You have successfully logged in as ${GJUtil.userName}!'; + + super.create(); + + add(confirmButton = new UIButton(windowSpr.x + (windowSpr.bWidth / 2) - 62, windowSpr.y + windowSpr.bHeight - 48, "Sweet!", function() { + close(); + }, 125)); + confirmButton.color = 0xFF00FF00; + } +} \ No newline at end of file diff --git a/source/funkin/menus/gamejolt/GameJoltLoginWindow.hx b/source/funkin/menus/gamejolt/GameJoltLoginWindow.hx new file mode 100644 index 0000000000..84a127cd7d --- /dev/null +++ b/source/funkin/menus/gamejolt/GameJoltLoginWindow.hx @@ -0,0 +1,52 @@ +package funkin.menus.gamejolt; + +import funkin.editors.ui.UISubstateWindow; +import funkin.editors.ui.UIButton; +import funkin.editors.ui.UITextBox; +import funkin.editors.ui.UIText; +import funkin.menus.gamejolt.GameJoltTokenInfoWindow; + +class GameJoltLoginWindow extends UISubstateWindow { + public var usernameBox:UITextBox; + public var userTokenLabel:UIText; + public var userTokenInfo:UIButton; + public var userTokenBox:UITextBox; + + public var closeButton:UIButton; + public var loginButton:UIButton; + + public var daX:Float; + + public override function create() { + //TODO: get translations for text + winTitle = "Login with GameJolt..."; + + winWidth = 350; + winHeight = 350; + + super.create(); + + daX = windowSpr.x + 20; + + add(usernameBox = new UITextBox(daX, windowSpr.y + 60, "")); + usernameBox.members.push(new UIText(daX, usernameBox.y - 24, 0, "Username")); + + add(userTokenBox = new UITextBox(daX, usernameBox.y + usernameBox.height + 60, "")); + userTokenBox.members.push(userTokenLabel = new UIText(daX, userTokenBox.y - 24, 0, "User Token")); + userTokenBox.members.push(userTokenInfo = new UIButton(daX + userTokenLabel.width, userTokenLabel.y, "?", () -> { FlxG.state.openSubState(new GameJoltTokenInfoWindow()); }, 24, 24)); + userTokenInfo.color = 0xFF3737FF; + userTokenBox.label.textField.displayAsPassword = true; + + add(loginButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, "Login", function() { + if (GJUtil.attemptLogin(usernameBox.label.text, userTokenBox.label.text, true)) { + FlxG.state.openSubState(new GameJoltLoginSuccess()); + close(); + } else { + + } + }, 125)); + + add(closeButton = new UIButton(loginButton.x - 20 - loginButton.bWidth, loginButton.y, TU.translate("editor.cancel"), close, 125)); + closeButton.color = 0xFFFF0000; + } +} \ No newline at end of file diff --git a/source/funkin/menus/gamejolt/GameJoltMenu.hx b/source/funkin/menus/gamejolt/GameJoltMenu.hx new file mode 100644 index 0000000000..0969b2dc08 --- /dev/null +++ b/source/funkin/menus/gamejolt/GameJoltMenu.hx @@ -0,0 +1,47 @@ +package funkin.menus.gamejolt; + +import funkin.editors.ui.UIState; +import funkin.editors.ui.UIButton; +import funkin.editors.ui.UIText; +import funkin.menus.gamejolt.GameJoltLoginWindow; + +class GameJoltMenu extends UIState +{ + var mainGroup:FlxTypedSpriteGroup = new FlxTypedSpriteGroup(); + + override function create() + { + super.create(); + + add(new FlxSprite().loadAnimatedGraphic(Paths.image('menus/menuDesat'))); + + createPages(); + } + + function createPages() + { + var lgIn:Bool = GJUtil.loggedIn; + + mainGroup.add(new UIButton((FlxG.width / 2) - 60, FlxG.height - 200, "Login", () -> { + openSubState(new GameJoltLoginWindow()); + })); + + add(mainGroup); + + var daGroup:FlxTypedSpriteGroup = new FlxTypedSpriteGroup(); + + daGroup.add(new UIText(0, 0, FlxG.width, "NOT LOGGED IN")); + + add(daGroup); + } + + override function update(elapsed:Float) + { + super.update(elapsed); + + if (controls.BACK) { + CoolUtil.playMenuSFX(CANCEL, 0.7); + FlxG.switchState(new MainMenuState()); + } + } +} \ No newline at end of file diff --git a/source/funkin/menus/gamejolt/GameJoltTokenInfoWindow.hx b/source/funkin/menus/gamejolt/GameJoltTokenInfoWindow.hx new file mode 100644 index 0000000000..4751dc45d6 --- /dev/null +++ b/source/funkin/menus/gamejolt/GameJoltTokenInfoWindow.hx @@ -0,0 +1,32 @@ +package funkin.menus.gamejolt; + +import funkin.editors.ui.UISprite; +import funkin.editors.ui.UIButton; +import funkin.editors.ui.UIText; +import funkin.editors.ui.UISubstateWindow; + +class GameJoltTokenInfoWindow extends UISubstateWindow { + public var text:UIText; + public var closeButton:UIButton; + + public var daX:Float; + + public override function create() { + //TODO: get translations for text + winTitle = "GameJolt Token Info"; + + winWidth = 756; + winHeight = 220; + + super.create(); + + daX = windowSpr.x + 20; + + add(text = new UIText(daX, windowSpr.y + 46, windowSpr.width - ((daX - windowSpr.x) * 2), "!! THIS IS NOT YOUR GAMEJOLT ACCOUNT PASSWORD !!\n\nTo access your game token, click on your profile icon, then click on \"Game Token\".")); + + //add(new UISprite(daX, text.y + text.height + 10).loadGraphic(Paths.image())) + add(closeButton = new UIButton(windowSpr.x + (windowSpr.bWidth / 2) - 62, windowSpr.y + windowSpr.bHeight - 48, TU.translate("editor.saveClose"), function() { + close(); + }, 125)); + } +} \ No newline at end of file diff --git a/source/hscript/Config.hx b/source/hscript/Config.hx index 7c8e03f105..b1e53488a0 100644 --- a/source/hscript/Config.hx +++ b/source/hscript/Config.hx @@ -29,7 +29,7 @@ class Config { // Incase any of your files fail // These are the module names public static final DISALLOW_CUSTOM_CLASSES = [ - "funkin.backend.utils.GameJoltSecurity", // don't want people getting those gamejolt keys! + ]; public static final DISALLOW_ABSTRACT_AND_ENUM = [ @@ -38,6 +38,7 @@ class Config { @:unreflective public static final IMPORT_BLACKLIST:Array = [ - + "funkin.backend.system.gamejolt.GameJoltSecurity", // don't want people getting those gamejolt keys! + "funkin.backend.system.gamejolt.GameJoltData", // Global data items and really bad things to modify ]; }