From 80e7539debc3abd9d117822e961f2abd56e84a8d Mon Sep 17 00:00:00 2001 From: George FunBook <gkurelic@gmail.com> Date: Thu, 18 Feb 2021 13:58:16 -0600 Subject: [PATCH] add prompt, finalize login UI --- Project.xml | 8 +- source/MainMenuState.hx | 166 ++++++++++++++++++++++++++++++++++++-- source/NGio.hx | 156 +++++++++++++++++++++++++---------- source/TitleState.hx | 42 +++++----- source/ui/MenuItemList.hx | 164 +++++++++++++++++++++++++++---------- source/ui/Prompt.hx | 120 +++++++++++++++++++++++++++ 6 files changed, 538 insertions(+), 118 deletions(-) create mode 100644 source/ui/Prompt.hx diff --git a/Project.xml b/Project.xml index f943faede..bb9ef84de 100644 --- a/Project.xml +++ b/Project.xml @@ -166,6 +166,12 @@ <!-- <haxedef name="SKIP_TO_PLAYSTATE" if="debug" /> --> - <haxedef name="NG_LOGIN" if="newgrounds" /> + + <!-- Enables Ng.core.verbose --> + <!-- <haxedef name="NG_VERBOSE" /> --> + <!-- Enables a NG debug session, so medals don't permently unlock --> + <!-- <haxedef name="NG_DEBUG" /> --> + <!-- pretends that the saved session Id was expired, forcing the reconnect prompt --> + <!-- <haxedef name="NG_FORCE_EXPIRED_SESSION" if="debug" /> --> </project> diff --git a/source/MainMenuState.hx b/source/MainMenuState.hx index a2d484650..db00ce94d 100644 --- a/source/MainMenuState.hx +++ b/source/MainMenuState.hx @@ -1,5 +1,7 @@ package; +import NGio; +import flixel.ui.FlxButton; import flixel.FlxG; import flixel.FlxObject; import flixel.FlxSprite; @@ -17,12 +19,13 @@ import io.newgrounds.NG; import lime.app.Application; import ui.MenuItemList; +import ui.Prompt; using StringTools; class MainMenuState extends MusicBeatState { - var menuItems:MenuItemList; + var menuItems:MainMenuItemList; var magenta:FlxSprite; var camFollow:FlxObject; @@ -63,7 +66,7 @@ class MainMenuState extends MusicBeatState add(magenta); // magenta.scrollFactor.set(); - menuItems = new MenuItemList('FNF_main_menu_assets'); + menuItems = new MainMenuItemList('FNF_main_menu_assets'); add(menuItems); menuItems.onChange.add(onMenuItemChange); menuItems.onAcceptPress.add(function(_) @@ -74,17 +77,18 @@ class MainMenuState extends MusicBeatState var hasPopupBlocker = #if web true #else false #end; - menuItems.addItem('story mode', function () startExitState(new StoryMenuState())); - menuItems.addItem('freeplay', function () startExitState(new FreeplayState())); + menuItems.enabled = false;// disable for intro + menuItems.createItem('story mode', function () startExitState(new StoryMenuState())); + menuItems.createItem('freeplay', function () startExitState(new FreeplayState())); // addMenuItem('options', function () startExitState(new OptionMenu())); #if (!switch) - menuItems.addItem('donate', selectDonate, hasPopupBlocker); + menuItems.createItem('donate', selectDonate, hasPopupBlocker); #end #if newgrounds if (NG.core.loggedIn) - menuItems.addItem("logout", selectLogout); + menuItems.createItem("logout", selectLogout); else - menuItems.addItem("login", selectLogin, hasPopupBlocker); + menuItems.createItem("login", selectLogin); #end // center vertically @@ -109,6 +113,16 @@ class MainMenuState extends MusicBeatState super.create(); } + override function finishTransIn() + { + super.finishTransIn(); + + menuItems.enabled = true; + + if (NGio.savedSessionFailed) + showSavedSessionFailed(); + } + function onMenuItemChange(selected:MenuItem) { camFollow.setPosition(selected.getGraphicMidpoint().x, selected.getGraphicMidpoint().y); @@ -123,13 +137,101 @@ class MainMenuState extends MusicBeatState #end } + #if newgrounds function selectLogin() { + showNgPrompt(true); + } + + function showSavedSessionFailed() + { + showNgPrompt(false); + } + + function showNgPrompt(fromUi:Bool) + { + menuItems.enabled = false; + + var prompt = new Prompt("prompt-ng_login", "Talking to server...", None); + prompt.closeCallback = function() menuItems.enabled = true; + openSubState(prompt); + function onLoginComplete(result:ConnectionResult) + { + switch (result) + { + case Success: + menuItems.resetItem("login", "logout", selectLogout); + prompt.setText("Login Successful"); + prompt.setButtons(Ok); + prompt.onYes = prompt.close; + case Fail(msg): + trace("Login Error:" + msg); + prompt.setText("Login failed"); + prompt.setButtons(Ok); + prompt.onYes = prompt.close; + case Cancelled: + if (prompt != null) + { + prompt.setText("Login cancelled by user"); + prompt.setButtons(Ok); + prompt.onYes = prompt.close; + } + } + } + + NGio.login + ( + function popupLauncher(openPassportUrl) + { + var choiceMsg = fromUi + ? #if web "Log in to Newgrounds?" #else null #end // User-input needed to allow popups + : "Your session has expired.\n Please login again."; + + if (choiceMsg != null) + { + prompt.setText(choiceMsg); + prompt.setButtons(Yes_No); + #if web + prompt.buttons.getItem("yes").fireInstantly = true; + #end + prompt.onYes = function() + { + prompt.setText("Connecting..."); + prompt.setButtons(None); + openPassportUrl(); + }; + prompt.onNo = function() + { + prompt.close(); + prompt = null; + NG.core.cancelLoginRequest(); + }; + } + else + { + prompt.setText("Connecting..."); + openPassportUrl(); + } + }, + onLoginComplete + ); } function selectLogout() { + menuItems.enabled = false; + var prompt = new Prompt("prompt-ng_login", "Log out of " + NG.core.user.name + "?", Yes_No); + prompt.closeCallback = function () menuItems.enabled = true; + prompt.onYes = function() + { + NGio.logout(); + prompt.close(); + menuItems.resetItem("logout", "login", selectLogin); + }; + prompt.onNo = prompt.close; + openSubState(prompt); } + #end function startExitState(state:FlxState) { @@ -156,9 +258,57 @@ class MainMenuState extends MusicBeatState FlxG.sound.music.volume += 0.5 * FlxG.elapsed; } - if (menuItems.active && controls.BACK) + if (menuItems.enabled && controls.BACK) FlxG.switchState(new TitleState()); super.update(elapsed); } } + + +private class MainMenuItemList extends MenuTypedItemList<MainMenuItem> +{ + public var atlas:FlxAtlasFrames; + + public function new (atlas) + { + super(Vertical); + + if (Std.is(atlas, String)) + this.atlas = Paths.getSparrowAtlas(cast atlas); + else + this.atlas = cast atlas; + } + + public function createItem(x = 0.0, y = 0.0, name:String, callback, fireInstantly = false) + { + var i = length; + var item = new MainMenuItem(x, y, name, atlas, callback); + item.fireInstantly = fireInstantly; + item.ID = i; + + return addItem(name, item); + } + + override function destroy() + { + super.destroy(); + atlas = null; + } +} +private class MainMenuItem extends MenuItem +{ + public function new(x = 0.0, y = 0.0, name, atlas, callback) + { + super(x, y, name, atlas, callback); + scrollFactor.set(); + } + + override function changeAnim(anim:String) + { + super.changeAnim(anim); + // position by center + centerOrigin(); + offset.copyFrom(origin); + } +} \ No newline at end of file diff --git a/source/NGio.hx b/source/NGio.hx index ec48ca613..b05b7e8aa 100644 --- a/source/NGio.hx +++ b/source/NGio.hx @@ -6,6 +6,7 @@ import flixel.util.FlxTimer; import io.newgrounds.NG; import io.newgrounds.NGLite; import io.newgrounds.components.ScoreBoardComponent.Period; +import io.newgrounds.objects.Error; import io.newgrounds.objects.Medal; import io.newgrounds.objects.Score; import io.newgrounds.objects.ScoreBoard; @@ -22,6 +23,11 @@ using StringTools; */ class NGio { + /** + * True, if the saved sessionId was used in the initial login, and failed to connect. + * Used in MainMenuState to show a popup to establish a new connection + */ + public static var savedSessionFailed(default, null):Bool = false; public static var isLoggedIn:Bool = false; public static var scoreboardsLoaded:Bool = false; @@ -31,54 +37,68 @@ class NGio public static var ngScoresLoaded(default, null):FlxSignal = new FlxSignal(); public static var GAME_VER:String = ""; - public static var GAME_VER_NUMS:String = ''; - public static var gotOnlineVer:Bool = false; + - public static function noLogin(api:String) + static public function checkVersion(callback:String->Void) { - trace('INIT NOLOGIN'); + trace('checking NG.io version'); GAME_VER = "v" + Application.current.meta.get('version'); - if (api.length != 0) - { - NG.create(api); - - new FlxTimer().start(2, function(tmr:FlxTimer) + NG.core.calls.app.getCurrentVersion(GAME_VER) + .addDataHandler(function(response) { - var call = NG.core.calls.app.getCurrentVersion(GAME_VER).addDataHandler(function(response:Response<GetCurrentVersionResult>) - { - GAME_VER = response.result.data.currentVersion; - GAME_VER_NUMS = GAME_VER.split(" ")[0].trim(); - trace('CURRENT NG VERSION: ' + GAME_VER); - gotOnlineVer = true; - }); - - call.send(); - }); - } + GAME_VER = response.result.data.currentVersion; + trace('CURRENT NG VERSION: ' + GAME_VER); + callback(GAME_VER); + }) + .send(); } - public function new(api:String, encKey:String) + static public function init(api:String, encKey:String) { + var api = APIStuff.API; + if (api == null || api.length == 0) + { + trace("Missing Newgrounds API key, aborting connection"); + return; + } trace("connecting to newgrounds"); - var sessionId:String = NGLite.getSessionId(); - if (sessionId != null) - trace("found web session id"); + #if NG_FORCE_EXPIRED_SESSION + var sessionId:String = "fake_session_id"; + function onSessionFail(error:Error) + { + trace("Forcing an expired saved session. " + + "To disable, comment out NG_FORCE_EXPIRED_SESSION in Project.xml"); + savedSessionFailed = true; + } + #else + var sessionId:String = NGLite.getSessionId(); + if (sessionId != null) + trace("found web session id"); + + #if (debug) + if (sessionId == null && APIStuff.SESSION != null) + { + trace("using debug session id"); + sessionId = APIStuff.SESSION; + } + #end - #if (debug) - if (sessionId == null && APIStuff.SESSION != null) - { - sessionId = APIStuff.SESSION; - trace("using debug session id"); - } + var onSessionFail:Error->Void = null; + if (sessionId == null && FlxG.save.data.sessionId != null) + { + trace("using stored session id"); + sessionId = FlxG.save.data.sessionId; + onSessionFail = function (error) savedSessionFailed = true; + } #end + NG.create(api, sessionId, #if NG_DEBUG true #else false #end, onSessionFail); - NG.create(api, sessionId); - NG.core.verbose = true; + #if NG_VERBOSE NG.core.verbose = true; #end // Set the encryption cipher/format to RC4/Base64. AES128 and Hex are not implemented yet - NG.core.initEncryption(encKey); // Found in you NG project view + NG.core.initEncryption(APIStuff.EncKey); // Found in you NG project view if (NG.core.attemptingLogin) { @@ -89,21 +109,53 @@ class NGio NG.core.onLogin.add(onNGLogin); } //GK: taking out auto login, adding a login button to the main menu - else + // else + // { + // /* They are NOT playing on newgrounds.com, no session id was found. We must start one manually, if we want to. + // * Note: This will cause a new browser window to pop up where they can log in to newgrounds + // */ + // NG.core.requestLogin(onNGLogin); + // } + } + + /** + * Attempts to log in to newgrounds by requesting a new session ID, only call if no session ID was found automatically + * @param popupLauncher The function to call to open the login url, must be inside + * a user input event or the popup blocker will block it. + * @param onComplete A callback with the result of the connection. + */ + static public function login(?popupLauncher:(Void->Void)->Void, onComplete:ConnectionResult->Void) + { + trace("Logging in manually"); + var onPending:Void->Void = null; + if (popupLauncher != null) { - /* They are NOT playing on newgrounds.com, no session id was found. We must start one manually, if we want to. - * Note: This will cause a new browser window to pop up where they can log in to newgrounds - */ - NG.core.requestLogin(onNGLogin); + onPending = function () popupLauncher(NG.core.openPassportUrl); } + + var onSuccess:Void->Void = onNGLogin; + var onFail:Error->Void = null; + var onCancel:Void->Void = null; + if (onComplete != null) + { + onSuccess = function () + { + onNGLogin(); + onComplete(Success); + } + onFail = function (e) onComplete(Fail(e.message)); + onCancel = function() onComplete(Cancelled); + } + + NG.core.requestLogin(onSuccess, onPending, onFail, onCancel); } - function onNGLogin():Void + static function onNGLogin():Void { trace('logged in! user:${NG.core.user.name}'); isLoggedIn = true; FlxG.save.data.sessionId = NG.core.sessionId; - // FlxG.save.flush(); + FlxG.save.flush(); // Load medals then call onNGMedalFetch() NG.core.requestMedals(onNGMedalFetch); @@ -112,9 +164,17 @@ class NGio ngDataLoaded.dispatch(); } + + static public function logout() + { + NG.core.logOut(); + + FlxG.save.data.sessionId = null; + FlxG.save.flush(); + } // --- MEDALS - function onNGMedalFetch():Void + static function onNGMedalFetch():Void { /* // Reading medal info @@ -132,7 +192,7 @@ class NGio } // --- SCOREBOARDS - function onNGBoardsFetch():Void + static function onNGBoardsFetch():Void { /* // Reading medal info @@ -174,7 +234,7 @@ class NGio } } - function onNGScoresFetch():Void + static function onNGScoresFetch():Void { scoreboardsLoaded = true; @@ -209,3 +269,13 @@ class NGio } } } + +enum ConnectionResult +{ + /** Log in successful */ + Success; + /** Could not login */ + Fail(msg:String); + /** User cancelled the login */ + Cancelled; +} diff --git a/source/TitleState.hx b/source/TitleState.hx index 8a8aeb42b..ffc691947 100644 --- a/source/TitleState.hx +++ b/source/TitleState.hx @@ -54,17 +54,11 @@ class TitleState extends MusicBeatState super.create(); - NGio.noLogin(APIStuff.API); - - #if ng - var ng:NGio = new NGio(APIStuff.API, APIStuff.EncKey); - trace('NEWGROUNDS LOL'); - #end - FlxG.save.bind('funkin', 'ninjamuffin99'); - Highscore.load(); + NGio.init(APIStuff.API, APIStuff.EncKey); + if (FlxG.save.data.weekUnlocked != null) { // FIX LATER!!! @@ -264,22 +258,26 @@ class TitleState extends MusicBeatState transitioning = true; // FlxG.sound.music.stop(); - new FlxTimer().start(2, function(tmr:FlxTimer) + if (!OutdatedSubState.leftState) { - // Check if version is outdated - - var version:String = "v" + Application.current.meta.get('version'); - - if (version.trim() != NGio.GAME_VER_NUMS && !OutdatedSubState.leftState) + NGio.checkVersion(function(version) { - trace('OLD VERSION!'); - FlxG.switchState(new OutdatedSubState()); - } - else - { - FlxG.switchState(new MainMenuState()); - } - }); + // Check if version is outdated + + var localVersion:String = "v" + Application.current.meta.get('version'); + var onlineVersion = version.split(" ")[0].trim(); + + if (version.trim() != onlineVersion) + { + trace('OLD VERSION!'); + FlxG.switchState(new OutdatedSubState()); + } + else + { + FlxG.switchState(new MainMenuState()); + } + }); + } // FlxG.sound.play(Paths.music('titleShoot'), 0.7); } diff --git a/source/ui/MenuItemList.hx b/source/ui/MenuItemList.hx index 241aac43d..a79ca1f2b 100644 --- a/source/ui/MenuItemList.hx +++ b/source/ui/MenuItemList.hx @@ -10,53 +10,110 @@ import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.util.FlxSignal; -typedef ItemAsset = OneOfTwo<String, FlxAtlasFrames> +typedef AtlasAsset = OneOfTwo<String, FlxAtlasFrames>; class MenuItemList extends MenuTypedItemList<MenuItem> { - public function addItem(x = 0.0, y = 0.0, name, callback, fireInstantly = false) + public var atlas:FlxAtlasFrames; + + public function new (atlas, navControls:NavControls = Vertical) { - var i = length; - var menuItem = new MenuItem(name, tex, callback, x, y); - menuItem.fireInstantly = fireInstantly; - menuItem.ID = i; - add(menuItem); + super(navControls); - if (i == selectedIndex) - menuItem.select(); - - return menuItem; + if (Std.is(atlas, String)) + this.atlas = Paths.getSparrowAtlas(cast atlas); + else + this.atlas = cast atlas; + } + + public function createItem(x = 0.0, y = 0.0, name, callback, fireInstantly = false) + { + var item = new MenuItem(x, y, name, atlas, callback); + item.fireInstantly = fireInstantly; + return addItem(name, item); + } + + override function destroy() + { + super.destroy(); + atlas = null; } } class MenuTypedItemList<T:MenuItem> extends FlxTypedGroup<T> { - public var tex:FlxAtlasFrames; - public var selectedIndex = 0; + public var selectedIndex(default, null) = 0; + /** Called when a new item is highlighted */ public var onChange(default, null) = new FlxTypedSignal<T->Void>(); + /** Called when an item is accepted */ public var onAcceptPress(default, null) = new FlxTypedSignal<T->Void>(); + /** The navigation control scheme to use */ + public var navControls:NavControls; + /** Set to false to disable nav control */ + public var enabled:Bool = true; - public function new (asset:ItemAsset) + var byName = new Map<String, T>(); + /** Set to true, internally to disable controls, without affecting vars like `enabled` */ + var busy:Bool = false; + + public function new (navControls:NavControls = Vertical) { + this.navControls = navControls; super(); + } + + function addItem(name:String, item:T):T + { + if (length == selectedIndex) + item.select(); - if (Std.is(asset, String)) - tex = Paths.getSparrowAtlas(cast asset); - else - tex = cast asset; + byName[name] = item; + return add(item); + } + + public function resetItem(oldName:String, newName:String, ?callback:Void->Void):T + { + if (!byName.exists(oldName)) + throw "No item named:" + oldName; + + var item = byName[oldName]; + byName.remove(oldName); + byName[newName] = item; + item.setItem(newName, callback); + + return item; } override function update(elapsed:Float) { super.update(elapsed); + if (enabled && !busy) + updateControls(); + } + + inline function updateControls() + { var controls = PlayerSettings.player1.controls; - if (controls.UP_P) - prev(); - - if (controls.DOWN_P) - next(); + switch(navControls) + { + case Vertical: + { + if (controls.UP_P ) prev(); + if (controls.DOWN_P) next(); + } + case Horizontal: + { + if (controls.LEFT_P ) prev(); + if (controls.RIGHT_P) next(); + } + case Both: + { + if (controls.LEFT_P || controls.UP_P ) prev(); + if (controls.RIGHT_P || controls.DOWN_P) next(); + } + } if (controls.ACCEPT) accept(); @@ -71,12 +128,12 @@ class MenuTypedItemList<T:MenuItem> extends FlxTypedGroup<T> selected.callback(); else { - active = false; + busy = true; FlxG.sound.play(Paths.sound('confirmMenu')); FlxFlicker.flicker(selected, 1, 0.06, true, false, function(_) { + busy = false; selected.callback(); - active = true; }); } } @@ -87,24 +144,35 @@ class MenuTypedItemList<T:MenuItem> extends FlxTypedGroup<T> function changeItem(amount:Int) { FlxG.sound.play(Paths.sound('scrollMenu')); + var index = selectedIndex + amount; + if (index >= length) + index = 0; + else if (index < 0) + index = length - 1; + + selectItem(index); + } + + public function selectItem(index:Int) + { members[selectedIndex].idle(); - selectedIndex += amount; - - if (selectedIndex >= length) - selectedIndex = 0; - else if (selectedIndex < 0) - selectedIndex = length - 1; + selectedIndex = index; var selected = members[selectedIndex]; selected.select(); onChange.dispatch(selected); } + public function getItem(name:String) + { + return byName[name]; + } + override function destroy() { super.destroy(); - tex = null; + byName.clear(); } } @@ -116,41 +184,49 @@ class MenuItem extends flixel.FlxSprite */ public var fireInstantly = false; - public function new (name, tex, callback, x = 0.0, y = 0.0) + public function new (x = 0.0, y = 0.0, name, tex, callback) { super(x, y); frames = tex; setItem(name, callback); + antialiasing = true; } - public function setItem(name:String, callback:Void->Void) + public function setItem(name:String, ?callback:Void->Void) { - this.callback = callback; + if (callback != null) + this.callback = callback; + + var selected = animation.curAnim != null && animation.curAnim.name == "selected"; animation.addByPrefix('idle', '$name basic', 24); animation.addByPrefix('selected', '$name white', 24); idle(); - scrollFactor.set(); - antialiasing = true; + if (selected) + select(); } - function updateSize() + function changeAnim(anim:String) { + animation.play(anim); updateHitbox(); - centerOrigin(); - offset.copyFrom(origin); } public function idle() { - animation.play('idle'); - updateSize(); + changeAnim('idle'); } public function select() { - animation.play('selected'); - updateSize(); + changeAnim('selected'); } +} + +enum NavControls +{ + Horizontal; + Vertical; + Both; } \ No newline at end of file diff --git a/source/ui/Prompt.hx b/source/ui/Prompt.hx new file mode 100644 index 000000000..dea595252 --- /dev/null +++ b/source/ui/Prompt.hx @@ -0,0 +1,120 @@ +package ui; + +import flixel.FlxSprite; +import flixel.graphics.frames.FlxAtlasFrames; +import flixel.text.FlxText; +import flixel.util.FlxColor; + +class Prompt extends flixel.FlxSubState +{ + inline static var MARGIN = 100; + + public var onYes:Void->Void; + public var onNo:Void->Void; + public var buttons:MenuItemList; + public var field:FlxText; + public var back:FlxSprite; + + var style:ButtonStyle; + + public function new (atlas, text:String, style:ButtonStyle = Ok) + { + this.style = style; + super(); + + var texture:FlxAtlasFrames; + if (Std.is(atlas, String)) + texture = Paths.getSparrowAtlas(cast atlas); + else + texture = cast atlas; + + back = new FlxSprite(); + back.frames = texture; + back.animation.addByPrefix("idle", "back"); + back.scrollFactor.set(0, 0); + + buttons = new MenuItemList(texture, Horizontal); + + field = new FlxText(); + field.setFormat(Paths.font("vcr.ttf"), 64, FlxColor.BLACK, CENTER); + field.text = text; + field.scrollFactor.set(0, 0); + } + + override function create() + { + super.create(); + + back.animation.play("idle"); + back.updateHitbox(); + back.screenCenter(XY); + add(back); + + field.y = back.y + MARGIN; + field.screenCenter(X); + add(field); + + createButtons(); + add(buttons); + } + + public function setButtons(style:ButtonStyle) + { + if (this.style != style) + { + this.style = style; + createButtons(); + } + } + + function createButtons() + { + // destroy previous buttons + while(buttons.members.length > 0) + { + buttons.remove(buttons.members[0], true).destroy(); + } + + switch(style) + { + case Yes_No : createButtonsHelper("yes", "no"); + case Ok : createButtonsHelper("ok"); + case Custom(yes, no): createButtonsHelper(yes, no); + case None : buttons.exists = false; + }; + } + + function createButtonsHelper(yes:String, ?no:String) + { + buttons.exists = true; + // pass anonymous functions rather than the current callbacks, in case they change later + var yesButton = buttons.createItem(yes, function() onYes()); + yesButton.screenCenter(X); + yesButton.y = back.y + back.height - yesButton.height - MARGIN; + yesButton.scrollFactor.set(0, 0); + if (no != null) + { + // place right + yesButton.x = back.x + back.width - yesButton.width - MARGIN; + + var noButton = buttons.createItem(no, function() onNo()); + noButton.x = back.x + MARGIN; + noButton.y = back.y + back.height - noButton.height - MARGIN; + noButton.scrollFactor.set(0, 0); + } + } + + public function setText(text:String) + { + field.text = text; + field.screenCenter(X); + } +} + +enum ButtonStyle +{ + Ok; + Yes_No; + Custom(yes:String, no:Null<String>);//Todo: more than 2 + None; +} \ No newline at end of file