diff --git a/.gitignore b/.gitignore index 5f82e547..7dbb7d60 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ export/ -.vscode/ \ No newline at end of file +.vscode/ +APIStuff.hx \ No newline at end of file diff --git a/art/thumbnailNewer.png b/art/thumbnailNewer.png new file mode 100644 index 00000000..a4196738 Binary files /dev/null and b/art/thumbnailNewer.png differ diff --git a/source/NGio.hx b/source/NGio.hx new file mode 100644 index 00000000..9e350e25 --- /dev/null +++ b/source/NGio.hx @@ -0,0 +1,140 @@ +package; + +import flixel.FlxG; +import flixel.util.FlxSignal; +import io.newgrounds.NG; +import io.newgrounds.components.ScoreBoardComponent.Period; +import io.newgrounds.objects.Medal; +import io.newgrounds.objects.Score; +import io.newgrounds.objects.ScoreBoard; +import openfl.display.Stage; + +/** + * MADE BY GEOKURELI THE LEGENED GOD HERO MVP + */ +class NGio +{ + public static var isLoggedIn:Bool = false; + public static var scoreboardsLoaded:Bool = false; + + public static var scoreboardArray:Array = []; + + public static var ngDataLoaded(default, null):FlxSignal = new FlxSignal(); + public static var ngScoresLoaded(default, null):FlxSignal = new FlxSignal(); + + public function new(api:String, encKey:String, ?sessionId:String) + { + trace("connecting to newgrounds"); + + NG.createAndCheckSession(api, sessionId); + + NG.core.verbose = true; + // 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 + + trace(NG.core.attemptingLogin); + + if (NG.core.attemptingLogin) + { + /* a session_id was found in the loadervars, this means the user is playing on newgrounds.com + * and we should login shortly. lets wait for that to happen + */ + trace("attempting login"); + NG.core.onLogin.add(onNGLogin); + } + 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); + } + } + + function onNGLogin():Void + { + trace('logged in! user:${NG.core.user.name}'); + isLoggedIn = true; + FlxG.save.data.sessionId = NG.core.sessionId; + // FlxG.save.flush(); + // Load medals then call onNGMedalFetch() + NG.core.requestMedals(onNGMedalFetch); + + // Load Scoreboards hten call onNGBoardsFetch() + NG.core.requestScoreBoards(onNGBoardsFetch); + + ngDataLoaded.dispatch(); + } + + // --- MEDALS + function onNGMedalFetch():Void + { + /* + // Reading medal info + for (id in NG.core.medals.keys()) + { + var medal = NG.core.medals.get(id); + trace('loaded medal id:$id, name:${medal.name}, description:${medal.description}'); + } + + // Unlocking medals + var unlockingMedal = NG.core.medals.get(54352);// medal ids are listed in your NG project viewer + if (!unlockingMedal.unlocked) + unlockingMedal.sendUnlock(); + */ + } + + // --- SCOREBOARDS + function onNGBoardsFetch():Void + { + /* + // Reading medal info + for (id in NG.core.scoreBoards.keys()) + { + var board = NG.core.scoreBoards.get(id); + trace('loaded scoreboard id:$id, name:${board.name}'); + } + */ + // var board = NG.core.scoreBoards.get(8004);// ID found in NG project view + + // Posting a score thats OVER 9000! + // board.postScore(FlxG.random.int(0, 1000)); + + // --- To view the scores you first need to select the range of scores you want to see --- + + // add an update listener so we know when we get the new scores + // board.onUpdate.add(onNGScoresFetch); + trace("shoulda got score by NOW!"); + // board.requestScores(20);// get the best 10 scores ever logged + // more info on scores --- http://www.newgrounds.io/help/components/#scoreboard-getscores + } + + function onNGScoresFetch():Void + { + scoreboardsLoaded = true; + + ngScoresLoaded.dispatch(); + /* + for (score in NG.core.scoreBoards.get(8737).scores) + { + trace('score loaded user:${score.user.name}, score:${score.formatted_value}'); + + } + */ + + // var board = NG.core.scoreBoards.get(8004);// ID found in NG project view + // board.postScore(HighScore.score); + + // NGio.scoreboardArray = NG.core.scoreBoards.get(8004).scores; + } + + inline static public function unlockMedal(id:Int) + { + if (isLoggedIn) + { + var medal = NG.core.medals.get(id); + if (!medal.unlocked) + medal.sendUnlock(); + } + } +} diff --git a/source/io/newgrounds/Call.hx b/source/io/newgrounds/Call.hx new file mode 100644 index 00000000..bd5d7aca --- /dev/null +++ b/source/io/newgrounds/Call.hx @@ -0,0 +1,227 @@ +package io.newgrounds; + +import io.newgrounds.utils.Dispatcher; +import io.newgrounds.utils.AsyncHttp; +import io.newgrounds.objects.Error; +import io.newgrounds.objects.events.Result; +import io.newgrounds.objects.events.Result.ResultBase; +import io.newgrounds.objects.events.Response; + +import haxe.ds.StringMap; +import haxe.Json; + +/** A generic way to handle calls agnostic to their type */ +interface ICallable { + + public var component(default, null):String; + + public function send():Void; + public function queue():Void; + public function destroy():Void; +} + +class Call + implements ICallable { + + public var component(default, null):String; + + var _core:NGLite; + var _properties:StringMap; + var _parameters:StringMap; + var _requireSession:Bool; + var _isSecure:Bool; + + // --- BASICALLY SIGNALS + var _dataHandlers:TypedDispatcher>; + var _successHandlers:Dispatcher; + var _httpErrorHandlers:TypedDispatcher; + var _statusHandlers:TypedDispatcher; + + public function new (core:NGLite, component:String, requireSession:Bool = false, isSecure:Bool = false) { + + _core = core; + this.component = component; + _requireSession = requireSession; + _isSecure = isSecure && core.encryptionHandler != null; + } + + /** adds a property to the input's object. **/ + public function addProperty(name:String, value:Dynamic):Call { + + if (_properties == null) + _properties = new StringMap(); + + _properties.set(name, value); + + return this; + } + + /** adds a parameter to the call's component object. **/ + public function addComponentParameter(name:String, value:Dynamic, defaultValue:Dynamic = null):Call { + + if (value == defaultValue)//TODO?: allow sending null value + return this; + + if (_parameters == null) + _parameters = new StringMap(); + + _parameters.set(name, value); + + return this; + } + + /** Handy callback setter for chained call modifiers. Called when ng.io replies successfully */ + public function addDataHandler(handler:Response->Void):Call { + + if (_dataHandlers == null) + _dataHandlers = new TypedDispatcher>(); + + _dataHandlers.add(handler); + return this; + } + + /** Handy callback setter for chained call modifiers. Called when ng.io replies successfully */ + public function addSuccessHandler(handler:Void->Void):Call { + + if (_successHandlers == null) + _successHandlers = new Dispatcher(); + + _successHandlers.add(handler); + return this; + } + + /** Handy callback setter for chained call modifiers. Called when ng.io does not reply for any reason */ + public function addErrorHandler(handler:Error->Void):Call { + + if (_httpErrorHandlers == null) + _httpErrorHandlers = new TypedDispatcher(); + + _httpErrorHandlers.add(handler); + return this; + } + + /** Handy callback setter for chained call modifiers. No idea when this is called; */ + public function addStatusHandler(handler:Int->Void):Call {//TODO:learn what this is for + + if (_statusHandlers == null) + _statusHandlers = new TypedDispatcher(); + + _statusHandlers.add(handler); + return this; + } + + /** + * Sends the call to the server, do not modify this object after calling this + * @param secure If encryption is enabled, it will encrypt the call. + **/ + public function send():Void { + + var data:Dynamic = {}; + data.app_id = _core.appId; + data.call = {}; + data.call.component = component; + + if (_core.debug) + addProperty("debug", true); + + if (_properties == null || !_properties.exists("session_id")) { + // --- HAS NO SESSION ID + + if (_core.sessionId != null) { + // --- AUTO ADD SESSION ID + + addProperty("session_id", _core.sessionId); + + } else if (_requireSession){ + + _core.logError(new Error('cannot send "$component" call without a sessionId')); + return; + } + } + + if (_properties != null) { + + for (field in _properties.keys()) + Reflect.setField(data, field, _properties.get(field)); + } + + if (_parameters != null) { + + data.call.parameters = {}; + + for (field in _parameters.keys()) + Reflect.setField(data.call.parameters, field, _parameters.get(field)); + } + + _core.logVerbose('Post - ${Json.stringify(data)}'); + + if (_isSecure) { + + var secureData = _core.encryptionHandler(Json.stringify(data.call)); + data.call = {}; + data.call.secure = secureData; + + _core.logVerbose(' secure - $secureData'); + } + + _core.markCallPending(this); + + AsyncHttp.send(_core, Json.stringify(data), onData, onHttpError, onStatus); + } + + /** Adds the call to the queue */ + public function queue():Void { + + _core.queueCall(this); + } + + function onData(reply:String):Void { + + _core.logVerbose('Reply - $reply'); + + if (_dataHandlers == null && _successHandlers == null) + return; + + var response = new Response(_core, reply); + + if (_dataHandlers != null) + _dataHandlers.dispatch(response); + + if (response.success && response.result.success && _successHandlers != null) + _successHandlers.dispatch(); + + destroy(); + } + + function onHttpError(message:String):Void { + + _core.logError(message); + + if (_httpErrorHandlers == null) + return; + + var error = new Error(message); + _httpErrorHandlers.dispatch(error); + } + + function onStatus(status:Int):Void { + + if (_statusHandlers == null) + return; + + _statusHandlers.dispatch(status); + } + + public function destroy():Void { + + _core = null; + + _properties = null; + _parameters = null; + + _dataHandlers = null; + _successHandlers = null; + _httpErrorHandlers = null; + _statusHandlers = null; + } +} \ No newline at end of file diff --git a/source/io/newgrounds/NG.hx b/source/io/newgrounds/NG.hx new file mode 100644 index 00000000..3445443e --- /dev/null +++ b/source/io/newgrounds/NG.hx @@ -0,0 +1,475 @@ +package io.newgrounds; + +#if ng_lite +typedef NG = NGLite; //TODO: test and make lite UI +#else +import io.newgrounds.utils.Dispatcher; +import io.newgrounds.objects.Error; +import io.newgrounds.objects.events.Result.SessionResult; +import io.newgrounds.objects.events.Result.MedalListResult; +import io.newgrounds.objects.events.Result.ScoreBoardResult; +import io.newgrounds.objects.events.Response; +import io.newgrounds.objects.User; +import io.newgrounds.objects.Medal; +import io.newgrounds.objects.Session; +import io.newgrounds.objects.ScoreBoard; + +import haxe.ds.IntMap; +import haxe.Timer; + +/** + * The Newgrounds API for Haxe. + * Contains many things ripped from MSGhero + * - https://github.com/MSGhero/NG.hx + * @author GeoKureli + */ +class NG extends NGLite { + + static public var core(default, null):NG; + static public var onCoreReady(default, null):Dispatcher = new Dispatcher(); + + // --- DATA + + /** The logged in user */ + public var user(get, never):User; + function get_user():User { + + if (_session == null) + return null; + + return _session.user; + } + public var passportUrl(get, never):String; + function get_passportUrl():String { + + if (_session == null || _session.status != SessionStatus.REQUEST_LOGIN) + return null; + + return _session.passportUrl; + } + public var medals(default, null):IntMap; + public var scoreBoards(default, null):IntMap; + + // --- EVENTS + + public var onLogin(default, null):Dispatcher; + public var onLogOut(default, null):Dispatcher; + public var onMedalsLoaded(default, null):Dispatcher; + public var onScoreBoardsLoaded(default, null):Dispatcher; + + // --- MISC + + public var loggedIn(default, null):Bool; + public var attemptingLogin(default, null):Bool; + + var _loginCancelled:Bool; + var _passportCallback:Void->Void; + + var _session:Session; + + /** + * Iniitializes the API, call before utilizing any other component + * @param appId The unique ID of your app as found in the 'API Tools' tab of your Newgrounds.com project. + * @param sessionId A unique session id used to identify the active user. + **/ + public function new(appId = "test", sessionId:String = null, ?onSessionFail:Error->Void) { + + _session = new Session(this); + onLogin = new Dispatcher(); + onLogOut = new Dispatcher(); + onMedalsLoaded = new Dispatcher(); + onScoreBoardsLoaded = new Dispatcher(); + + attemptingLogin = sessionId != null; + + super(appId, sessionId, onSessionFail); + } + + /** + * Creates NG.core, the heart and soul of the API. This is not the only way to create an instance, + * nor is NG a forced singleton, but it's the only way to set the static NG.core. + **/ + static public function create(appId = "test", sessionId:String = null, ?onSessionFail:Error->Void):Void { + + core = new NG(appId, sessionId, onSessionFail); + + onCoreReady.dispatch(); + } + + /** + * Creates NG.core, and tries to create a session. This is not the only way to create an instance, + * nor is NG a forced singleton, but it's the only way to set the static NG.core. + **/ + static public function createAndCheckSession + ( appId = "test" + , backupSession:String = null + , ?onSessionFail:Error->Void + ):Void { + + var session = NGLite.getSessionId(); + if (session == null) + session = backupSession; + + create(appId, session, onSessionFail); + + core.host = getHost(); + if (core.sessionId != null) + core.attemptingLogin = true; + } + + // ------------------------------------------------------------------------------------------- + // APP + // ------------------------------------------------------------------------------------------- + + override function checkInitialSession(failHandler:Error->Void, response:Response):Void { + + onSessionReceive(response, null, null, failHandler); + } + + /** + * Begins the login process + * + * @param onSuccess Called when the login is a success + * @param onPending Called when the passportUrl has been identified, call NG.core.openPassportLink + * to open the link continue the process. Leave as null to open the url automatically + * NOTE: Browser games must open links on click events or else it will be blocked by + * the popup blocker. + * @param onFail + * @param onCancel Called when the user denies the passport connection. + */ + public function requestLogin + ( onSuccess:Void->Void = null + , onPending:Void->Void = null + , onFail :Error->Void = null + , onCancel :Void->Void = null + ):Void { + + if (attemptingLogin) { + + logError("cannot request another login until the previous attempt is complete"); + return; + } + + if (loggedIn) { + + logError("cannot log in, already logged in"); + return; + } + + attemptingLogin = true; + _loginCancelled = false; + _passportCallback = null; + + var call = calls.app.startSession(true) + .addDataHandler(onSessionReceive.bind(_, onSuccess, onPending, onFail, onCancel)); + + if (onFail != null) + call.addErrorHandler(onFail); + + call.send(); + } + + function onSessionReceive + ( response :Response + , onSuccess:Void->Void = null + , onPending:Void->Void = null + , onFail :Error->Void = null + , onCancel :Void->Void = null + ):Void { + + if (!response.success || !response.result.success) { + + sessionId = null; + endLoginAndCall(null); + + if (onFail != null) + onFail(!response.success ? response.error : response.result.error); + + return; + } + + _session.parse(response.result.data.session); + sessionId = _session.id; + + logVerbose('session started - status: ${_session.status}'); + + if (_session.status == SessionStatus.REQUEST_LOGIN) { + + _passportCallback = checkSession.bind(null, onSuccess, onCancel); + if (onPending != null) + onPending(); + else + openPassportUrl(); + + } else + checkSession(null, onSuccess, onCancel); + } + + /** + * Call this once the passport link is established and it will load the passport URL and + * start checking for session connect periodically + */ + public function openPassportUrl():Void { + + if (passportUrl != null) { + + logVerbose('loading passport: ${passportUrl}'); + openPassportHelper(passportUrl); + onPassportUrlOpen(); + + } else + logError("Cannot open passport"); + } + + + static function openPassportHelper(url:String):Void { + var window = "_blank"; + + #if flash + flash.Lib.getURL(new flash.net.URLRequest(url), window); + #elseif (js && html5) + js.Browser.window.open(url, window); + #elseif desktop + + #if (sys && windows) + Sys.command("start", ["", url]); + #elseif mac + Sys.command("/usr/bin/open", [url]); + #elseif linux + Sys.command("/usr/bin/xdg-open", [path, "&"]); + #end + + #elseif android + JNI.createStaticMethod + ( "org/haxe/lime/GameActivity" + , "openURL" + , "(Ljava/lang/String;Ljava/lang/String;)V" + ) (url, window); + #end + } + + /** + * Call this once the passport link is established and it will start checking for session connect periodically + */ + public function onPassportUrlOpen():Void { + + if (_passportCallback != null) + _passportCallback(); + + _passportCallback = null; + } + + function checkSession(response:Response, onSucceess:Void->Void, onCancel:Void->Void):Void { + + if (response != null) { + + if (!response.success || !response.result.success) { + + log("login cancelled via passport"); + + endLoginAndCall(onCancel); + return; + } + + _session.parse(response.result.data.session); + } + + if (_session.status == SessionStatus.USER_LOADED) { + + loggedIn = true; + endLoginAndCall(onSucceess); + onLogin.dispatch(); + + } else if (_session.status == SessionStatus.REQUEST_LOGIN){ + + var call = calls.app.checkSession() + .addDataHandler(checkSession.bind(_, onSucceess, onCancel)); + + // Wait 3 seconds and try again + timer(3.0, + function():Void { + + // Check if cancelLoginRequest was called + if (!_loginCancelled) + call.send(); + else { + + log("login cancelled via cancelLoginRequest"); + endLoginAndCall(onCancel); + } + } + ); + + } else + // The user cancelled the passport + endLoginAndCall(onCancel); + } + + public function cancelLoginRequest():Void { + + if (attemptingLogin) + _loginCancelled = true; + } + + function endLoginAndCall(callback:Void->Void):Void { + + attemptingLogin = false; + _loginCancelled = false; + + if (callback != null) + callback(); + } + + public function logOut(onComplete:Void->Void = null):Void { + + var call = calls.app.endSession() + .addSuccessHandler(onLogOutSuccessful); + + if (onComplete != null) + call.addSuccessHandler(onComplete); + + call.addSuccessHandler(onLogOut.dispatch) + .send(); + } + + function onLogOutSuccessful():Void { + + _session.expire(); + sessionId = null; + loggedIn = false; + } + + // ------------------------------------------------------------------------------------------- + // MEDALS + // ------------------------------------------------------------------------------------------- + + public function requestMedals(onSuccess:Void->Void = null, onFail:Error->Void = null):Void { + + var call = calls.medal.getList() + .addDataHandler(onMedalsReceived); + + if (onSuccess != null) + call.addSuccessHandler(onSuccess); + + if (onFail != null) + call.addErrorHandler(onFail); + + call.send(); + } + + function onMedalsReceived(response:Response):Void { + + if (!response.success || !response.result.success) + return; + + var idList:Array = new Array(); + + if (medals == null) { + + medals = new IntMap(); + + for (medalData in response.result.data.medals) { + + var medal = new Medal(this, medalData); + medals.set(medal.id, medal); + idList.push(medal.id); + } + } else { + + for (medalData in response.result.data.medals) { + + medals.get(medalData.id).parse(medalData); + idList.push(medalData.id); + } + } + + logVerbose('${response.result.data.medals.length} Medals received [${idList.join(", ")}]'); + + onMedalsLoaded.dispatch(); + } + + // ------------------------------------------------------------------------------------------- + // SCOREBOARDS + // ------------------------------------------------------------------------------------------- + + public function requestScoreBoards(onSuccess:Void->Void = null, onFail:Error->Void = null):Void { + + if (scoreBoards != null) { + + log("aborting scoreboard request, all scoreboards are loaded"); + + if (onSuccess != null) + onSuccess(); + + return; + } + + var call = calls.scoreBoard.getBoards() + .addDataHandler(onBoardsReceived); + + if (onSuccess != null) + call.addSuccessHandler(onSuccess); + + if (onFail != null) + call.addErrorHandler(onFail); + + call.send(); + } + + function onBoardsReceived(response:Response):Void { + + if (!response.success || !response.result.success) + return; + + var idList:Array = new Array(); + + if (scoreBoards == null) { + + scoreBoards = new IntMap(); + + for (boardData in response.result.data.scoreboards) { + + var board = new ScoreBoard(this, boardData); + scoreBoards.set(board.id, board); + idList.push(board.id); + } + } + + logVerbose('${response.result.data.scoreboards.length} ScoreBoards received [${idList.join(", ")}]'); + + onScoreBoardsLoaded.dispatch(); + } + + // ------------------------------------------------------------------------------------------- + // HELPERS + // ------------------------------------------------------------------------------------------- + + function timer(delay:Float, callback:Void->Void):Void { + + var timer = new Timer(Std.int(delay * 1000)); + timer.run = function func():Void { + + timer.stop(); + callback(); + } + } + + static var urlParser:EReg = ~/^(?:http[s]?:\/\/)?([^:\/\s]+)(:[0-9]+)?((?:\/\w+)*\/)([\w\-\.]+[^#?\s]+)([^#\s]*)?(#[\w\-]+)?$/i;//TODO:trim + /** Used to get the current web host of your game. */ + static public function getHost():String { + + var url = NGLite.getUrl(); + + if (url == null || url == "") + return ""; + + if (url.indexOf("file") == 0) + return ""; + + if (urlParser.match(url)) + return urlParser.matched(1); + + return ""; + } +} +#end \ No newline at end of file diff --git a/source/io/newgrounds/NGLite.hx b/source/io/newgrounds/NGLite.hx new file mode 100644 index 00000000..0c9d5bdf --- /dev/null +++ b/source/io/newgrounds/NGLite.hx @@ -0,0 +1,287 @@ +package io.newgrounds; + +import haxe.crypto.Base64; +import haxe.io.Bytes; +import haxe.PosInfos; + +import io.newgrounds.Call.ICallable; +import io.newgrounds.components.ComponentList; +import io.newgrounds.crypto.EncryptionFormat; +import io.newgrounds.crypto.Cipher; +import io.newgrounds.crypto.Rc4; +import io.newgrounds.objects.Error; +import io.newgrounds.objects.events.Response; +import io.newgrounds.objects.events.Result.ResultBase; +import io.newgrounds.objects.events.Result.SessionResult; +import io.newgrounds.utils.Dispatcher; + +#if !(html5 || flash || desktop || neko) + #error "Target not supported, use: Flash, JS/HTML5, cpp or maybe neko"; +#end + +/** + * The barebones NG.io API. Allows API calls with code completion + * and retrieves server data via strongly typed Objects + * + * Contains many things ripped from MSGhero's repo + * - https://github.com/MSGhero/NG.hx + * + * @author GeoKureli + */ +class NGLite { + + static public var core(default, null):NGLite; + static public var onCoreReady(default, null):Dispatcher = new Dispatcher(); + + /** Enables verbose logging */ + public var verbose:Bool; + public var debug:Bool; + /** The unique ID of your app as found in the 'API Tools' tab of your Newgrounds.com project. */ + public var appId(default, null):String; + /** The name of the host the game is being played on */ + public var host:String; + + @:isVar + public var sessionId(default, set):String; + function set_sessionId(value:String):String { + + return this.sessionId = value == "" ? null : value; + } + + /** Components used to call the NG server directly */ + public var calls(default, null):ComponentList; + + /** + * Converts an object to an encrypted string that can be decrypted by the server. + * Set your preffered encrypter here, + * or just call setDefaultEcryptionHandler with your app's encryption settings + **/ + public var encryptionHandler:String->String; + + /** + * Iniitializes the API, call before utilizing any other component + * @param appId The unique ID of your app as found in the 'API Tools' tab of your Newgrounds.com project. + * @param sessionId A unique session id used to identify the active user. + **/ + public function new(appId = "test", sessionId:String = null, ?onSessionFail:Error->Void) { + + this.appId = appId; + this.sessionId = sessionId; + + calls = new ComponentList(this); + + if (this.sessionId != null) { + + calls.app.checkSession() + .addDataHandler(checkInitialSession.bind(onSessionFail)) + .addErrorHandler(initialSessionFail.bind(onSessionFail)) + .send(); + } + } + + function checkInitialSession(onFail:Error->Void, response:Response):Void { + + if (!response.success || !response.result.success || response.result.data.session.expired) { + + initialSessionFail(onFail, response.success ? response.result.error : response.error); + } + } + + function initialSessionFail(onFail:Error->Void, error:Error):Void { + + sessionId = null; + + if (onFail != null) + onFail(error); + } + + /** + * Creates NG.core, the heart and soul of the API. This is not the only way to create an instance, + * nor is NG a forced singleton, but it's the only way to set the static NG.core. + **/ + static public function create(appId = "test", sessionId:String = null, ?onSessionFail:Error->Void):Void { + + core = new NGLite(appId, sessionId, onSessionFail); + + onCoreReady.dispatch(); + } + + /** + * Creates NG.core, and tries to create a session. This is not the only way to create an instance, + * nor is NG a forced singleton, but it's the only way to set the static NG.core. + **/ + static public function createAndCheckSession + ( appId = "test" + , backupSession:String = null + , ?onSessionFail:Error->Void + ):Void { + + var session = getSessionId(); + if (session == null) + session = backupSession; + + create(appId, session, onSessionFail); + } + + inline static public function getUrl():String { + + #if html5 + return js.Browser.document.location.href; + #elseif flash + return flash.Lib.current.stage.loaderInfo != null + ? flash.Lib.current.stage.loaderInfo.url + : null; + #else + return null; + #end + } + + static public function getSessionId():String { + + #if html5 + + var url = getUrl(); + + // Check for URL params + var index = url.indexOf("?"); + if (index != -1) { + + // Check for session ID in params + for (param in url.substr(index + 1).split("&")) { + + index = param.indexOf("="); + if (index != -1 && param.substr(0, index) == "ngio_session_id") + return param.substr(index + 1); + } + } + + #elseif flash + + if (flash.Lib.current.stage.loaderInfo != null + && Reflect.hasField(flash.Lib.current.stage.loaderInfo.parameters, "ngio_session_id")) + return Reflect.field(flash.Lib.current.stage.loaderInfo.parameters, "ngio_session_id"); + + #end + + return null; + + // --- EXAMPLE LOADER PARAMS + //{ "1517703669" : "" + //, "ng_username" : "GeoKureli" + //, "NewgroundsAPI_SessionID" : "F1LusbG6P8Qf91w7zeUE37c1752563f366688ac6153996d12eeb111a2f60w2xn" + //, "NewgroundsAPI_PublisherID" : 1 + //, "NewgroundsAPI_UserID" : 488329 + //, "NewgroundsAPI_SandboxID" : "5a76520e4ae1e" + //, "ngio_session_id" : "0c6c4e02567a5116734ba1a0cd841dac28a42e79302290" + //, "NewgroundsAPI_UserName" : "GeoKureli" + //} + } + + // ------------------------------------------------------------------------------------------- + // CALLS + // ------------------------------------------------------------------------------------------- + + var _queuedCalls:Array = new Array(); + var _pendingCalls:Array = new Array(); + + @:allow(io.newgrounds.Call) + @:generic + function queueCall(call:Call):Void { + + logVerbose('queued - ${call.component}'); + + _queuedCalls.push(call); + checkQueue(); + } + + @:allow(io.newgrounds.Call) + @:generic + function markCallPending(call:Call):Void { + + _pendingCalls.push(call); + + call.addDataHandler(function (_):Void { onCallComplete(call); }); + call.addErrorHandler(function (_):Void { onCallComplete(call); }); + } + + function onCallComplete(call:ICallable):Void { + + _pendingCalls.remove(call); + checkQueue(); + } + + function checkQueue():Void { + + if (_pendingCalls.length == 0 && _queuedCalls.length > 0) + _queuedCalls.shift().send(); + } + + // ------------------------------------------------------------------------------------------- + // LOGGING / ERRORS + // ------------------------------------------------------------------------------------------- + + /** Called internally, set this to your preferred logging method */ + dynamic public function log(any:Dynamic, ?pos:PosInfos):Void {//TODO: limit access via @:allow + + haxe.Log.trace('[Newgrounds API] :: ${any}', pos); + } + + /** used internally, logs if verbose is true */ + inline public function logVerbose(any:Dynamic, ?pos:PosInfos):Void {//TODO: limit access via @:allow + + if (verbose) + log(any, pos); + } + + /** Used internally. Logs by default, set this to your preferred error handling method */ + dynamic public function logError(any:Dynamic, ?pos:PosInfos):Void {//TODO: limit access via @:allow + + log('Error: $any', pos); + } + + /** used internally, calls log error if the condition is false. EX: if (assert(data != null, "null data")) */ + inline public function assert(condition:Bool, msg:Dynamic, ?pos:PosInfos):Bool {//TODO: limit access via @:allow + if (!condition) + logError(msg, pos); + + return condition; + } + + // ------------------------------------------------------------------------------------------- + // ENCRYPTION + // ------------------------------------------------------------------------------------------- + + /** Sets */ + public function initEncryption + ( key :String + , cipher:Cipher = Cipher.RC4 + , format:EncryptionFormat = EncryptionFormat.BASE_64 + ):Void { + + if (cipher == Cipher.NONE) + encryptionHandler = null; + else if (cipher == Cipher.RC4) + encryptionHandler = encryptRc4.bind(key, format); + else + throw "aes not yet implemented"; + } + + function encryptRc4(key:String, format:EncryptionFormat, data:String):String { + + if (format == EncryptionFormat.HEX) + throw "hex format not yet implemented"; + + var keyBytes:Bytes; + if (format == EncryptionFormat.BASE_64) + keyBytes = Base64.decode(key); + else + keyBytes = null;//TODO + + var dataBytes = new Rc4(keyBytes).crypt(Bytes.ofString(data)); + + if (format == EncryptionFormat.BASE_64) + return Base64.encode(dataBytes); + + return null; + } +} \ No newline at end of file diff --git a/source/io/newgrounds/components/AppComponent.hx b/source/io/newgrounds/components/AppComponent.hx new file mode 100644 index 00000000..1ea5b8b1 --- /dev/null +++ b/source/io/newgrounds/components/AppComponent.hx @@ -0,0 +1,44 @@ +package io.newgrounds.components; + +import io.newgrounds.objects.events.Result; +import io.newgrounds.objects.events.Result.SessionResult; +import io.newgrounds.NGLite; + +class AppComponent extends Component { + + public function new (core:NGLite) { super(core); } + + public function startSession(force:Bool = false):Call { + + return new Call(_core, "App.startSession") + .addComponentParameter("force", force, false); + } + + public function checkSession():Call { + + return new Call(_core, "App.checkSession", true); + } + + public function endSession():Call { + + return new Call(_core, "App.endSession", true); + } + + public function getCurrentVersion(version:String):Call { + + return new Call(_core, "App.getCurrentVersion") + .addComponentParameter("version", version); + } + + public function getHostLicense():Call { + + return new Call(_core, "App.getHostLicense") + .addComponentParameter("host", _core.host); + } + + public function logView():Call { + + return new Call(_core, "App.logView") + .addComponentParameter("host", _core.host); + } +} \ No newline at end of file diff --git a/source/io/newgrounds/components/Component.hx b/source/io/newgrounds/components/Component.hx new file mode 100644 index 00000000..3c588ff1 --- /dev/null +++ b/source/io/newgrounds/components/Component.hx @@ -0,0 +1,13 @@ +package io.newgrounds.components; + +import io.newgrounds.NGLite; + +class Component { + + var _core:NGLite; + + public function new(core:NGLite) { + + this._core = core; + } +} \ No newline at end of file diff --git a/source/io/newgrounds/components/ComponentList.hx b/source/io/newgrounds/components/ComponentList.hx new file mode 100644 index 00000000..315abf4e --- /dev/null +++ b/source/io/newgrounds/components/ComponentList.hx @@ -0,0 +1,25 @@ +package io.newgrounds.components; +class ComponentList { + + var _core:NGLite; + + // --- COMPONENTS + public var medal : MedalComponent; + public var app : AppComponent; + public var event : EventComponent; + public var scoreBoard: ScoreBoardComponent; + public var loader : LoaderComponent; + public var gateway : GatewayComponent; + + public function new(core:NGLite) { + + _core = core; + + medal = new MedalComponent (_core); + app = new AppComponent (_core); + event = new EventComponent (_core); + scoreBoard = new ScoreBoardComponent(_core); + loader = new LoaderComponent (_core); + gateway = new GatewayComponent (_core); + } +} diff --git a/source/io/newgrounds/components/EventComponent.hx b/source/io/newgrounds/components/EventComponent.hx new file mode 100644 index 00000000..2e631773 --- /dev/null +++ b/source/io/newgrounds/components/EventComponent.hx @@ -0,0 +1,16 @@ +package io.newgrounds.components; + +import io.newgrounds.objects.events.Result.LogEventResult; +import io.newgrounds.NGLite; + +class EventComponent extends Component { + + public function new (core:NGLite){ super(core); } + + public function logEvent(eventName:String):Call { + + return new Call(_core, "Event.logEvent") + .addComponentParameter("event_name", eventName) + .addComponentParameter("host", _core.host); + } +} \ No newline at end of file diff --git a/source/io/newgrounds/components/GatewayComponent.hx b/source/io/newgrounds/components/GatewayComponent.hx new file mode 100644 index 00000000..4f0ece61 --- /dev/null +++ b/source/io/newgrounds/components/GatewayComponent.hx @@ -0,0 +1,25 @@ +package io.newgrounds.components; + +import io.newgrounds.objects.events.Result; +import io.newgrounds.NGLite; + +class GatewayComponent extends Component { + + public function new (core:NGLite){ super(core); } + + public function getDatetime():Call { + + return new Call(_core, "Gateway.getDatetime"); + } + + public function getVersion():Call { + + return new Call(_core, "Gateway.getVersion"); + } + + public function ping():Call { + + return new Call(_core, "Gateway.ping"); + } + +} \ No newline at end of file diff --git a/source/io/newgrounds/components/LoaderComponent.hx b/source/io/newgrounds/components/LoaderComponent.hx new file mode 100644 index 00000000..717cc2eb --- /dev/null +++ b/source/io/newgrounds/components/LoaderComponent.hx @@ -0,0 +1,44 @@ +package io.newgrounds.components; + +import io.newgrounds.objects.events.Result; +import io.newgrounds.NGLite; + +class LoaderComponent extends Component { + + public function new (core:NGLite){ super(core); } + + public function loadAuthorUrl(redirect:Bool = false):Call { + + return new Call(_core, "Loader.loadAuthorUrl") + .addComponentParameter("host", _core.host) + .addComponentParameter("redirect", redirect, true); + } + + public function loadMoreGames(redirect:Bool = false):Call { + + return new Call(_core, "Loader.loadMoreGames") + .addComponentParameter("host", _core.host) + .addComponentParameter("redirect", redirect, true); + } + + public function loadNewgrounds(redirect:Bool = false):Call { + + return new Call(_core, "Loader.loadNewgrounds") + .addComponentParameter("host", _core.host) + .addComponentParameter("redirect", redirect, true); + } + + public function loadOfficialUrl(redirect:Bool = false):Call { + + return new Call(_core, "Loader.loadOfficialUrl") + .addComponentParameter("host", _core.host) + .addComponentParameter("redirect", redirect, true); + } + + public function loadReferral(redirect:Bool = false):Call { + + return new Call(_core, "Loader.loadReferral") + .addComponentParameter("host", _core.host) + .addComponentParameter("redirect", redirect, true); + } +} \ No newline at end of file diff --git a/source/io/newgrounds/components/MedalComponent.hx b/source/io/newgrounds/components/MedalComponent.hx new file mode 100644 index 00000000..7e56621c --- /dev/null +++ b/source/io/newgrounds/components/MedalComponent.hx @@ -0,0 +1,21 @@ +package io.newgrounds.components; + +import io.newgrounds.objects.events.Result; +import io.newgrounds.Call; +import io.newgrounds.NGLite; + +class MedalComponent extends Component { + + public function new(core:NGLite):Void { super(core); } + + public function unlock(id:Int):Call { + + return new Call(_core, "Medal.unlock", true, true) + .addComponentParameter("id", id); + } + + public function getList():Call { + + return new Call(_core, "Medal.getList"); + } +} \ No newline at end of file diff --git a/source/io/newgrounds/components/ScoreBoardComponent.hx b/source/io/newgrounds/components/ScoreBoardComponent.hx new file mode 100644 index 00000000..7417e67c --- /dev/null +++ b/source/io/newgrounds/components/ScoreBoardComponent.hx @@ -0,0 +1,114 @@ +package io.newgrounds.components; + +import io.newgrounds.objects.User; +import io.newgrounds.objects.events.Response; +import io.newgrounds.objects.events.Result; +import io.newgrounds.objects.events.Result.ScoreBoardResult; +import io.newgrounds.objects.events.Result.ScoreResult; +import io.newgrounds.NGLite; +import io.newgrounds.objects.ScoreBoard; + +import haxe.ds.IntMap; + +class ScoreBoardComponent extends Component { + + public var allById:IntMap; + + public function new (core:NGLite){ super(core); } + + // ------------------------------------------------------------------------------------------- + // GET SCORES + // ------------------------------------------------------------------------------------------- + + public function getBoards():Call { + + return new Call(_core, "ScoreBoard.getBoards"); + } + + /*function onBoardsReceive(response:Response):Void { + + if (!response.result.success) + return; + + allById = new IntMap(); + + for (boardData in response.result.scoreboards) + createBoard(boardData); + + _core.log('${response.result.scoreboards.length} ScoreBoards loaded'); + }*/ + + // ------------------------------------------------------------------------------------------- + // GET SCORES + // ------------------------------------------------------------------------------------------- + + public function getScores + ( id :Int + , limit :Int = 10 + , skip :Int = 0 + , period:Period = Period.DAY + , social:Bool = false + , tag :String = null + , user :Dynamic = null + ):Call { + + if (user != null && !Std.is(user, String) && !Std.is(user, Int)) + user = user.id; + + return new Call(_core, "ScoreBoard.getScores") + .addComponentParameter("id" , id ) + .addComponentParameter("limit" , limit , 10) + .addComponentParameter("skip" , skip , 0) + .addComponentParameter("period", period, Period.DAY) + .addComponentParameter("social", social, false) + .addComponentParameter("tag" , tag , null) + .addComponentParameter("user" , user , null); + } + + // ------------------------------------------------------------------------------------------- + // POST SCORE + // ------------------------------------------------------------------------------------------- + + public function postScore(id:Int, value:Int, tag:String = null):Call { + + return new Call(_core, "ScoreBoard.postScore", true, true) + .addComponentParameter("id" , id) + .addComponentParameter("value", value) + .addComponentParameter("tag" , tag , null); + } + + /*function onScorePosted(response:Response):Void { + + if (!response.result.success) + return; + + allById = new IntMap(); + + //createBoard(data.data.scoreBoard).parseScores(data.data.scores); + }*/ + + inline function createBoard(data:Dynamic):ScoreBoard { + + var board = new ScoreBoard(_core, data); + _core.logVerbose('created $board'); + + allById.set(board.id, board); + + return board; + } +} + +@:enum +abstract Period(String) to String from String{ + + /** Indicates scores are from the current day. */ + var DAY = "D"; + /** Indicates scores are from the current week. */ + var WEEK = "W"; + /** Indicates scores are from the current month. */ + var MONTH = "M"; + /** Indicates scores are from the current year. */ + var YEAR = "Y"; + /** Indicates scores are from all-time. */ + var ALL = "A"; +} \ No newline at end of file diff --git a/source/io/newgrounds/crypto/Cipher.hx b/source/io/newgrounds/crypto/Cipher.hx new file mode 100644 index 00000000..2f4c0075 --- /dev/null +++ b/source/io/newgrounds/crypto/Cipher.hx @@ -0,0 +1,8 @@ +package io.newgrounds.crypto; + +@:enum +abstract Cipher(String) to String{ + var NONE = "none"; + var AES_128 = "aes128"; + var RC4 = "rc4"; +} \ No newline at end of file diff --git a/source/io/newgrounds/crypto/EncryptionFormat.hx b/source/io/newgrounds/crypto/EncryptionFormat.hx new file mode 100644 index 00000000..6e8f17fd --- /dev/null +++ b/source/io/newgrounds/crypto/EncryptionFormat.hx @@ -0,0 +1,7 @@ +package io.newgrounds.crypto; + +@:enum +abstract EncryptionFormat(String) to String { + var BASE_64 = "base64"; + var HEX = "hex"; +} \ No newline at end of file diff --git a/source/io/newgrounds/crypto/Rc4.hx b/source/io/newgrounds/crypto/Rc4.hx new file mode 100644 index 00000000..54dafa74 --- /dev/null +++ b/source/io/newgrounds/crypto/Rc4.hx @@ -0,0 +1,68 @@ +package io.newgrounds.crypto; + +import haxe.io.Bytes; + +/** + * The following was straight-up ganked from https://github.com/iskolbin/rc4hx + * + * You da real MVP iskolbin... + * + * The MIT License (MIT) + * + * Copyright (c) 2015 iskolbin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. +**/ +class Rc4 { + var perm = Bytes.alloc( 256 ); + var index1: Int = 0; + var index2: Int = 0; + + public function new( key: Bytes ) { + for ( i in 0...256 ) { + perm.set( i, i ); + } + + var j: Int = 0; + for ( i in 0...256 ) { + j = ( j + perm.get( i ) + key.get( i % key.length )) % 256; + swap( i, j ); + } + } + + inline function swap( i: Int, j: Int ): Void { + var temp = perm.get( i ); + perm.set( i, perm.get( j )); + perm.set( j, temp ); + } + + public function crypt( input: Bytes ): Bytes { + var output = Bytes.alloc( input.length ); + + for ( i in 0...input.length ) { + index1 = ( index1 + 1 ) % 256; + index2 = ( index2 + perm.get( index1 )) % 256; + swap( index1, index2 ); + var j = ( perm.get( index1 ) + perm.get( index2 )) % 256; + output.set( i, input.get( i ) ^ perm.get( j )); + } + + return output; + } +} \ No newline at end of file diff --git a/source/io/newgrounds/objects/Error.hx b/source/io/newgrounds/objects/Error.hx new file mode 100644 index 00000000..6e82e4e2 --- /dev/null +++ b/source/io/newgrounds/objects/Error.hx @@ -0,0 +1,20 @@ +package io.newgrounds.objects; +class Error { + + public var code(default, null):Int; + public var message(default, null):String; + + public function new (message:String, code:Int = 0) { + + this.message = message; + this.code = code; + } + + public function toString():String { + + if (code > 0) + return '#$code - $message'; + + return message; + } +} diff --git a/source/io/newgrounds/objects/Medal.hx b/source/io/newgrounds/objects/Medal.hx new file mode 100644 index 00000000..3cecc7a6 --- /dev/null +++ b/source/io/newgrounds/objects/Medal.hx @@ -0,0 +1,118 @@ +package io.newgrounds.objects; + +import io.newgrounds.objects.events.Response; +import io.newgrounds.objects.events.Result.MedalUnlockResult; +import io.newgrounds.utils.Dispatcher; +import io.newgrounds.NGLite; + +class Medal extends Object { + + inline static public var EASY :Int = 1; + inline static public var MODERATE :Int = 2; + inline static public var CHALLENGING:Int = 3; + inline static public var DIFFICULT :Int = 4; + inline static public var BRUTAL :Int = 5; + + static var difficultyNames:Array = + [ "Easy" + , "Moderate" + , "Challenging" + , "Difficult" + , "Brutal" + ]; + // --- FROM SERVER + public var id (default, null):Int; + public var name (default, null):String; + public var description(default, null):String; + public var icon (default, null):String; + public var value (default, null):Int; + public var difficulty (default, null):Int; + public var secret (default, null):Bool; + public var unlocked (default, null):Bool; + // --- HELPERS + public var difficultyName(get, never):String; + + public var onUnlock:Dispatcher; + + public function new(core:NGLite, data:Dynamic = null):Void { + + onUnlock = new Dispatcher(); + + super(core, data); + } + + @:allow(io.newgrounds.NG) + override function parse(data:Dynamic):Void { + + var wasLocked = !unlocked; + + id = data.id; + name = data.name; + description = data.description; + icon = data.icon; + value = data.value; + difficulty = data.difficulty; + secret = data.secret == 1; + unlocked = data.unlocked; + + super.parse(data); + + if (wasLocked && unlocked) + onUnlock.dispatch(); + + } + + public function sendUnlock():Void { + + if (_core.sessionId == null) { + // --- Unlock regardless, show medal popup to encourage NG signup + unlocked = true; + onUnlock.dispatch(); + //TODO: save unlock in local save + } + + _core.calls.medal.unlock(id) + .addDataHandler(onUnlockResponse) + .send(); + } + + function onUnlockResponse(response:Response):Void { + + if (response.success && response.result.success) { + + parse(response.result.data.medal); + + // --- Unlock response doesn't include unlock=true, so parse won't change it. + if (!unlocked) { + + unlocked = true; + onUnlock.dispatch(); + } + } + } + + /** Locks the medal on the client and sends an unlock request, Server responds the same either way. */ + public function sendDebugUnlock():Void { + + if (NG.core.sessionId == null) { + + onUnlock.dispatch(); + + } else { + + unlocked = false; + + sendUnlock(); + } + } + + public function get_difficultyName():String { + + return difficultyNames[difficulty - 1]; + } + + public function toString():String { + + return 'Medal: $id@$name (${unlocked ? "unlocked" : "locked"}, $value pts, $difficultyName).'; + } +} \ No newline at end of file diff --git a/source/io/newgrounds/objects/Object.hx b/source/io/newgrounds/objects/Object.hx new file mode 100644 index 00000000..32abd0fe --- /dev/null +++ b/source/io/newgrounds/objects/Object.hx @@ -0,0 +1,33 @@ +package io.newgrounds.objects; + +import io.newgrounds.utils.Dispatcher; +import io.newgrounds.NGLite; + +class Object { + + var _core:NGLite; + + public var onUpdate(default, null):Dispatcher; + + public function new(core:NGLite, data:Dynamic = null) { + + this._core = core; + + onUpdate = new Dispatcher(); + + if (data != null) + parse(data); + } + + @:allow(io.newgrounds.NGLite) + function parse(data:Dynamic):Void { + + onUpdate.dispatch(); + } + + + public function destroy():Void { + + _core = null; + } +} \ No newline at end of file diff --git a/source/io/newgrounds/objects/Score.hx b/source/io/newgrounds/objects/Score.hx new file mode 100644 index 00000000..0eb69826 --- /dev/null +++ b/source/io/newgrounds/objects/Score.hx @@ -0,0 +1,17 @@ +package io.newgrounds.objects; + +/** We don't want to serialize scores since there's a bajillion of them. */ +typedef Score = { + + /** The value value in the format selected in your scoreboard settings. */ + var formatted_value:String; + + /** The tag attached to this value (if any). */ + var tag:String; + + /** The user who earned value. If this property is absent, the value belongs to the active user. */ + var user:User; + + /** The integer value of the value. */ + var value:Int; +} \ No newline at end of file diff --git a/source/io/newgrounds/objects/ScoreBoard.hx b/source/io/newgrounds/objects/ScoreBoard.hx new file mode 100644 index 00000000..1859b080 --- /dev/null +++ b/source/io/newgrounds/objects/ScoreBoard.hx @@ -0,0 +1,76 @@ +package io.newgrounds.objects; + +import io.newgrounds.components.ScoreBoardComponent.Period; +import io.newgrounds.objects.events.Response; +import io.newgrounds.objects.events.Result; +import io.newgrounds.objects.events.Result.ScoreResult; +import io.newgrounds.NGLite; + +class ScoreBoard extends Object { + + public var scores(default, null):Array; + + /** The numeric ID of the scoreboard.*/ + public var id(default, null):Int; + + /** The name of the scoreboard. */ + public var name(default, null):String; + + public function new(core:NGLite, data:Dynamic):Void {super(core, data); } + + override function parse(data:Dynamic):Void { + + id = data.id; + name = data.name; + + super.parse(data); + } + + /** + * Fetches score data from the server, this removes all of the existing scores cached + * + * We don't unify the old and new scores because a user's rank or score may change between requests + */ + public function requestScores + ( limit :Int = 10 + , skip :Int = 0 + , period:Period = Period.ALL + , social:Bool = false + , tag :String = null + , user :Dynamic = null + ):Void { + + _core.calls.scoreBoard.getScores(id, limit, skip, period, social, tag, user) + .addDataHandler(onScoresReceived) + .send(); + } + + function onScoresReceived(response:Response):Void { + + if (!response.success || !response.result.success) + return; + + scores = response.result.data.scores; + _core.logVerbose('received ${scores.length} scores'); + + onUpdate.dispatch(); + } + + public function postScore(value :Int, tag:String = null):Void { + + _core.calls.scoreBoard.postScore(id, value, tag) + .addDataHandler(onScorePosted) + .send(); + } + + function onScorePosted(response:Response):Void { + + + } + + public function toString():String { + + return 'ScoreBoard: $id@$name'; + } + +} \ No newline at end of file diff --git a/source/io/newgrounds/objects/Session.hx b/source/io/newgrounds/objects/Session.hx new file mode 100644 index 00000000..887df792 --- /dev/null +++ b/source/io/newgrounds/objects/Session.hx @@ -0,0 +1,65 @@ +package io.newgrounds.objects; + +class Session extends Object { + + /** If true, the session_id is expired. Use App.startSession to get a new one.*/ + public var expired(default, null):Bool; + + /** A unique session identifier */ + public var id(default, null):String; + + /** If the session has no associated user but is not expired, this property will provide a URL that can be used to sign the user in. */ + public var passportUrl(default, null):String; + + /** If true, the user would like you to remember their session id. */ + public var remember(default, null):Bool; + + /** If the user has not signed in, or granted access to your app, this will be null */ + public var user(default, null):User; + + //TODO:desciption + public var status(get, never):SessionStatus; + + public function new(core:NGLite, data:Dynamic = null) { super(core, data); } + + override public function parse(data:Dynamic):Void { + + id = data.id; + expired = data.expired; + passportUrl = data.passport_url; + remember = data.remember; + + // --- KEEP THE SAME INSTANCE + if (user == null) + user = data.user; + // TODO?: update original user instance with new data. (probly not) + + super.parse(data); + } + + public function get_status():SessionStatus { + + if (expired || id == null || id == "") + return SessionStatus.SESSION_EXPIRED; + + if (user != null && user.name != null && user.name != "") + return SessionStatus.USER_LOADED; + + return SessionStatus.REQUEST_LOGIN; + } + + public function expire():Void { + + expired = true; + id = null; + user = null; + } +} + +@:enum +abstract SessionStatus(String) { + + var SESSION_EXPIRED = "session-expired"; + var REQUEST_LOGIN = "request-login"; + var USER_LOADED = "user-loaded"; +} \ No newline at end of file diff --git a/source/io/newgrounds/objects/User.hx b/source/io/newgrounds/objects/User.hx new file mode 100644 index 00000000..1ff7ce00 --- /dev/null +++ b/source/io/newgrounds/objects/User.hx @@ -0,0 +1,19 @@ +package io.newgrounds.objects; + +typedef User = { + + /** The user's icon images. */ + var icons:UserIcons; + + /** The user's numeric ID. */ + var id:Int; + + /** The user's textual name. */ + var name:String; + + /** Returns true if the user has a Newgrounds Supporter upgrade. */ + var supporter:Bool; + + /** The user's NG profile url. */ + var url:String; +} diff --git a/source/io/newgrounds/objects/UserIcons.hx b/source/io/newgrounds/objects/UserIcons.hx new file mode 100644 index 00000000..b5e56b22 --- /dev/null +++ b/source/io/newgrounds/objects/UserIcons.hx @@ -0,0 +1,14 @@ +package io.newgrounds.objects; + +typedef UserIcons = { + + /**The URL of the user's large icon. */ + var large:String; + + /** The URL of the user's medium icon. */ + var medium:String; + + /** The URL of the user's small icon. */ + var small:String; +} + diff --git a/source/io/newgrounds/objects/events/Response.hx b/source/io/newgrounds/objects/events/Response.hx new file mode 100644 index 00000000..107dc2be --- /dev/null +++ b/source/io/newgrounds/objects/events/Response.hx @@ -0,0 +1,43 @@ +package io.newgrounds.objects.events; + +import io.newgrounds.objects.events.Result.ResultBase; +import haxe.Json; +import io.newgrounds.objects.Error; + +typedef DebugResponse = { + + var exec_time:Int; + var input:Dynamic; +} + +class Response { + + public var success(default, null):Bool; + public var error(default, null):Error; + public var debug(default, null):DebugResponse; + public var result(default, null):Result; + + public function new (core:NGLite, reply:String) { + + var data:Dynamic; + + try { + data = Json.parse(reply); + + } catch (e:Dynamic) { + + data = Json.parse('{"success":false,"error":{"message":"${Std.string(reply)}","code":0}}'); + } + + success = data.success; + debug = data.debug; + + if (!success) { + error = new Error(data.error.message, data.error.code); + core.logError('Call unseccessful: $error'); + return; + } + + result = new Result(core, data.result); + } +} diff --git a/source/io/newgrounds/objects/events/Result.hx b/source/io/newgrounds/objects/events/Result.hx new file mode 100644 index 00000000..eaf87261 --- /dev/null +++ b/source/io/newgrounds/objects/events/Result.hx @@ -0,0 +1,109 @@ +package io.newgrounds.objects.events; + +class Result { + + public var echo(default, null):String; + public var component(default, null):String; + + public var data(default, null):T; + public var success(default, null):Bool; + public var debug(default, null):Bool; + public var error(default, null):Error; + + public function new(core:NGLite, data:Dynamic) { + + echo = data.echo; + component = data.component; + + data = data.data; + success = data.success; + debug = data.debug; + + if(!data.success) { + + error = new Error(data.error.message, data.error.code); + core.logError('$component fail: $error'); + + } else + this.data = data; + } +} + +typedef ResultBase = { }; + +typedef SessionResult = { + > ResultBase, + + var session:Dynamic; +} + +typedef GetHostResult = { + > ResultBase, + + var host_approved:Bool; +} + +typedef GetCurrentVersionResult = { + > ResultBase, + + var current_version:String; + var client_deprecated:Bool; +} + +typedef LogEventResult = { + > ResultBase, + + var event_name:String; +} + +typedef GetDateTimeResult = { + > ResultBase, + + var datetime:String; +} + +typedef GetVersionResult = { + > ResultBase, + + var version:String; +} + +typedef PingResult = { + > ResultBase, + + var pong:String; +} + +typedef MedalListResult = { + > ResultBase, + + var medals:Array; +} + +typedef MedalUnlockResult = { + > ResultBase, + + var medal_score:String; + var medal:Dynamic; +} + +typedef ScoreBoardResult = { + > ResultBase, + + var scoreboards:Array; +} + +typedef ScoreResult = { + > ResultBase, + + var scores:Array; + var scoreboard:Dynamic; +} + +typedef PostScoreResult = { + > ResultBase, + + var tag:String; + var scoreboard:Dynamic; + var score:Score; +} \ No newline at end of file diff --git a/source/io/newgrounds/swf/LoadingBar.hx b/source/io/newgrounds/swf/LoadingBar.hx new file mode 100644 index 00000000..9c7c590a --- /dev/null +++ b/source/io/newgrounds/swf/LoadingBar.hx @@ -0,0 +1,23 @@ +package io.newgrounds.swf; + +import openfl.display.MovieClip; + +class LoadingBar extends MovieClip { + + public var bar(default, null):MovieClip; + + public function new() { + super(); + + setProgress(0.0); + } + + /** + * + * @param value The ratio of bytes loaded to bytes total + */ + public function setProgress(value:Float):Void { + + bar.gotoAndStop(1 + Std.int(value * (bar.totalFrames - 1))); + } +} diff --git a/source/io/newgrounds/swf/MedalPopup.hx b/source/io/newgrounds/swf/MedalPopup.hx new file mode 100644 index 00000000..f55ef426 --- /dev/null +++ b/source/io/newgrounds/swf/MedalPopup.hx @@ -0,0 +1,151 @@ +package io.newgrounds.swf; + +import io.newgrounds.swf.common.BaseAsset; +import io.newgrounds.objects.Medal; + +import openfl.text.TextFieldAutoSize; +import openfl.text.TextField; +import openfl.display.DisplayObject; +import openfl.display.Loader; +import openfl.display.MovieClip; +import openfl.net.URLRequest; +import openfl.events.Event; + +class MedalPopup extends BaseAsset { + + static inline var FRAME_HIDDEN:String = "hidden"; + static inline var FRAME_MEDAL_UNLOCKED:String = "medalUnlocked"; + static inline var FRAME_INTRO_COMPLETE:String = "introComplete"; + static inline var FRAME_UNLOCK_COMPLETE:String = "unlockComplete"; + static inline var MIN_TEXT_SIZE:Int = 12; + + public var medalIcon(default, null):MovieClip; + public var medalName(default, null):MovieClip; + public var medalPoints(default, null):MovieClip; + + public var alwaysOnTop:Bool; + #if !ng_lite + public var requiresSession:Bool; + #end + + var _animQueue = new ArrayVoid>(); + var _scrollSpeed:Float; + + public function new() { + super(); + + mouseEnabled = false; + mouseChildren = false; + + hide(); + addFrameScript(totalFrames - 1, onUnlockAnimComplete); + } + + function hide():Void { + + visible = false; + gotoAndStop(FRAME_HIDDEN); + } + + #if !ng_lite + override function onReady():Void { + super.onReady(); + + if (NG.core.medals != null) + onMedalsLoaded(); + else + NG.core.onLogin.addOnce(NG.core.requestMedals.bind(onMedalsLoaded)); + } + + function onMedalsLoaded():Void { + + for (medal in NG.core.medals) + medal.onUnlock.add(onMedalOnlock.bind(medal)); + } + + function onMedalOnlock(medal:Medal):Void { + + if (requiresSession && !NG.core.loggedIn) + return; + + var loader = new Loader(); + loader.load(new URLRequest(medal.icon)); + + playAnim(loader, medal.name, medal.value); + } + + #end + + public function playAnim(icon:DisplayObject, name:String, value:Int):Void { + + if (currentLabel == FRAME_HIDDEN) + playNextAnim(icon, name, value); + else + _animQueue.push(playNextAnim.bind(icon, name, value)); + } + + function playNextAnim(icon:DisplayObject, name:String, value:Int):Void { + + visible = true; + gotoAndPlay(FRAME_MEDAL_UNLOCKED); + + if (alwaysOnTop && parent != null) { + + parent.setChildIndex(this, parent.numChildren - 1); + } + + while(medalIcon.numChildren > 0) + medalIcon.removeChildAt(0); + + cast(medalPoints.getChildByName("field"), TextField).text = Std.string(value); + + var field:TextField = cast medalName.getChildByName("field"); + field.autoSize = TextFieldAutoSize.LEFT; + field.x = 0; + field.text = ""; + var oldWidth = medalName.width; + field.text = name; + + _scrollSpeed = 0; + if (field.width > oldWidth + 4) { + + field.x = oldWidth + 4; + initScroll(field); + } + + medalIcon.addChild(icon); + } + + function initScroll(field:TextField):Void { + //TODO: Find out why scrollrect didn't work + + var animDuration = 0; + + for (frame in currentLabels){ + + if (frame.name == FRAME_INTRO_COMPLETE ) + animDuration -= frame.frame; + else if (frame.name == FRAME_UNLOCK_COMPLETE) + animDuration += frame.frame; + } + + _scrollSpeed = (field.width + field.x + 4) / animDuration; + field.addEventListener(Event.ENTER_FRAME, updateScroll); + } + + function updateScroll(e:Event):Void{ + + if (currentLabel == FRAME_INTRO_COMPLETE) + cast (e.currentTarget, TextField).x -= _scrollSpeed; + } + + function onUnlockAnimComplete():Void { + + cast (medalName.getChildByName("field"), TextField).removeEventListener(Event.ENTER_FRAME, updateScroll); + + if (_animQueue.length == 0) + hide(); + else + (_animQueue.shift())(); + } +} diff --git a/source/io/newgrounds/swf/ScoreBrowser.hx b/source/io/newgrounds/swf/ScoreBrowser.hx new file mode 100644 index 00000000..c4232c22 --- /dev/null +++ b/source/io/newgrounds/swf/ScoreBrowser.hx @@ -0,0 +1,250 @@ +package io.newgrounds.swf; + +import openfl.events.Event; +import io.newgrounds.swf.common.DropDown; +import io.newgrounds.objects.Score; +import io.newgrounds.objects.events.Result.ScoreBoardResult; +import io.newgrounds.objects.events.Result.ScoreResult; +import io.newgrounds.objects.events.Response; +import io.newgrounds.swf.common.BaseAsset; +import io.newgrounds.swf.common.Button; +import io.newgrounds.components.ScoreBoardComponent.Period; + +import openfl.display.MovieClip; +import openfl.text.TextField; + +class ScoreBrowser extends BaseAsset { + + public var prevButton (default, null):MovieClip; + public var nextButton (default, null):MovieClip; + public var reloadButton (default, null):MovieClip; + public var listBox (default, null):MovieClip; + public var loadingIcon (default, null):MovieClip; + public var errorIcon (default, null):MovieClip; + public var scoreContainer(default, null):MovieClip; + public var titleField (default, null):TextField; + public var pageField (default, null):TextField; + + public var period(get, set):Period; + function get_period():Period { return _periodDropDown.value; } + function set_period(value:Period):Period { return _periodDropDown.value = value; } + + public var title(get, set):String; + function get_title():String { return titleField.text; } + function set_title(value:String):String { return titleField.text = value; } + + public var tag(default, set):String; + function set_tag(value:String):String { + + if (this.tag != value) { + + this.tag = value; + delayReload(); + } + + return value; + } + + public var social(default, set):Bool; + function set_social(value:Bool):Bool { + + if (this.social != value) { + + this.social = value; + delayReload(); + } + + return value; + } + + public var boardId(default, set):Int; + function set_boardId(value:Int):Int { + + _boardIDSet = true; + + if (this.boardId != value) { + + this.boardId = value; + delayReload(); + } + + return value; + } + + public var page(default, set):Int; + function set_page(value:Int):Int { + + if (this.page != value) { + + this.page = value; + delayReload(); + } + + return value; + } + + var _scores:Array; + var _limit:Int = 0; + var _periodDropDown:DropDown; + var _boardIDSet:Bool; + + public function new() { super(); } + + override function setDefaults():Void { + super.setDefaults(); + + boardId = -1; + _boardIDSet = false; + + scoreContainer.visible = false; + loadingIcon.visible = false; + reloadButton.visible = false; + errorIcon.visible = false; + errorIcon.addFrameScript(errorIcon.totalFrames - 1, errorIcon.stop); + + //TODO: prevent memory leaks? + new Button(prevButton, onPrevClick); + new Button(nextButton, onNextClick); + new Button(reloadButton, reload); + _periodDropDown = new DropDown(listBox, delayReload); + _periodDropDown.addItem("Current day" , Period.DAY ); + _periodDropDown.addItem("Current week" , Period.WEEK ); + _periodDropDown.addItem("Current month", Period.MONTH); + _periodDropDown.addItem("Current year" , Period.YEAR ); + _periodDropDown.addItem("All time" , Period.ALL ); + _periodDropDown.value = Period.ALL; + + _scores = new Array(); + while(true) { + + var score:MovieClip = cast scoreContainer.getChildByName('score${_scores.length}'); + if (score == null) + break; + + new Button(score); + _scores.push(score); + } + + _limit = _scores.length; + } + + override function onReady():Void { + super.onReady(); + + if (boardId == -1 && !_boardIDSet) { + + #if ng_lite + NG.core.calls.scoreBoard.getBoards() + .addDataHandler(onBoardsRecieved) + .queue(); + #else + if (NG.core.scoreBoards != null) + onBoardsLoaded(); + else + NG.core.requestScoreBoards(onBoardsLoaded); + #end + } + + reload(); + } + + #if ng_lite + function onBoardsRecieved(response:Response):Void { + + if (response.success && response.result.success) { + + for (board in response.result.data.scoreboards) { + + NG.core.log('No boardId specified defaulting to ${board.name}'); + boardId = board.id; + return; + } + } + } + #else + function onBoardsLoaded():Void { + + for (board in NG.core.scoreBoards) { + + NG.core.log('No boardId specified defaulting to ${board.name}'); + boardId = board.id; + return; + } + } + #end + + /** Used internally to avoid multiple server requests from various property changes in a small time-frame. **/ + function delayReload():Void { + + addEventListener(Event.EXIT_FRAME, onDelayComplete); + } + + function onDelayComplete(e:Event):Void { reload(); } + + public function reload():Void { + removeEventListener(Event.EXIT_FRAME, onDelayComplete); + + errorIcon.visible = false; + scoreContainer.visible = false; + pageField.text = 'page ${page + 1}'; + + if (_coreReady && boardId != -1 && _limit > 0 && period != null) { + + loadingIcon.visible = true; + + NG.core.calls.scoreBoard.getScores(boardId, _limit, _limit * page, period, social, tag) + .addDataHandler(onScoresReceive) + .send(); + } + } + + function onScoresReceive(response:Response):Void { + + loadingIcon.visible = false; + + if (response.success && response.result.success) { + + scoreContainer.visible = true; + + var i = _limit; + while(i > 0) { + i--; + + if (i < response.result.data.scores.length) + drawScore(i, response.result.data.scores[i], _scores[i]); + else + drawScore(i, null, _scores[i]); + } + + } else { + + errorIcon.visible = true; + errorIcon.gotoAndPlay(1); + reloadButton.visible = true; + } + } + + inline function drawScore(rank:Int, score:Score, asset:MovieClip):Void { + + if (score == null) + asset.visible = false; + else { + + asset.visible = true; + cast (asset.getChildByName("nameField" ), TextField).text = score.user.name; + cast (asset.getChildByName("scoreField"), TextField).text = score.formatted_value; + cast (asset.getChildByName("rankField" ), TextField).text = Std.string(rank + 1); + } + } + + function onPrevClick():Void { + + if (page > 0) + page--; + } + + function onNextClick():Void { + + page++; + } +} diff --git a/source/io/newgrounds/swf/common/BaseAsset.hx b/source/io/newgrounds/swf/common/BaseAsset.hx new file mode 100644 index 00000000..da1f6126 --- /dev/null +++ b/source/io/newgrounds/swf/common/BaseAsset.hx @@ -0,0 +1,35 @@ +package io.newgrounds.swf.common; + +import openfl.events.Event; +import openfl.display.MovieClip; + +class BaseAsset extends MovieClip { + + var _coreReady:Bool = false; + + public function new() { + super(); + + setDefaults(); + + if (stage != null) + onAdded(null); + else + addEventListener(Event.ADDED_TO_STAGE, onAdded); + } + + function setDefaults():Void { } + + function onAdded(e:Event):Void { + + if (NG.core != null) + onReady(); + else + NG.onCoreReady.add(onReady); + } + + function onReady():Void { + + _coreReady = true; + } +} diff --git a/source/io/newgrounds/swf/common/Button.hx b/source/io/newgrounds/swf/common/Button.hx new file mode 100644 index 00000000..5f461752 --- /dev/null +++ b/source/io/newgrounds/swf/common/Button.hx @@ -0,0 +1,151 @@ +package io.newgrounds.swf.common; + +import openfl.display.Stage; +import openfl.events.Event; +import openfl.events.MouseEvent; +import openfl.display.MovieClip; + +class Button { + + var _enabled:Bool; + public var enabled(get, set):Bool; + function get_enabled():Bool { return _enabled; } + function set_enabled(value:Bool):Bool { + + if (value != _enabled) { + + _enabled = value; + updateEnabled(); + } + + return value; + } + + public var onClick:Void->Void; + public var onOver:Void->Void; + public var onOut:Void->Void; + + var _target:MovieClip; + var _down:Bool; + var _over:Bool; + var _foundLabels:Array; + + public function new(target:MovieClip, onClick:Void->Void = null, onOver:Void->Void = null, onOut:Void->Void = null) { + + _target = target; + this.onClick = onClick; + this.onOver = onOver; + this.onOut = onOut; + + _foundLabels = new Array(); + for (label in _target.currentLabels) + _foundLabels.push(label.name); + + _target.stop(); + _target.addEventListener(Event.ADDED_TO_STAGE, onAdded); + if (target.stage != null) + onAdded(null); + + enabled = true; + } + + function onAdded(e:Event):Void { + + var stage = _target.stage; + stage.addEventListener(MouseEvent.MOUSE_UP, mouseHandler); + _target.addEventListener(MouseEvent.MOUSE_OVER, mouseHandler); + _target.addEventListener(MouseEvent.MOUSE_OUT, mouseHandler); + _target.addEventListener(MouseEvent.MOUSE_DOWN, mouseHandler); + _target.addEventListener(MouseEvent.CLICK, mouseHandler); + + function selfRemoveEvent(e:Event):Void { + + _target.removeEventListener(Event.REMOVED_FROM_STAGE, selfRemoveEvent); + onRemove(e, stage); + } + _target.addEventListener(Event.REMOVED_FROM_STAGE, selfRemoveEvent); + } + + function onRemove(e:Event, stage:Stage):Void { + + stage.removeEventListener(MouseEvent.MOUSE_UP, mouseHandler); + _target.removeEventListener(MouseEvent.MOUSE_OVER, mouseHandler); + _target.removeEventListener(MouseEvent.MOUSE_OUT, mouseHandler); + _target.removeEventListener(MouseEvent.MOUSE_DOWN, mouseHandler); + _target.removeEventListener(MouseEvent.CLICK, mouseHandler); + } + + function mouseHandler(event:MouseEvent):Void { + + switch(event.type) { + + case MouseEvent.MOUSE_OVER: + + _over = true; + + if (onOver != null) + onOver(); + + case MouseEvent.MOUSE_OUT: + + _over = false; + + if (onOut != null) + onOut(); + + case MouseEvent.MOUSE_DOWN: + + _down = true; + + case MouseEvent.MOUSE_UP: + + _down = false; + + case MouseEvent.CLICK: + + if (enabled && onClick != null) + onClick(); + } + updateState(); + } + + function updateEnabled():Void { + + updateState(); + + _target.useHandCursor = enabled; + _target.buttonMode = enabled; + } + + function updateState():Void { + + var state = determineState(); + + if (_target.currentLabel != state && _foundLabels.indexOf(state) != -1) + _target.gotoAndStop(state); + } + + function determineState():String { + + if (enabled) { + + if (_over) + return _down ? "down" : "over"; + + return "up"; + + } + return "disabled"; + } + + public function destroy():Void { + + _target.removeEventListener(Event.ADDED_TO_STAGE, onAdded); + + _target = null; + onClick = null; + onOver = null; + onOut = null; + _foundLabels = null; + } +} diff --git a/source/io/newgrounds/swf/common/DropDown.hx b/source/io/newgrounds/swf/common/DropDown.hx new file mode 100644 index 00000000..a882cf39 --- /dev/null +++ b/source/io/newgrounds/swf/common/DropDown.hx @@ -0,0 +1,88 @@ +package io.newgrounds.swf.common; + + +import haxe.ds.StringMap; + +import openfl.display.MovieClip; +import openfl.display.Sprite; +import openfl.text.TextField; + +class DropDown { + + public var value(default, set):String; + function set_value(v:String):String { + + if (this.value == v) + return v; + + this.value = v; + _selectedLabel.text = _values.get(v); + + if (_onChange != null) + _onChange(); + + return v; + } + + var _choiceContainer:Sprite; + var _selectedLabel:TextField; + var _onChange:Void->Void; + var _values:StringMap; + var _unusedChoices:Array; + + public function new(target:MovieClip, label:String = "", onChange:Void->Void = null) { + + _onChange = onChange; + + _selectedLabel = cast cast(target.getChildByName("currentItem"), MovieClip).getChildByName("label"); + _selectedLabel.text = label; + + _values = new StringMap(); + + new Button(cast target.getChildByName("button"), onClickExpand); + new Button(cast target.getChildByName("currentItem"), onClickExpand); + _choiceContainer = new Sprite(); + _choiceContainer.visible = false; + target.addChild(_choiceContainer); + + _unusedChoices = new Array(); + while(true) { + + var item:MovieClip = cast target.getChildByName('item${_unusedChoices.length}'); + if (item == null) + break; + + target.removeChild(item); + _unusedChoices.push(item); + } + } + + public function addItem(name:String, value:String):Void { + + _values.set(value, name); + + if (_unusedChoices.length == 0) { + + NG.core.logError('cannot create another dropBox item max=${_choiceContainer.numChildren}'); + return; + } + + var button = _unusedChoices.shift(); + cast(button.getChildByName("label"), TextField).text = name; + _choiceContainer.addChild(button); + + new Button(button, onChoiceClick.bind(value)); + } + + function onClickExpand():Void { + + _choiceContainer.visible = !_choiceContainer.visible; + } + + function onChoiceClick(name:String):Void { + + value = name; + + _choiceContainer.visible = false; + } +} \ No newline at end of file diff --git a/source/io/newgrounds/utils/AsyncHttp.hx b/source/io/newgrounds/utils/AsyncHttp.hx new file mode 100644 index 00000000..4fbe1438 --- /dev/null +++ b/source/io/newgrounds/utils/AsyncHttp.hx @@ -0,0 +1,203 @@ +package io.newgrounds.utils; + +import io.newgrounds.NGLite; + +import haxe.Http; +import haxe.Timer; + +#if neko +import neko.vm.Thread; +#elseif java +import java.vm.Thread; +#elseif cpp +import cpp.vm.Thread; +#end + +/** + * Uses Threading to turn hxcpp's synchronous http requests into asynchronous processes + * + * @author GeoKureli + */ +class AsyncHttp { + + inline static var PATH:String = "https://newgrounds.io/gateway_v3.php"; + + static public function send + ( core:NGLite + , data:String + , onData:String->Void + , onError:String->Void + , onStatus:Int->Void + ) { + + core.logVerbose('sending: $data'); + + #if (neko || java || cpp) + sendAsync(core, data, onData, onError, onStatus); + #else + sendSync(core, data, onData, onError, onStatus); + #end + } + + static function sendSync + ( core:NGLite + , data:String + , onData:String->Void + , onError:String->Void + , onStatus:Int->Void + ):Void { + + var http = new Http(PATH); + http.setParameter("input", data); + http.onData = onData; + http.onError = onError; + http.onStatus = onStatus; + // #if js http.async = async; #end + http.request(true); + } + + #if (neko || java || cpp) + static var _deadPool:Array = []; + static var _livePool:Array = []; + static var _map:Map = new Map(); + static var _timer:Timer; + + static var _count:Int = 0; + + var _core:NGLite; + var _key:Int; + var _onData:String->Void; + var _onError:String->Void; + var _onStatus:Int->Void; + var _worker:Thread; + + public function new (core:NGLite) { + + _core = core; + _worker = Thread.create(sendThreaded); + _key = _count++; + _map[_key] = this; + _core.logVerbose('async http created: $_key'); + } + + function start(data:String, onData:String->Void, onError:String->Void, onStatus:Int->Void) { + + _core.logVerbose('async http started: $_key'); + + if (_livePool.length == 0) + startTimer(); + + _deadPool.remove(this); + _livePool.push(this); + + _onData = onData; + _onError = onError; + _onStatus = onStatus; + _worker.sendMessage({ source:Thread.current(), args:data, key:_key, core:_core }); + } + + function handleMessage(data:ReplyData):Void { + + _core.logVerbose('handling message: $_key'); + + if (data.status != null) { + + _core.logVerbose('\t- status: ${data.status}'); + _onStatus(cast data.status); + return; + } + + var tempFunc:Void->Void; + if (data.data != null) { + + _core.logVerbose('\t- data'); + tempFunc = _onData.bind(data.data); + + } else { + + _core.logVerbose('\t- error'); + tempFunc = _onError.bind(data.error); + } + + cleanUp(); + // Delay the call until destroy so that we're more likely to use a single + // thread on daisy-chained calls + tempFunc(); + } + + inline function cleanUp():Void { + + _onData = null; + _onError = null; + + _deadPool.push(this); + _livePool.remove(this); + + if (_livePool.length == 0) + stopTimer(); + } + + static function sendAsync + ( core:NGLite + , data:String + , onData:String->Void + , onError:String->Void + , onStatus:Int->Void + ):Void { + + var http:AsyncHttp; + if (_deadPool.length == 0) + http = new AsyncHttp(core); + else + http = _deadPool[0]; + + http.start(data, onData, onError, onStatus); + } + + static function startTimer():Void { + + if (_timer != null) + return; + + _timer = new Timer(1000 / 60.0); + _timer.run = update; + } + + static function stopTimer():Void { + + _timer.stop(); + _timer = null; + } + + static public function update():Void { + + var message:ReplyData = cast Thread.readMessage(false); + if (message != null) + _map[message.key].handleMessage(message); + } + + static function sendThreaded():Void { + + while(true) { + + var data:LoaderData = cast Thread.readMessage(true); + data.core.logVerbose('start message received: ${data.key}'); + + sendSync + ( data.core + , data.args + , function(reply ) { data.source.sendMessage({ key:data.key, data :reply }); } + , function(error ) { data.source.sendMessage({ key:data.key, error :error }); } + , function(status) { data.source.sendMessage({ key:data.key, status:status }); } + ); + } + } + + #end +} + + +#if (neko || java || cpp) +typedef LoaderData = { source:Thread, key:Int, args:String, core:NGLite }; +typedef ReplyData = { key:Int, ?data:String, ?error:String, ?status:Null }; +#end \ No newline at end of file diff --git a/source/io/newgrounds/utils/Dispatcher.hx b/source/io/newgrounds/utils/Dispatcher.hx new file mode 100644 index 00000000..699da01d --- /dev/null +++ b/source/io/newgrounds/utils/Dispatcher.hx @@ -0,0 +1,118 @@ +package io.newgrounds.utils; + +/** + * Basically shitty signals, but I didn't want to have external references. +**/ +class Dispatcher { + + var _list:ArrayVoid>; + var _once:ArrayVoid>; + + public function new() { + + _list = new ArrayVoid>(); + _once = new ArrayVoid>(); + } + + public function add(handler:Void->Void, once:Bool = false):Bool { + + if (_list.indexOf(handler) != -1) { + + // ---- REMOVE ONCE + if (!once && _once.indexOf(handler) != -1) + _once.remove(handler); + + return false; + } + + _list.unshift(handler); + if (once) + _once.unshift(handler); + + return true; + } + + inline public function addOnce(handler:Void->Void):Bool { + + return add(handler, true); + } + + public function remove(handler:Void->Void):Bool { + + _once.remove(handler); + return _list.remove(handler); + } + + public function dispatch():Void { + + var i = _list.length - 1; + while(i >= 0) { + + var handler = _list[i]; + + if (_once.remove(handler)) + _list.remove(handler); + + handler(); + + i--; + } + } +} + +class TypedDispatcher { + + var _list:ArrayVoid>; + var _once:ArrayVoid>; + + public function new() { + + _list = new ArrayVoid>(); + _once = new ArrayVoid>(); + } + + public function add(handler:T->Void, once:Bool = false):Bool { + + if (_list.indexOf(handler) != -1) { + + // ---- REMOVE ONCE + if (!once && _once.indexOf(handler) != -1) + _once.remove(handler); + + return false; + } + + _list.unshift(handler); + if (once) + _once.unshift(handler); + + return true; + } + + inline public function addOnce(handler:T->Void):Bool { + + return add(handler, true); + } + + public function remove(handler:T->Void):Bool { + + _once.remove(handler); + return _list.remove(handler); + } + + public function dispatch(arg:T):Void { + + var i = _list.length - 1; + while(i >= 0) { + + var handler = _list[i]; + + if (_once.remove(handler)) + _list.remove(handler); + + handler(arg); + + i--; + } + } +} \ No newline at end of file