diff --git a/.github/workflows/build-game-linux.yml b/.github/workflows/build-game-linux.yml index 5503213c..a821e7d8 100644 --- a/.github/workflows/build-game-linux.yml +++ b/.github/workflows/build-game-linux.yml @@ -27,6 +27,7 @@ jobs: haxelib install hmm --quiet haxelib run hmm install --quiet haxelib run lime rebuild hxcpp + haxelib run lime rebuild systools linux - name: Compile run: haxelib run lime build linux diff --git a/.github/workflows/build-game-macos.yml b/.github/workflows/build-game-macos.yml index 3f236aea..ef08f2fd 100644 --- a/.github/workflows/build-game-macos.yml +++ b/.github/workflows/build-game-macos.yml @@ -23,6 +23,7 @@ jobs: haxelib install hmm --quiet haxelib run hmm install --quiet haxelib run lime rebuild hxcpp + haxelib run lime rebuild systools mac - name: Compile run: haxelib run lime build mac - name: Upload Artifact diff --git a/.github/workflows/build-game-win32.yml b/.github/workflows/build-game-win32.yml index ced99ae9..5fa288b3 100644 --- a/.github/workflows/build-game-win32.yml +++ b/.github/workflows/build-game-win32.yml @@ -25,6 +25,7 @@ jobs: haxelib run hmm install --quiet haxelib run lime rebuild hxcpp haxelib run lime rebuild cpp + haxelib run lime rebuild systools windows - name: Compile run: haxelib run lime build windows -32 -release -D 32bits diff --git a/.github/workflows/build-game-windows-debug.yml b/.github/workflows/build-game-windows-debug.yml index a69556a0..f58dd23d 100644 --- a/.github/workflows/build-game-windows-debug.yml +++ b/.github/workflows/build-game-windows-debug.yml @@ -24,6 +24,7 @@ jobs: haxelib install hmm --quiet haxelib run hmm install --quiet haxelib run lime rebuild hxcpp + haxelib run lime rebuild systools windows - name: Compile run: haxelib run lime build windows -debug diff --git a/.github/workflows/build-game-windows.yml b/.github/workflows/build-game-windows.yml index c5200914..c44fc355 100644 --- a/.github/workflows/build-game-windows.yml +++ b/.github/workflows/build-game-windows.yml @@ -24,6 +24,7 @@ jobs: haxelib install hmm --quiet haxelib run hmm install --quiet haxelib run lime rebuild hxcpp + haxelib run lime rebuild systools windows - name: Compile run: haxelib run lime build windows diff --git a/.gitignore b/.gitignore index b297ec18..eb9ec339 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ libs/ lib/ .vs/ APIStuff.hx +GJKeys.hx dump/ export/ RECOVER_*.fla diff --git a/assets/preload/data/credits.json b/assets/preload/data/credits.json index e23fbe16..dfd53c1e 100644 --- a/assets/preload/data/credits.json +++ b/assets/preload/data/credits.json @@ -340,7 +340,7 @@ }, { "header": "Charles would like to specially thank", - "body": [{ "line": "JustKolosaki" }, { "line": "anysad" }] + "body": [{ "line": "JustKolosaki" }, { "line": "anysad" }, { "line": "GamerPablito" }] } ] } diff --git a/hmm.json b/hmm.json index c61a13d4..de90b560 100644 --- a/hmm.json +++ b/hmm.json @@ -200,6 +200,20 @@ "ref": null, "url": "https://github.com/MaybeMaru/moonchart" }, + { + "name": "tentools", + "type": "git", + "dir": null, + "ref": "6a8077182dfb949ed64e11075cda641687231379", + "url": "https://github.com/TentaRJ/tentools" + }, + { + "name": "systools", + "type": "git", + "dir": null, + "ref": "2055163b700afeaf8f566c32442148cdbc429e44", + "url": "https://github.com/haya3218/retools" + }, { "name": "openfl", "type": "git", diff --git a/project.hxp b/project.hxp index 2fb25bdc..7338dea0 100644 --- a/project.hxp +++ b/project.hxp @@ -696,6 +696,10 @@ class Project extends HXProject { addHaxelib('moonchart'); // Funny chart support + addHaxelib('tentools'); // idk + addHaxelib('systools'); // idk + addNdll('systools', 'systools'); // i had to add this function just for systools lol + if (isDebug()) { addHaxelib('hxcpp-debug-server'); // VSCode debug support } @@ -867,6 +871,17 @@ class Project extends HXProject { this.haxelibs.push(new Haxelib(name, version)); } + /** + * Add a NDLL to the project. + * NEEDED FOR SYSTOOLS!!!! + * @param name The name of the NDLL to add. + * @param version The name for the library that the NDLL uses. + */ + public function addNdll(name:String, haxelib:Null = null):Void { + this.ndlls.push(new NDLL(name, haxelib)); + } + + /** * Add a `haxeflag` to the project. */ diff --git a/source/Main.hx b/source/Main.hx index b754725c..3766eb6c 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -4,6 +4,9 @@ import flixel.FlxG; import flixel.FlxGame; import flixel.FlxState; import flixel.tweens.FlxTween; +#if systools +import funkin.api.gamejolt.GameJolt; +#end import funkin.Preferences; #if desktop import funkin.audio.ALSoftConfig; // Just to make sure DCE doesn't remove this, since it's not directly referenced anywhere else. @@ -59,6 +62,10 @@ class Main extends Sprite public static var lightMode:Bool = false; #end + #if systools + public static var gjToastManager:GJToastManager; + #end + // You can pretty much ignore everything from here on - your code should go in your states. // [ * -- INTERNAL VARIABLES - PLS DONT TOUCH THEM! -- * ] // @:dox(hide) @@ -170,6 +177,10 @@ class Main extends Sprite game._customSoundTray = funkin.ui.options.FunkinSoundTray; addChild(game); + #if systools + gjToastManager = new GJToastManager(); + addChild(gjToastManager); + #end #if FEATURE_DEBUG_FUNCTIONS game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil()); @@ -182,7 +193,6 @@ class Main extends Sprite addChild(fpsCounter); #end */ - #if hxcpp_debug_server trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.'); #else diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 260903c4..1dde566a 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -143,6 +143,14 @@ class InitState extends FlxState NGio.init(); #end + // + // GAMEJOLT API SETUP + // + #if systools + GameJoltAPI.connect(); + GameJoltAPI.authDaUser(Save.instance.gjUser, Save.instance.gjToken); + #end + // // DISCORD API SETUP // diff --git a/source/funkin/api/gamejolt/GameJolt.hx b/source/funkin/api/gamejolt/GameJolt.hx new file mode 100644 index 00000000..9afd9ab5 --- /dev/null +++ b/source/funkin/api/gamejolt/GameJolt.hx @@ -0,0 +1,632 @@ +package funkin.api.gamejolt; + +#if systools +// GameJolt things +import tentools.api.FlxGameJolt as GJApi; +// Login things +import flixel.FlxSprite; +import psych.ui.PsychUIInputText; +import flixel.graphics.FlxGraphic; +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.text.FlxText; +import flixel.ui.FlxButton; +import flixel.util.FlxColor; +import lime.system.System; +// Toast things +import flixel.FlxG; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.util.FlxTimer; +import openfl.Lib; +import openfl.display.Bitmap; +import openfl.display.BitmapData; +import openfl.display.Sprite; +import openfl.text.TextField; +import openfl.text.TextFormat; + +using StringTools; + +class GameJoltAPI // Connects to tentools.api.FlxGameJolt +{ + /** + * Inline variable to see if the user has logged in. + * True for logged in, false for not logged in. (Read Only!) + */ + public static var userLogin(default, null):Bool = false; // For User Login Achievement (Like IC) + + /** + * Inline variable to see if the user wants to submit scores. + */ + public static var leaderboardToggle:Bool; + + /** + * Grabs the username of the actual logged in user and returns it + */ + public static function getUser():String + return GJApi.username; + + /** + * Grabs the game token of the actual logged in user and returns it + */ + public static function getToken():String + return GJApi.usertoken; + + /** + * Sets the game API key from GJKeys.api + */ + public static function connect() + { + trace("Grabbing API keys..."); + + GJApi.init(Std.int(GJKeys.id), Std.string(GJKeys.key), function(data:Bool) { + #if debug + var daDesc:String = "If you are a developer, check GJKeys.hx\nMake sure the id and key are formatted correctly!"; + Main.gjToastManager.createToast(GameJoltInfo.imagePath, 'Game${!data ? " not" : ""} authenticated!', !data ? daDesc : "Success!"); + #end + }); + } + + /** + * Inline function to auth the user. Shouldn't be used outside of GameJoltAPI things. + * @param in1 username + * @param in2 token + * @param loginArg Used in only GameJoltLogin + */ + public static function authDaUser(in1:String, in2:String, ?loginArg:Bool = false) + { + if (!userLogin && in1 != "" && in2 != "") + { + GJApi.authUser(in1, in2, function(v:Bool) { + trace("User: " + in1); + trace("Token: " + in2); + + if (v) + { + Main.gjToastManager.createToast(GameJoltInfo.imagePath, 'SIGNED IN: ${in1.toUpperCase()}', "CONNECTED TO GAMEJOLT!"); + trace("User authenticated!"); + Save.instance.gjUser = in1; + Save.instance.gjToken = in2; + Save.instance.flush(); + userLogin = true; + startSession(); + } + else + { + Main.gjToastManager.createToast(GameJoltInfo.imagePath, "Not signed in!\nSign in to save your GameJolt trophies!", ""); + trace("User login failure!"); + // FlxG.switchState(new GameJoltLogin()); + } + + if (loginArg) FlxG.switchState(() -> new GameJoltLogin()); + }); + } + } + + /** + * Inline function to deauth the user, shouldn't be used out of GameJoltLogin state! + * @return Logs the user out and closes the game + */ + public static function deAuthDaUser() + { + closeSession(); + userLogin = false; + trace('User: ${Save.instance.gjUser} | Token: ${Save.instance.gjToken}'); + Save.instance.gjUser = ""; + Save.instance.gjToken = ""; + Save.instance.flush(); + trace("Logged out!"); + System.exit(0); + } + + /** + * Awards a trophy to the user! + * @param id Trophy ID. Check your game's API settings for trophy IDs. + */ + public static function getTrophy(id:Int) + { + if (userLogin) GJApi.addTrophy(id, + (data:Map) -> trace(!data.exists("message") ? data : 'Could not add Trophy [$id] : ${data.get("message")}')); + } + + /** + * Checks a trophy to see if it was collected + * @param id Trophy ID + * @return Bool (True for achieved, false for unachieved) + */ + public static function checkTrophy(id:Int):Bool + { + var value:Bool = false; + var trophy:Null> = pullTrophy(id); + + if (trophy != null) + { + value = trophy.get("achieved") == "true"; + trace('Trophy state [$id]: ${value ? "achieved" : "unachieved"}'); + } + + return value; + } + + /** + * Pulls a trophy info by passing its ID + * @param id Trophy ID + * @return Map or null if the process failed + */ + public static function pullTrophy(id:Int):Null> + { + var returnable:Map = []; + + GJApi.fetchTrophy(id, (data:Map) -> returnable = data); + if (returnable.exists("message")) + { + trace('Failed to pull trophy [$id] : ${returnable.get("message")}'); + return null; + } + return returnable; + } + + /** + * Add a score to a table! + * @param score Score of the song. **Can only be an int value!** + * @param tableID ID of the table you want to add the score to! + * @param extraData (Optional) You could put accuracy or any other details here! + */ + public static function addScore(score:Int, tableID:Int, ?extraData:String) + { + var retFormat:String = 'Score: $score'; + if (GameJoltAPI.leaderboardToggle) + { + trace("Trying to add a score"); + var formData:Null = extraData != null ? extraData.split(" ").join("%20") : null; + + if (formData != null) retFormat += '\nExtra Data: $formData'; + + GJApi.addScore(score + "%20Points", score, tableID, false, null, formData, function(data:Map) { + trace("Score submitted with a result of: " + data.get("success")); + Main.gjToastManager.createToast(GameJoltInfo.imagePath, "Score submitted!", retFormat, true); + }); + } + else + { + if (extraData != null) retFormat += '\nExtra Data: $extraData'; + + retFormat += "\nScore was not submitted due to score submitting being disabled!"; + Main.gjToastManager.createToast(GameJoltInfo.imagePath, "Score not submitted!", retFormat, true); + } + } + + /** + * Return the highest score from a table! + * + * Usable by pulling the data from the map by [function].get(); + * + * Values returned in the map: score, sort, user_id, user, extra_data, stored, guest, success + * + * @param id The table you want to pull from + * @return Map or null if not available + */ + public static function pullHighScore(id:Int):Null> + { + var returnable:Null>; + GJApi.fetchScore(id, 1, function(data:Map) { + if (!data.exists('message')) + { + trace('Could not pull High Score from Table [$id] :' + data.get('message')); + returnable = null; + } + else + { + trace(data); + returnable = data; + } + }); + return returnable; + } + + /** + * Inline function to start the session. Shouldn't be used out of GameJoltAPI + * Starts the session + */ + public static function startSession() + { + GJApi.openSession(function() { + trace("Session started!"); + new FlxTimer().start(20, tmr -> pingSession(), 0); + }); + } + + /** + * Tells GameJolt that you are still active! + * Called every 20 seconds by a loop in startSession(). + */ + public static function pingSession() + GJApi.pingSession(true, () -> trace("Ping!")); + + /** + * Closes the session, used for signing out + */ + public static function closeSession() + GJApi.closeSession(() -> trace('Closed out the session')); +} + +class GameJoltInfo +{ + /** + * Inline variable to change the font for the GameJolt API elements. **Example: `Paths.font("vcr.ttf");`** + * @param font You can change the font by doing **Paths.font([Name of your font file])** or by listing your file path. + * If *null*, will default to the normal font. + */ + public static var font:String = null; + + /** + * Inline variable to change the font for the notifications made by Firubii. + * + * Don't make it a NULL variable. Worst mistake of my life. + */ + public static var fontPath:String = "assets/fonts/vcr.ttf"; + + /** + * Image to show for notifications. Leave NULL for no image, it's all good :) + * + * Example: `Paths.getLibraryPath("images/stepmania-icon.png");` + */ + public static var imagePath:String = null; + + /* Other things that shouldn't be messed with are below this line! */ + /** + * GameJolt + FNF version. + */ + public static var version:String = "1.1"; + + /** + * Random quotes I got from other people. Nothing more, nothing less. Just for funny. + */ +} + +class GameJoltLogin extends MusicBeatState +{ + var bgImage:FlxGraphic = Paths.image('menuDesat'); + var usernameText:FlxText; + var tokenText:FlxText; + var usernameBox:PsychUIInputText; + var tokenBox:PsychUIInputText; + var signInBox:FlxButton; + var helpBox:FlxButton; + var leaderBox:FlxButton; + var logOutBox:FlxButton; + var cancelBox:FlxButton; + var username1:FlxText; + var username2:FlxText; + var baseX:Int = -275; + + public static var charBop:FlxSprite; + + override function create() + { + trace("gj api real? - " + Std.string(GJApi.initialized)); + FlxG.mouse.visible = true; + + var bg:FlxSprite = new FlxSprite().loadGraphic(bgImage); + bg.setGraphicSize(FlxG.width, FlxG.height); + bg.updateHitbox(); + bg.screenCenter(); + bg.scrollFactor.set(); + bg.antialiasing = true; + bg.alpha = 0.25; + add(bg); + + if (!GameJoltAPI.userLogin) + { + usernameText = new FlxText(0, 125, 300, "Username:", 14); + tokenText = new FlxText(0, 225, 300, "Token:", 14); + + usernameBox = new PsychUIInputText(0, 175, 100, null, 8); + tokenBox = new PsychUIInputText(0, 275, 200, null, 8); + tokenBox.passwordMode = true; + + signInBox = new FlxButton(0, 475, "Sign In", function() { + trace(usernameBox.text + " " + tokenBox.text); + GameJoltAPI.authDaUser(usernameBox.text, tokenBox.text, true); + }); + + helpBox = new FlxButton(0, 550, "How do I get my GameJolt token?", () -> openLink('https://www.youtube.com/watch?v=T5-x7kAGGnE')); + helpBox.color = FlxColor.fromRGB(84, 155, 149); + + add(usernameText); + add(usernameBox); + add(tokenText); + add(tokenBox); + add(signInBox); + add(helpBox); + } + else + { + username1 = new FlxText(0, 95, 0, "You're signed in to GameJolt as:", 32); + username1.alignment = CENTER; + username1.screenCenter(X); + username1.x += baseX; + + username2 = new FlxText(0, 145, 0, "" + GameJoltAPI.getUser() + "", 40); + username2.alignment = CENTER; + username2.screenCenter(X); + username2.x += baseX; + + logOutBox = new FlxButton(0, 550, "Logout & Close", () -> GameJoltAPI.deAuthDaUser()); + logOutBox.color = FlxColor.RED; // FlxColor.fromRGB(255, 134, 61); + + add(username1); + add(username2); + add(logOutBox); + } + + cancelBox = new FlxButton(0, 625, "Back", exit); + add(cancelBox); + + forEachOfType(FlxText, item -> setTextData(item)); + forEachOfType(PsychUIInputText, item -> setTextData(item)); + forEachOfType(FlxButton, function(item:FlxButton) { + item.screenCenter(X); + item.setGraphicSize(Std.int(item.width) * 3); + item.x += baseX; + }); + + super.create(); + } + + override function update(elapsed:Float) + { + super.update(elapsed); + + if (FlxG.keys.justPressed.ESCAPE) exit(); + } + + override function beatHit() + { + super.beatHit(); + } + + function setTextData(text:FlxText) + { + text.screenCenter(X); + text.x += baseX; + text.alignment = CENTER; + + if (GameJoltInfo.font != null) text.font = GameJoltInfo.font; + } + + function exit() + { + Save.instance.flush(); + FlxG.mouse.visible = false; + FlxG.switchState(() -> new funkin.ui.options.OptionsState()); + } + + function openLink(url:String) + { + #if linux + Sys.command('/usr/bin/xdg-open', [url, "&"]); + #else + FlxG.openURL(url); + #end + } +} + +/* + * The toast things, pulled from Hololive Funkin + * Thank you Firubii for the code for this! + * https://twitter.com/firubiii + * https://github.com/firubii + * ILYSM + */ +class GJToastManager extends Sprite +{ + public static var ENTER_TIME:Float = 0.5; + public static var DISPLAY_TIME:Float = 3.0; + public static var LEAVE_TIME:Float = 0.5; + public static var TOTAL_TIME:Float = ENTER_TIME + DISPLAY_TIME + LEAVE_TIME; + + var playTime:FlxTimer = new FlxTimer(); + + public function new() + { + super(); + FlxG.signals.postStateSwitch.add(onStateSwitch); + FlxG.signals.gameResized.add(onWindowResized); + } + + /** + * Create a toast! + * + * Usage: **Main.gjToastManager.createToast(iconPath, title, description);** + * @param iconPath Path for the image **Paths.getLibraryPath("image/example.png")** (Set to null if you won't add an image, it's all OK) + * @param title Title for the toast + * @param description Description for the toast + * @param sound Want to have an alert sound? Set this to **true**! Defaults to **false**. + */ + public function createToast(iconPath:Null, title:String, description:String, sound:Bool = false, color:String = '#3848CC'):Void + { + if (sound) FlxG.sound.play(Paths.sound('confirmMenu')); + + var toast = new Toast(iconPath, title, description, color); + addChild(toast); + + playTime.start(TOTAL_TIME); + playToasts(); + } + + public function playToasts():Void + { + for (i in 0...numChildren) + { + var child = getChildAt(i); + FlxTween.cancelTweensOf(child); + FlxTween.tween(child, {y: (numChildren - 1 - i) * child.height}, ENTER_TIME, + { + ease: FlxEase.quadOut, + onComplete: function(tween:FlxTween) { + FlxTween.cancelTweensOf(child); + FlxTween.tween(child, {y: (i + 1) * -child.height}, LEAVE_TIME, + { + ease: FlxEase.quadOut, + startDelay: DISPLAY_TIME, + onComplete: function(tween:FlxTween) { + cast(child, Toast).removeChildren(); + removeChild(child); + } + }); + } + }); + } + } + + public function collapseToasts():Void + { + for (i in 0...numChildren) + { + var child = getChildAt(i); + FlxTween.tween(child, {y: (i + 1) * -child.height}, LEAVE_TIME, + { + ease: FlxEase.quadOut, + onComplete: function(tween:FlxTween) { + cast(child, Toast).removeChildren(); + removeChild(child); + } + }); + } + } + + public function onStateSwitch():Void + { + if (!playTime.active) return; + + var elapsedSec = playTime.elapsedTime / 1000; + if (elapsedSec < ENTER_TIME) + { + for (i in 0...numChildren) + { + var child = getChildAt(i); + FlxTween.cancelTweensOf(child); + FlxTween.tween(child, {y: (numChildren - 1 - i) * child.height}, ENTER_TIME - elapsedSec, + { + ease: FlxEase.quadOut, + onComplete: function(tween:FlxTween) { + FlxTween.cancelTweensOf(child); + FlxTween.tween(child, {y: (i + 1) * -child.height}, LEAVE_TIME, + { + ease: FlxEase.quadOut, + startDelay: DISPLAY_TIME, + onComplete: function(tween:FlxTween) { + cast(child, Toast).removeChildren(); + removeChild(child); + } + }); + } + }); + } + } + else if (elapsedSec < DISPLAY_TIME) + { + for (i in 0...numChildren) + { + var child = getChildAt(i); + FlxTween.cancelTweensOf(child); + FlxTween.tween(child, {y: (i + 1) * -child.height}, LEAVE_TIME, + { + ease: FlxEase.quadOut, + startDelay: DISPLAY_TIME - (elapsedSec - ENTER_TIME), + onComplete: function(tween:FlxTween) { + cast(child, Toast).removeChildren(); + removeChild(child); + } + }); + } + } + else if (elapsedSec < LEAVE_TIME) + { + for (i in 0...numChildren) + { + var child = getChildAt(i); + FlxTween.tween(child, {y: (i + 1) * -child.height}, LEAVE_TIME - (elapsedSec - ENTER_TIME - DISPLAY_TIME), + { + ease: FlxEase.quadOut, + onComplete: function(tween:FlxTween) { + cast(child, Toast).removeChildren(); + removeChild(child); + } + }); + } + } + } + + public function onWindowResized(x:Int, y:Int):Void + { + for (i in 0...numChildren) + { + var child = getChildAt(i); + child.x = Lib.current.stage.stageWidth - child.width; + } + } +} + +class Toast extends Sprite +{ + var back:Bitmap; + var icon:Bitmap; + var title:TextField; + var desc:TextField; + + public function new(iconPath:Null, titleText:String, description:String, color:String = '#3848CC') + { + super(); + back = new Bitmap(new BitmapData(500, 125, true, 0xFF000000)); + back.alpha = 0.9; + back.x = back.y = 0; + + if (iconPath != null) + { + var iconBmp = FlxG.bitmap.add(Paths.image(iconPath)); + iconBmp.persist = true; + icon = new Bitmap(iconBmp.bitmap); + icon.width = 100; + icon.height = 100; + icon.x = 10; + icon.y = 10; + } + + title = new TextField(); + title.text = titleText.toUpperCase(); + title.setTextFormat(new TextFormat(openfl.utils.Assets.getFont(GameJoltInfo.fontPath).fontName, 30, FlxColor.fromString(color), true)); + title.wordWrap = true; + title.width = 360; + title.x = iconPath != null ? 120 : 5; + title.y = 5; + + desc = new TextField(); + desc.text = description.toUpperCase(); + desc.setTextFormat(new TextFormat(openfl.utils.Assets.getFont(GameJoltInfo.fontPath).fontName, 24, FlxColor.WHITE)); + desc.wordWrap = true; + desc.width = 360; + desc.height = 95; + desc.x = iconPath != null ? 120 : 5; + desc.y = 35; + + if (titleText.length >= 25 || titleText.contains("\n")) + { + desc.y += 25; + desc.height -= 25; + } + + addChild(back); + + if (iconPath != null) addChild(icon); + + addChild(title); + addChild(desc); + + width = back.width; + height = back.height; + x = Lib.current.stage.stageWidth - width; + y = -height; + } +} +#end diff --git a/source/funkin/import.hx b/source/funkin/import.hx index b7468ec6..e021443a 100644 --- a/source/funkin/import.hx +++ b/source/funkin/import.hx @@ -5,12 +5,19 @@ package; import funkin.util.Constants; import funkin.Paths; import funkin.Preferences; +import funkin.ui.MusicBeatState; +import funkin.ui.MusicBeatSubState; import flixel.FlxG; // This one in particular causes a compile error if you're using macros. import flixel.system.debug.watch.Tracker; #if FEATURE_DISCORD_RPC import funkin.api.discord.DiscordClient; // IDK, I'm just tired of putting this in a lot of states... - #end +// For GameJolt, LOL. +#if systools +import funkin.api.gamejolt.GameJolt; +import funkin.api.gamejolt.GameJolt.GameJoltAPI; +#end + // These are great. using Lambda; using StringTools; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index f7d3923d..5256056c 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -3423,7 +3423,9 @@ class PlayState extends MusicBeatSubState { if (currentSong.validScore) { - NGio.unlockMedal(60961); + if (PlayStatePlaylist.campaignId == "xweek1") GameJoltAPI.getTrophy(249308); + else if (PlayStatePlaylist.campaignId == "xweek2") GameJoltAPI.getTrophy(249304); + else if (PlayStatePlaylist.campaignId == "xweek3") GameJoltAPI.getTrophy(249305); var data = { diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 6baad814..32fc3791 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -80,6 +80,13 @@ class Save { sessionId: null, } + #if systools + gamejolt: + { + username: null, + token: null, + } + #end }, scores: { @@ -253,6 +260,42 @@ class Save return data.api.newgrounds.sessionId; } + #if systools + /** + * The current username for the logged-in GameJolt user, or null if the user is cringe. + */ + public var gjUser(get, set):Null; + + function get_gjUser():Null + { + return data.api.gamejolt.username; + } + + function set_gjUser(value:Null):Null + { + return data.api.gamejolt.username = value; + flush(); + return data.api.gamejolt.username; + } + + /** + * The current token for the logged-in GameJolt user, or null if the user is cringe. + */ + public var gjToken(get, set):Null; + + function get_gjToken():Null + { + return data.api.gamejolt.token; + } + + function set_gjToken(value:Null):Null + { + return data.api.gamejolt.token = value; + flush(); + return data.api.gamejolt.token; + } + #end + public var enabledModIds(get, set):Array; function get_enabledModIds():Array @@ -1025,7 +1068,6 @@ class Save trace('[SAVE] YOOOOO WE GOT SAVE DATA - SLOT ${slot}!!!!!'); var gameSave = SaveDataMigrator.migrate(FlxG.save.data); FlxG.save.mergeData(gameSave.data, true); - return gameSave; } } @@ -1236,6 +1278,9 @@ typedef RawSaveData = typedef SaveApiData = { var newgrounds:SaveApiNewgroundsData; + #if systools + var gamejolt:SaveApiGamejoltData; + #end } typedef SaveApiNewgroundsData = @@ -1243,6 +1288,14 @@ typedef SaveApiNewgroundsData = var sessionId:Null; } +#if systools +typedef SaveApiGamejoltData = +{ + var username:Null; + var token:Null; +} +#end + typedef SaveDataUnlocks = { /** diff --git a/source/funkin/ui/options/OptionsState.hx b/source/funkin/ui/options/OptionsState.hx index 36179cf2..a7afec1e 100644 --- a/source/funkin/ui/options/OptionsState.hx +++ b/source/funkin/ui/options/OptionsState.hx @@ -259,10 +259,8 @@ class OptionsMenu extends Page createItem("INPUT OFFSETS", () -> FlxG.state.openSubState(new LatencyState())); #end - #if newgrounds - if (NGio.isLoggedIn) createItem("LOGOUT", selectLogout); - else - createItem("LOGIN", selectLogin); + #if systools + createItem("GAMEJOLT", gamejoltLogin); #end createItem("EXIT", exit); } @@ -290,6 +288,11 @@ class OptionsMenu extends Page return items.length > 2; } + #if systools + function gamejoltLogin() + FlxG.switchState(() -> new GameJoltLogin()); + #end + #if newgrounds function selectLogin() { diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index 95f51f16..e3028556 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -450,9 +450,9 @@ class TitleState extends MusicBeatState { if (FlxG.sound.music != null) FlxG.sound.music.onComplete = null; // netStream.play(Paths.file('music/kickstarterTrailer.mp4')); - NGio.unlockMedal(60960); + GameJoltAPI.getTrophy(249319); // If it's Friday according to da clock - if (Date.now().getDay() == 5) NGio.unlockMedal(61034); + if (Date.now().getDay() == 5) GameJoltAPI.getTrophy(249307); if (Preferences.flashingLights) { titleText.animation.play('press'); diff --git a/source/psych/ui/PsychUIEventHandler.hx b/source/psych/ui/PsychUIEventHandler.hx new file mode 100644 index 00000000..b22e213a --- /dev/null +++ b/source/psych/ui/PsychUIEventHandler.hx @@ -0,0 +1,20 @@ +package psych.ui; + +class PsychUIEventHandler +{ + public static function event(id:String, sender:Dynamic) + { + var state:Dynamic = cast FlxG.state; + if (state == null) return; + + while (state.subState != null) + state = cast state.subState; + + if (state != null && state.UIEvent != null) state.UIEvent(id, sender); + } +} + +interface PsychUIEvent +{ + public function UIEvent(id:String, sender:Dynamic):Void; +} diff --git a/source/psych/ui/PsychUIInputText.hx b/source/psych/ui/PsychUIInputText.hx new file mode 100644 index 00000000..e9fc548a --- /dev/null +++ b/source/psych/ui/PsychUIInputText.hx @@ -0,0 +1,741 @@ +package psych.ui; + +import flixel.FlxObject; +import flixel.input.keyboard.FlxKey; +import flixel.util.FlxDestroyUtil; +import flash.events.KeyboardEvent; +import lime.system.Clipboard; + +/** + * had to take this from psych for now until i fix flixel-ui lol + * + * @see https://github.com/ShadowMario/FNF-PsychEngine/blob/main/source/backend/ui/PsychUIInputText.hx + */ +enum abstract AccentCode(Int) from Int from UInt to Int to UInt +{ + var NONE = -1; + var GRAVE = 0; + var ACUTE = 1; + var CIRCUMFLEX = 2; + var TILDE = 3; +} + +enum abstract FilterMode(Int) from Int from UInt to Int to UInt +{ + var NO_FILTER:Int = 0; + var ONLY_ALPHA:Int = 1; + var ONLY_NUMERIC:Int = 2; + var ONLY_ALPHANUMERIC:Int = 3; + var ONLY_HEXADECIMAL:Int = 4; + var CUSTOM_FILTER:Int = 5; +} + +enum abstract CaseMode(Int) from Int from UInt to Int to UInt +{ + var ALL_CASES:Int = 0; + var UPPER_CASE:Int = 1; + var LOWER_CASE:Int = 2; +} + +class PsychUIInputText extends FlxSpriteGroup +{ + public static final CHANGE_EVENT = "inputtext_change"; + + static final KEY_TILDE = 126; + static final KEY_ACUTE = 180; + + public static var focusOn(default, set):PsychUIInputText = null; + + public var name:String; + public var bg:FlxSprite; + public var behindText:FlxSprite; + public var selection:FlxSprite; + public var textObj:FlxText; + public var caret:FlxSprite; + public var onChange:String->String->Void; + + public var fieldWidth(default, set):Int = 0; + public var maxLength(default, set):Int = 0; + public var passwordMask(default, set):Bool = false; + public var text(default, set):String = null; + + public var forceCase(default, set):CaseMode = ALL_CASES; + public var filterMode(default, set):FilterMode = NO_FILTER; + public var customFilterPattern(default, set):EReg; + + public var selectedFormat:FlxTextFormat = new FlxTextFormat(FlxColor.WHITE); + + public function new(x:Float = 0, y:Float = 0, wid:Int = 100, ?text:String = '', size:Int = 8) + { + super(x, y); + this.bg = new FlxSprite().makeGraphic(1, 1, FlxColor.BLACK); + this.behindText = new FlxSprite(1, 1).makeGraphic(1, 1, FlxColor.WHITE); + this.selection = new FlxSprite().makeGraphic(1, 1, FlxColor.WHITE); + this.textObj = new FlxText(1, 1, Math.max(1, wid - 2), '', size); + this.caret = new FlxSprite().makeGraphic(1, 1, FlxColor.WHITE); + add(this.bg); + add(this.behindText); + add(this.selection); + add(this.textObj); + add(this.caret); + + this.textObj.color = FlxColor.BLACK; + this.textObj.textField.selectable = false; + this.textObj.textField.wordWrap = false; + this.textObj.textField.multiline = false; + this.selection.color = FlxColor.BLUE; + + @:bypassAccessor fieldWidth = wid; + setGraphicSize(wid + 2, this.textObj.height + 2); + updateHitbox(); + this.text = text; + + FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); + } + + public var selectIndex:Int = -1; + public var caretIndex(default, set):Int = -1; + + var _caretTime:Float = 0; + + var _nextAccent:AccentCode = NONE; + + public var inInsertMode:Bool = false; + + function onKeyDown(e:KeyboardEvent) + { + if (focusOn != this) return; + + var keyCode:Int = e.keyCode; + var charCode:Int = e.charCode; + var flxKey:FlxKey = cast keyCode; + + // Fix missing cedilla + switch (keyCode) + { + case 231: // ç and Ç + charCode = e.shiftKey ? 0xC7 : 0xE7; + } + + // Control key actions + if (e.controlKey) + { + switch (flxKey) + { + case A: // select all text + selectIndex = Std.int(Math.min(0, text.length - 1)); + caretIndex = text.length; + + case X, C: // cut/copy selected text to clipboard + if (caretIndex >= 0 && selectIndex != 0 && caretIndex != selectIndex) + { + Clipboard.text = text.substring(caretIndex, selectIndex); + if (flxKey == X) deleteSelection(); + } + + case V: // paste from clipboard + if (Clipboard.text == null) return; + + if (selectIndex > -1 && selectIndex != caretIndex) deleteSelection(); + + var lastText = text; + text = text.substring(0, caretIndex) + Clipboard.text + text.substring(caretIndex); + caretIndex += Clipboard.text.length; + if (onChange != null) onChange(lastText, text); + if (broadcastInputTextEvent) PsychUIEventHandler.event(CHANGE_EVENT, this); + + case BACKSPACE: + if (selectIndex < 0 || selectIndex == caretIndex) + { + var lastText = text; + var deletedText:String = text.substr(0, Std.int(Math.max(0, caretIndex - 1))); + var space:Int = deletedText.lastIndexOf(' '); + if (space > -1 && space != caretIndex - 1) + { + var start:String = deletedText.substring(0, space + 1); + var end:String = text.substring(caretIndex); + caretIndex -= Std.int(Math.max(0, text.length - (start.length + end.length))); + text = start + end; + } + else + { + text = text.substring(caretIndex); + caretIndex = 0; + } + selectIndex = -1; + if (onChange != null) onChange(lastText, text); + if (broadcastInputTextEvent) PsychUIEventHandler.event(CHANGE_EVENT, this); + } + else + deleteSelection(); + + case DELETE: + if (selectIndex < 0 || selectIndex == caretIndex) + { + // This is| a test + // This is test + var deletedText:String = text.substring(caretIndex); + var spc:Int = 0; + var space:Int = deletedText.indexOf(' '); + while (deletedText.substr(spc, 1) == ' ') + { + spc++; + space = deletedText.substr(spc).indexOf(' '); + } + + var lastText = text; + if (space > -1) + { + text = text.substr(0, caretIndex) + text.substring(caretIndex + space + spc); + } + else + text = text.substr(0, caretIndex); + if (onChange != null) onChange(lastText, text); + if (broadcastInputTextEvent) PsychUIEventHandler.event(CHANGE_EVENT, this); + } + else + deleteSelection(); + + case LEFT: + if (caretIndex > 0) + { + do + { + caretIndex--; + var a:String = text.substr(caretIndex - 1, 1); + var b:String = text.substr(caretIndex, 1); + // trace(a, b); + if (a == ' ' && b != ' ') break; + } + while (caretIndex > 0); + } + + case RIGHT: + if (caretIndex < text.length) + { + do + { + caretIndex++; + var a:String = text.substr(caretIndex - 1, 1); + var b:String = text.substr(caretIndex, 1); + // trace(a, b); + if (a != ' ' && b == ' ') break; + } + while (caretIndex < text.length); + } + + default: + } + updateCaret(); + return; + } + + static final ignored:Array = [SHIFT, CONTROL, ESCAPE]; + if (ignored.contains(flxKey)) return; + + var lastAccent = _nextAccent; + switch (keyCode) + { + case KEY_TILDE: + _nextAccent = !e.shiftKey ? TILDE : CIRCUMFLEX; + if (lastAccent == NONE) return; + case KEY_ACUTE: + _nextAccent = !e.shiftKey ? ACUTE : GRAVE; + if (lastAccent == NONE) return; + default: + lastAccent = NONE; + } + + // trace(keyCode, charCode, flxKey); + switch (flxKey) + { + case LEFT: // move caret to left + if (!FlxG.keys.pressed.SHIFT) selectIndex = -1; + else if (selectIndex == -1) selectIndex = caretIndex; + caretIndex = Std.int(Math.max(0, caretIndex - 1)); + + case RIGHT: // move caret to right + if (!FlxG.keys.pressed.SHIFT) selectIndex = -1; + else if (selectIndex == -1) selectIndex = caretIndex; + caretIndex = Std.int(Math.min(text.length, caretIndex + 1)); + + case HOME: // move caret to the begin + if (!FlxG.keys.pressed.SHIFT) selectIndex = -1; + else if (selectIndex == -1) selectIndex = caretIndex; + caretIndex = 0; + + case END: // move caret to the end + if (!FlxG.keys.pressed.SHIFT) selectIndex = -1; + else if (selectIndex == -1) selectIndex = caretIndex; + caretIndex = text.length; + + case INSERT: // change to insert mode + inInsertMode = !inInsertMode; + + case BACKSPACE: // Delete letter to the left of caret + if (caretIndex <= 0) return; + + if (selectIndex > -1 && selectIndex != caretIndex) deleteSelection(); + else + { + var lastText = text; + text = text.substring(0, caretIndex - 1) + text.substring(caretIndex); + caretIndex--; + if (onChange != null) onChange(lastText, text); + if (broadcastInputTextEvent) PsychUIEventHandler.event(CHANGE_EVENT, this); + } + _nextAccent = NONE; + + case DELETE: // Delete letter to the right of caret + if (selectIndex > -1 && selectIndex != caretIndex) + { + deleteSelection(); + updateCaret(); + return; + } + + if (caretIndex >= text.length) return; + + var lastText = text; + if (caretIndex < 1) text = text.substr(1); + else + text = text.substring(0, caretIndex) + text.substring(caretIndex + 1); + + if (caretIndex >= text.length) caretIndex = text.length; + + if (onChange != null) onChange(lastText, text); + if (broadcastInputTextEvent) PsychUIEventHandler.event(CHANGE_EVENT, this); + + case SPACE: // space or last accent pressed + if (_nextAccent != NONE) _typeLetter(getAccentCharCode(_nextAccent)); + else + _typeLetter(charCode); + _nextAccent = NONE; + + case A, O: // these support all accents + var grave:Int = 0x0; + var capital:Int = 0x0; + switch (flxKey) + { + case A: + grave = 0xC0; + capital = 0x41; + case O: + grave = 0xD2; + capital = 0x4f; + default: + } + if (_nextAccent != NONE) charCode += grave - capital + _nextAccent; + + _typeLetter(charCode); + _nextAccent = NONE; + + case E, I, U: // these support grave, acute and circumflex + var grave:Int = 0x0; + var capital:Int = 0x0; + switch (flxKey) + { + case E: + grave = 0xC8; + capital = 0x45; + case I: + grave = 0xCC; + capital = 0x49; + case U: + grave = 0xD9; + capital = 0x55; + default: + } + if (_nextAccent == GRAVE || _nextAccent == ACUTE || _nextAccent == CIRCUMFLEX) // Supported accents + charCode += grave - capital + _nextAccent; + else if (_nextAccent == TILDE) // Unsupported accent + _typeLetter(getAccentCharCode(_nextAccent)); + + _typeLetter(charCode); + _nextAccent = NONE; + + case N: // it only supports tilde + if (_nextAccent == TILDE) charCode += 0xD1 - 0x4E; + else + _typeLetter(getAccentCharCode(_nextAccent)); + + _typeLetter(charCode); + _nextAccent = NONE; + + case ESCAPE: + focusOn = null; + + case ENTER: + onPressEnter(e); + + default: + if (charCode < 1) if ((charCode = getAccentCharCode(_nextAccent)) < 1) return; + + if (lastAccent != NONE) _typeLetter(getAccentCharCode(lastAccent)); + else if (_nextAccent != NONE) _typeLetter(getAccentCharCode(_nextAccent)); + _typeLetter(charCode); + _nextAccent = NONE; + } + updateCaret(); + } + + public dynamic function onPressEnter(e:KeyboardEvent) + focusOn = null; + + public var unfocus:Void->Void; + + public static function set_focusOn(v:PsychUIInputText) + { + if (focusOn != null && focusOn != v && focusOn.exists) + { + if (focusOn.unfocus != null) focusOn.unfocus(); + focusOn.resetCaret(); + } + return (focusOn = v); + } + + override function update(elapsed:Float) + { + super.update(elapsed); + + if (FlxG.mouse.justPressed) + { + if (FlxG.mouse.overlaps(behindText, camera)) + { + if (!FlxG.keys.pressed.SHIFT) selectIndex = -1; + else if (selectIndex == -1) selectIndex = caretIndex; + focusOn = this; + caretIndex = 0; + var lastBound:Float = 0; + var textObjX:Float = textObj.getScreenPosition(camera).x; + var mousePosX:Float = FlxG.mouse.getScreenPosition(camera).x; + var txtX:Float = textObjX - textObj.textField.scrollH; + + for (i => bound in _boundaries) + { + if (mousePosX >= txtX + (bound - lastBound) / 2) + { + caretIndex = i + 1; + txtX += bound - lastBound; + lastBound = bound; + } + else + break; + } + updateCaret(); + } + else if (focusOn == this) focusOn = null; + + // trace('changed focus to: ' + this); + } + + if (focusOn == this) + { + _caretTime = (_caretTime + elapsed) % 1; + if (textObj != null && textObj.exists) + { + var drewSelection:Bool = false; + if (selection != null && selection.exists) + { + if (selectIndex != -1 && selectIndex != caretIndex) + { + selection.visible = true; + drewSelection = true; + } + else + selection.visible = false; + } + + if (caret != null && caret.exists) + { + if (!drewSelection && _caretTime < 0.5 && caret.x >= textObj.x) + { + caret.visible = true; + caret.color = textObj.color; + } + else + caret.visible = false; + } + } + } + else + { + _caretTime = 0; + inInsertMode = false; + if (selection != null && selection.exists) selection.visible = false; + if (caret != null && caret.exists) caret.visible = false; + } + } + + public function resetCaret() + { + selectIndex = -1; + caretIndex = 0; + updateCaret(); + } + + public function updateCaret() + { + if (textObj == null || !textObj.exists) return; + + var textField = textObj.textField; + textField.setSelection(caretIndex, caretIndex); + _caretTime = 0; + if (caret != null && caret.exists) + { + caret.y = textObj.y + 2; + caret.x = textObj.x + 1 - textObj.textField.scrollH; + if (caretIndex > 0) caret.x += _boundaries[Std.int(Math.max(0, Math.min(_boundaries.length - 1, caretIndex - 1)))]; + } + + if (selection != null && selection.exists) + { + selection.y = textObj.y + 2; + selection.x = textObj.x + 1 - textObj.textField.scrollH; + if (selectIndex > 0) selection.x += _boundaries[Std.int(Math.max(0, Math.min(_boundaries.length - 1, selectIndex - 1)))]; + + selection.scale.y = textField.textHeight; + selection.scale.x = caret.x - selection.x; + if (selection.scale.x < 0) + { + selection.scale.x = Math.abs(selection.scale.x); + selection.x -= selection.scale.x; + } + + if (selection.x < textObj.x) + { + var diff:Float = textObj.x - selection.x; + selection.x += diff; + selection.scale.x -= diff; + } + if (selection.x + selection.scale.x > textObj.x + textObj.width) selection.scale.x += (textObj.x + textObj.width - selection.x - selection.scale.x); + + selection.updateHitbox(); + + if (text.length > 0) + { + textObj.removeFormat(selectedFormat); + if (selectIndex != -1 && selectIndex != caretIndex) + { + textObj.addFormat(selectedFormat, caretIndex < selectIndex ? caretIndex : selectIndex, caretIndex < selectIndex ? selectIndex : caretIndex); + } + } + } + else if (text.length > 0) textObj.removeFormat(selectedFormat); + } + + function deleteSelection() + { + var lastText:String = text; + if (selectIndex > caretIndex) + { + text = text.substring(0, caretIndex) + text.substring(selectIndex); + } + else + { + text = text.substring(0, selectIndex) + text.substring(caretIndex); + caretIndex = selectIndex; + } + selectIndex = -1; + if (onChange != null) onChange(lastText, text); + if (broadcastInputTextEvent) PsychUIEventHandler.event(CHANGE_EVENT, this); + } + + override public function destroy() + { + _boundaries = null; + if (focusOn == this) focusOn = null; + FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); + super.destroy(); + } + + function set_caretIndex(v:Int) + { + caretIndex = v; + updateCaret(); + return v; + } + + override public function setGraphicSize(width:Float = 0, height:Float = 0) + { + super.setGraphicSize(width, height); + bg.setGraphicSize(width, height); + behindText.setGraphicSize(width - 2, height - 2); + if (textObj != null && textObj.exists) + { + textObj.scale.x = 1; + textObj.scale.y = 1; + if (caret != null && caret.exists) caret.setGraphicSize(1, textObj.height - 4); + } + } + + override public function updateHitbox() + { + super.updateHitbox(); + bg.updateHitbox(); + behindText.updateHitbox(); + if (textObj != null && textObj.exists) + { + textObj.updateHitbox(); + if (caret != null && caret.exists) caret.updateHitbox(); + } + } + + function set_fieldWidth(v:Int) + { + textObj.fieldWidth = Math.max(1, v - 2); + textObj.textField.selectable = false; + textObj.textField.wordWrap = false; + textObj.textField.multiline = false; + return (fieldWidth = v); + } + + function set_maxLength(v:Int) + { + var lastText = text; + v = Std.int(Math.max(0, v)); + if (v > 0 && text.length > v) text = text.substr(0, v); + if (onChange != null) onChange(lastText, text); + if (broadcastInputTextEvent) PsychUIEventHandler.event(CHANGE_EVENT, this); + return (maxLength = v); + } + + function set_passwordMask(v:Bool) + { + passwordMask = v; + text = text; + return passwordMask; + } + + var _boundaries:Array = []; + + function set_text(v:String) + { + for (i in 0..._boundaries.length) + _boundaries.pop(); + v = filter(v); + + textObj.text = ''; + if (v != null && v.length > 0) + { + if (v.length > 1) + { + for (i in 0...v.length) + { + var toPrint:String = v.substr(i, 1); + if (toPrint == '\n') toPrint = ' '; + textObj.textField.appendText(!passwordMask ? toPrint : '*'); + _boundaries.push(textObj.textField.textWidth); + } + } + else + { + textObj.text = !passwordMask ? v : '*'; + _boundaries.push(textObj.textField.textWidth); + } + } + text = v; + updateCaret(); + return v; + } + + public static function getAccentCharCode(accent:AccentCode) + { + switch (accent) + { + case TILDE: + return 0x7E; + case CIRCUMFLEX: + return 0x5E; + case ACUTE: + return 0xB4; + case GRAVE: + return 0x60; + default: + return 0x0; + } + } + + public var broadcastInputTextEvent:Bool = true; + + function _typeLetter(charCode:Int) + { + if (charCode < 1) return; + + if (selectIndex > -1 && selectIndex != caretIndex) deleteSelection(); + + var letter:String = String.fromCharCode(charCode); + letter = filter(letter); + if (letter.length > 0 && (maxLength == 0 || (text.length + letter.length) <= maxLength)) + { + var lastText = text; + // trace('Drawing character: $letter'); + if (!inInsertMode) text = text.substring(0, caretIndex) + letter + text.substring(caretIndex); + else + text = text.substring(0, caretIndex) + letter + text.substring(caretIndex + 1); + + caretIndex += letter.length; + if (onChange != null) onChange(lastText, text); + if (broadcastInputTextEvent) PsychUIEventHandler.event(CHANGE_EVENT, this); + } + _caretTime = 0; + } + + // from FlxInputText + function set_forceCase(v:CaseMode) + { + forceCase = v; + text = filter(text); + return forceCase; + } + + function set_filterMode(v:FilterMode) + { + filterMode = v; + text = filter(text); + return filterMode; + } + + function set_customFilterPattern(cfp:EReg) + { + customFilterPattern = cfp; + filterMode = CUSTOM_FILTER; + return customFilterPattern; + } + + private function filter(text:String):String + { + switch (forceCase) + { + case UPPER_CASE: + text = text.toUpperCase(); + case LOWER_CASE: + text = text.toLowerCase(); + default: + } + if (forceCase == UPPER_CASE) text = text.toUpperCase(); + else if (forceCase == LOWER_CASE) text = text.toLowerCase(); + + if (filterMode != NO_FILTER) + { + var pattern:EReg; + switch (filterMode) + { + case ONLY_ALPHA: + pattern = ~/[^a-zA-Z]*/g; + case ONLY_NUMERIC: + pattern = ~/[^0-9]*/g; + case ONLY_ALPHANUMERIC: + pattern = ~/[^a-zA-Z0-9]*/g; + case ONLY_HEXADECIMAL: + pattern = ~/[^a-fA-F0-9]*/g; + case CUSTOM_FILTER: + pattern = customFilterPattern; + default: + throw new flash.errors.Error("FlxInputText: Unknown filterMode (" + filterMode + ")"); + } + text = pattern.replace(text, ""); + } + return text; + } +}