├── flox ├── build │ ├── idea │ │ ├── .idea │ │ │ ├── .name │ │ │ ├── copyright │ │ │ │ └── profiles_settings.xml │ │ │ ├── scopes │ │ │ │ └── scope_settings.xml │ │ │ ├── encodings.xml │ │ │ ├── flexCompiler.xml │ │ │ ├── vcs.xml │ │ │ ├── misc.xml │ │ │ ├── modules.xml │ │ │ └── compiler.xml │ │ └── README.md │ └── ant │ │ ├── package-descriptions.xml │ │ ├── build.properties │ │ └── build.xml ├── .project ├── src │ └── com │ │ └── gamua │ │ └── flox │ │ ├── utils │ │ ├── parseBool.as │ │ ├── HttpMethod.as │ │ ├── formatString.as │ │ ├── registerClassAlias.as │ │ ├── setTimeout.as │ │ ├── createURL.as │ │ ├── describeType.as │ │ ├── execute.as │ │ ├── createUID.as │ │ ├── cloneObject.as │ │ ├── HttpStatus.as │ │ ├── DateUtil.as │ │ ├── Base64.as │ │ └── SHA256.as │ │ ├── flox_internal.as │ │ ├── Access.as │ │ ├── TimeScope.as │ │ ├── AuthenticationType.as │ │ ├── events │ │ └── QueueEvent.as │ │ ├── Authentication.as │ │ ├── Score.as │ │ ├── SharedObjectPool.as │ │ ├── PersistentQueue.as │ │ ├── PersistentStore.as │ │ ├── GameSession.as │ │ ├── Query.as │ │ └── Player.as ├── doc │ └── generate.sh ├── flox-as3.iml ├── .flexLibProperties └── .actionScriptProperties ├── tests ├── config │ ├── .gitignore │ └── live-server.xml ├── build │ └── ant │ │ └── build.xml ├── .project ├── src │ ├── Startup.as │ ├── com │ │ └── gamua │ │ │ └── flox │ │ │ ├── utils │ │ │ ├── CustomEntity.as │ │ │ └── makeHttpRequest.as │ │ │ ├── SharedObjectPoolTest.as │ │ │ ├── Constants.as │ │ │ ├── HeroTest.as │ │ │ ├── PersistentStoreTest.as │ │ │ ├── PersistentQueueTest.as │ │ │ ├── AnalyticsTest.as │ │ │ ├── RestServiceTest.as │ │ │ ├── UtilsTest.as │ │ │ ├── AccessTest.as │ │ │ └── ScoreTest.as │ ├── starling │ │ └── unit │ │ │ ├── TestGuiFull.as │ │ │ ├── TestGui.as │ │ │ ├── TestGuiEx.as │ │ │ ├── TestRunner.as │ │ │ └── UnitTest.as │ └── TestSuite.as ├── flox-as3-tests.iml └── .actionScriptProperties ├── .gitignore ├── LICENSE.md ├── README.md └── CHANGELOG.md /flox/build/idea/.idea/.name: -------------------------------------------------------------------------------- 1 | flox -------------------------------------------------------------------------------- /tests/config/.gitignore: -------------------------------------------------------------------------------- 1 | test-server.xml -------------------------------------------------------------------------------- /flox/build/idea/.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /flox/build/idea/.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /flox/build/idea/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /flox/build/idea/.idea/flexCompiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /flox/build/idea/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build and Release Folders 2 | bin/ 3 | bin-debug/ 4 | bin-release/ 5 | flox/doc/html/ 6 | 7 | # Flash Builder 8 | .settings/ 9 | .flexProperties 10 | .FlexUnitSettings 11 | 12 | # IntelliJ IDEA 13 | *.iws 14 | **/.idea/workspace.xml 15 | **/.idea/tasks.xml 16 | **/.idea/dictionaries 17 | 18 | # OS X folders 19 | .DS_Store 20 | 21 | -------------------------------------------------------------------------------- /flox/build/idea/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/build/ant/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /flox/build/idea/README.md: -------------------------------------------------------------------------------- 1 | This folder contains a hidden ".idea" directory with an IntelliJ-IDEA project. That project references all modules that are part of the Flox download (i.e. Flox itself and the unit tests). 2 | 3 | Somewhat surprisingly, the unit tests require the Starling Framework to run. So you need to download Starling and add it to IDEA as global library. -------------------------------------------------------------------------------- /flox/build/idea/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flox/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Flox-AS3 4 | 5 | 6 | 7 | 8 | 9 | com.adobe.flexbuilder.project.flexbuilder 10 | 11 | 12 | 13 | 14 | 15 | com.adobe.flexbuilder.project.aslibnature 16 | com.adobe.flexbuilder.project.actionscriptnature 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/config/live-server.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://www.flox.cc/api 4 | 5 | 6 | zYJqACazUYnqA9De 7 | 8 | 9 | qOSPgoS71azKwN1k 10 | 11 | 12 | 13 | gamua-unit-tests 14 | 150a1bb6-b33d-4eb3-8848-23051f200359 15 | 16 | 17 | 9LSCZS3yQlF1dGFr 18 | iamDM9f36SS3Uucp 19 | 20 | 21 | -------------------------------------------------------------------------------- /flox/build/ant/package-descriptions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Flox-AS3-Tests 4 | 5 | 6 | Flox-AS3 7 | Starling 8 | 9 | 10 | 11 | com.adobe.flexbuilder.project.flexbuilder 12 | 13 | 14 | 15 | 16 | 17 | com.adobe.flexbuilder.project.actionscriptnature 18 | 19 | 20 | -------------------------------------------------------------------------------- /flox/build/ant/build.properties: -------------------------------------------------------------------------------- 1 | # basic properties 2 | version = 1.4 3 | src.dir = ${basedir}/src 4 | deploy.dir = ${basedir}/bin 5 | doc.dir = ${basedir}/doc/html 6 | swf.version = 40 7 | 8 | # location of the AIR SDK 9 | airsdk.root = /Users/redge/Dropbox/Development/library/flash/air/air-30 10 | airsdk.bin = ${airsdk.root}/bin 11 | airsdk.lib = ${airsdk.root}/lib 12 | airsdk.config = ${airsdk.root}/frameworks/flex-config.xml 13 | airsdk.framework = ${airsdk.root}/frameworks 14 | 15 | # path to compiler jars 16 | asdoc = ${airsdk.lib}/legacy/asdoc.jar 17 | compc = ${airsdk.lib}/compc-cli.jar 18 | mxmlc = ${airsdk.lib}/mxmlc-cli.jar 19 | adt = ${airsdk.lib}/adt.jar 20 | -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/utils/parseBool.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2012 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox.utils 9 | { 10 | /** Parses a string containing a boolean value (yes/no, true/false, 1/0). */ 11 | public function parseBool(str:String):Boolean 12 | { 13 | var value:String = str.toLowerCase(); 14 | if (str == "true" || str == "yes") return true; 15 | else return false; 16 | } 17 | } -------------------------------------------------------------------------------- /flox/doc/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script creates a nice API reference documentation for the Flox source. 4 | # It uses the "ASDoc" tool that comes with the Flex SDK. 5 | # Adapt the ASDOC variable below so that it points to the correct path. 6 | 7 | if [ $# -ne 1 ] 8 | then 9 | echo "Usage: `basename $0` [version]" 10 | echo " (version like '1.0')" 11 | exit 1 12 | fi 13 | 14 | version=$1 15 | ASDOC="/Users/redge/Dropbox/Development/library/flash/air/air-28/bin/asdoc" 16 | 17 | "${ASDOC}" \ 18 | -doc-sources ../src \ 19 | -main-title "Flox AS3 Reference (v$version)" \ 20 | -window-title "Flox AS3 Reference" \ 21 | -package-description-file=../build/ant/package-descriptions.xml 22 | -output html 23 | -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/flox_internal.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2012 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox 9 | { 10 | /** 11 | * This namespace is used for undocumented APIs -- usually implementation 12 | * details -- which can't be private because they need to visible 13 | * to other classes. 14 | * 15 | * APIs in this namespace are completely unsupported and are likely to 16 | * change in future versions of Flox. 17 | */ 18 | public namespace flox_internal; 19 | } -------------------------------------------------------------------------------- /flox/build/idea/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/utils/HttpMethod.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2012 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox.utils 9 | { 10 | /** Provides a list of HTTP methods (verbs). */ 11 | public final class HttpMethod 12 | { 13 | /** @private */ 14 | public function HttpMethod() { throw new Error("This class cannot be instantiated."); } 15 | 16 | public static const GET:String = "get"; 17 | public static const POST:String = "post"; 18 | public static const PUT:String = "put"; 19 | public static const DELETE:String = "delete"; 20 | } 21 | } -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/utils/formatString.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2012 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox.utils 9 | { 10 | /** Formats a String in .Net-style, with curly braces ("{0}"). Does not support any 11 | * number formatting options yet. */ 12 | public function formatString(format:String, ...args):String 13 | { 14 | if (args.length == 1 && (args[0] is Array || args[0] is Vector)) 15 | args = args[0]; 16 | 17 | for (var i:int=0; i 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/src/com/gamua/flox/utils/CustomEntity.as: -------------------------------------------------------------------------------- 1 | package com.gamua.flox.utils 2 | { 3 | import com.gamua.flox.Entity; 4 | 5 | public class CustomEntity extends Entity 6 | { 7 | private var mName:String; 8 | private var mAge:int; 9 | private var mData:Object; 10 | private var mBirthday:Date; 11 | 12 | public var publicMember:String; 13 | 14 | public function CustomEntity(name:String="unknown", age:int=0) 15 | { 16 | mName = name; 17 | mAge = age; 18 | mData = { value: int(Math.random() * 1000) }; 19 | mBirthday = new Date(); 20 | publicMember = "undefined"; 21 | } 22 | 23 | protected override function onConflict(remoteEntity:Entity):void 24 | { 25 | var that:CustomEntity = remoteEntity as CustomEntity; 26 | this.age = Math.max(this.age, that.age); 27 | } 28 | 29 | public function get name():String { return mName; } 30 | public function set name(value:String):void { mName = value; } 31 | 32 | public function get age():int { return mAge; } 33 | public function set age(value:int):void { mAge = value; } 34 | 35 | public function get data():Object { return mData; } 36 | public function set data(value:Object):void { mData = value; } 37 | 38 | public function get birthday():Date { return mBirthday; } 39 | public function set birthday(value:Date):void { mBirthday = value; } 40 | } 41 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Simplified BSD License 2 | ====================== 3 | 4 | Copyright 2013 Gamua. All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this list of 10 | conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 13 | of conditions and the following disclaimer in the documentation and/or other materials 14 | provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY GAMUA "AS IS" AND ANY EXPRESS OR IMPLIED 17 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 18 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GAMUA OR 19 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 24 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | The views and conclusions contained in the software and documentation are those of the 27 | authors and should not be interpreted as representing official policies, either expressed 28 | or implied, of Gamua. 29 | -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/AuthenticationType.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2013 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox 9 | { 10 | /** A utility class providing strings of authentication types. This class contains all 11 | * types that are currently supported by Flox. It will be extended with additional types in 12 | * the future. */ 13 | public final class AuthenticationType 14 | { 15 | /** @private */ 16 | public function AuthenticationType() 17 | { 18 | throw new Error("This class cannot be instantiated."); 19 | } 20 | 21 | /** A guest account, i.e. the user is not authenticated at all. */ 22 | public static const GUEST:String = "guest"; 23 | 24 | /** Knowledge of a single identifier allows the player to login. */ 25 | public static const KEY:String = "key"; 26 | 27 | /** The user proves to have access to a certain e-mail address. */ 28 | public static const EMAIL:String = "email"; 29 | 30 | /** The user has registered an e-mail + password combination. */ 31 | public static const EMAIL_AND_PASSWORD:String = "emailPwd"; 32 | 33 | // public static const FACEBOOK:String = "facebook"; 34 | // public static const TWITTER:String = "twitter"; 35 | // public static const GAMUA:String = "gamua"; 36 | } 37 | } -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/utils/describeType.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2012 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox.utils 9 | { 10 | import flash.utils.describeType; 11 | import flash.utils.getQualifiedClassName; 12 | 13 | /** Produces an XML object that describes the ActionScript object named as the parameter of 14 | * the method. The results are a little different from the 'flash.utils' method with the 15 | * same name: passing an instance or its class produces the same results, and that result 16 | * is being cached. */ 17 | public function describeType(value:*):XML 18 | { 19 | var type:String = getQualifiedClassName(value); 20 | var description:XML = cache[type]; 21 | 22 | if (description == null) 23 | { 24 | // We always use the underlying class and return the factory element. 25 | // That way, the type description is always the same, no matter if you pass 26 | // the class or an instance of it. 27 | 28 | if (!(value is Class)) 29 | value = Object(value).constructor; 30 | 31 | description = XML(flash.utils.describeType(value).factory); 32 | cache[type] = description; 33 | } 34 | 35 | return description; 36 | } 37 | } 38 | 39 | import flash.utils.Dictionary; 40 | var cache:Dictionary = new Dictionary(); -------------------------------------------------------------------------------- /tests/src/com/gamua/flox/SharedObjectPoolTest.as: -------------------------------------------------------------------------------- 1 | package com.gamua.flox 2 | { 3 | import com.gamua.flox.utils.setTimeout; 4 | 5 | import flash.net.SharedObject; 6 | 7 | import starling.unit.UnitTest; 8 | 9 | public class SharedObjectPoolTest extends UnitTest 10 | { 11 | private static const NAME:String = "test"; 12 | 13 | public function testAccess(onComplete:Function):void 14 | { 15 | var interval:Number = 0.2; 16 | 17 | SharedObjectPool.startAutoCleanup(interval); 18 | 19 | var so1:SharedObject = SharedObjectPool.getObject(NAME); 20 | so1.data.value = "value"; 21 | 22 | var so2:SharedObject = SharedObjectPool.getObject(NAME); 23 | 24 | assertEqual(so1, so2); 25 | assertEqual(so1.data.value, so2.data.value); 26 | 27 | setTimeout(afterFirstCleanup, 300); 28 | setTimeout(afterSecondCleanup, 500); 29 | setTimeout(afterThirdCleanup, 700, onComplete); 30 | } 31 | 32 | private function afterFirstCleanup():void 33 | { 34 | assert(SharedObjectPool.contains(NAME)); 35 | var restored:SharedObject = SharedObjectPool.getObject(NAME); 36 | } 37 | 38 | private function afterSecondCleanup():void 39 | { 40 | assert(SharedObjectPool.contains(NAME)); 41 | } 42 | 43 | private function afterThirdCleanup(onComplete:Function):void 44 | { 45 | assert(!SharedObjectPool.contains(NAME)); 46 | onComplete(); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/utils/execute.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2012 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox.utils 9 | { 10 | /** Executes a function with the specified arguments. If the argument count does not match 11 | * the function, the argument list is cropped / filled up with null values. */ 12 | public function execute(func:Function, ...args):void 13 | { 14 | if (func != null) 15 | { 16 | var i:int; 17 | var maxNumArgs:int = func.length; 18 | 19 | for (i=args.length; i 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /flox/.flexLibProperties: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/src/com/gamua/flox/utils/makeHttpRequest.as: -------------------------------------------------------------------------------- 1 | package com.gamua.flox.utils 2 | { 3 | import flash.events.Event; 4 | import flash.events.HTTPStatusEvent; 5 | import flash.events.IOErrorEvent; 6 | import flash.net.URLLoader; 7 | import flash.net.URLRequest; 8 | import flash.net.URLVariables; 9 | 10 | public function makeHttpRequest(method:String, url:String, data:URLVariables, 11 | onComplete:Function, onError:Function):void 12 | { 13 | var httpStatus:int = -1; 14 | var loader:URLLoader = new URLLoader(); 15 | loader.addEventListener(Event.COMPLETE, onLoaderComplete); 16 | loader.addEventListener(IOErrorEvent.IO_ERROR, onLoaderError); 17 | loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onLoaderHttpStatus); 18 | 19 | var request:URLRequest = new URLRequest(url); 20 | request.data = data; 21 | request.method = method; 22 | 23 | loader.load(request); 24 | 25 | function onLoaderComplete(event:Event):void 26 | { 27 | closeLoader(); 28 | execute(onComplete, loader.data as String); 29 | } 30 | 31 | function onLoaderError(event:IOErrorEvent):void 32 | { 33 | closeLoader(); 34 | execute(onError, event.text, httpStatus); 35 | } 36 | 37 | function onLoaderHttpStatus(event:HTTPStatusEvent):void 38 | { 39 | httpStatus = event.status; 40 | } 41 | 42 | function closeLoader():void 43 | { 44 | loader.removeEventListener(Event.COMPLETE, onLoaderComplete); 45 | loader.removeEventListener(IOErrorEvent.IO_ERROR, onLoaderError); 46 | loader.removeEventListener(HTTPStatusEvent.HTTP_STATUS, onLoaderHttpStatus); 47 | loader.close(); 48 | } 49 | 50 | function encodeForUri(object:Object):String 51 | { 52 | var urlVariables:URLVariables = new URLVariables(); 53 | for (var key:String in object) urlVariables[key] = object[key]; 54 | return urlVariables.toString(); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/events/QueueEvent.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2013 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox.events 9 | { 10 | import com.gamua.flox.utils.HttpStatus; 11 | 12 | import flash.events.Event; 13 | 14 | /** An Event that is dispatched when Flox has processed the request queue. The event contains 15 | * information about the outcome of the queue processing - i.e. the http status and 16 | * (in case of an error) error message of the last processed request. */ 17 | public class QueueEvent extends Event 18 | { 19 | /** The type of the event that is dispatched when the request queue finished processing. */ 20 | public static const QUEUE_PROCESSED:String = "queueProcessed"; 21 | 22 | private var mError:String; 23 | private var mHttpStatus:int; 24 | 25 | /** Creates a new QueueEvent with the specified 'success' value. */ 26 | public function QueueEvent(type:String, httpStatus:int=200, error:String="") 27 | { 28 | super(type); 29 | mError = error; 30 | mHttpStatus = httpStatus; 31 | } 32 | 33 | /** This property indicates if all queue elements were processed: if it's true, the queue 34 | * is now empty. */ 35 | public function get success():Boolean 36 | { 37 | return HttpStatus.isSuccess(mHttpStatus) || !HttpStatus.isTransientError(mHttpStatus); 38 | } 39 | 40 | /** If unsuccessful, contains the error message that caused queue processing to stop. */ 41 | public function get error():String { return mError; } 42 | 43 | /** Contains the http status of the last processed queue element. The queue will stop 44 | * processing either when it's empty, or when there is a transient error. */ 45 | public function get httpStatus():int { return mHttpStatus; } 46 | } 47 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flox SDK - ActionScript 3 2 | ========================= 3 | 4 | What is Flox? 5 | ------------- 6 | 7 | Flox is a server backend especially for game developers, providing all the basics you need for a game: analytics, leaderboards, custom entities, and much more. The focus of Flox lies on its scalability (guaranteed by running in the Google App Engine) and ease of use. 8 | 9 | While you can communicate with our servers directly via REST, we provide powerful SDKs for the most popular development platforms, including advanced features like offline-support and data caching. With these SDKs, integrating Flox into your game is just a matter of minutes. 10 | 11 | More information about Flox can be found here: [Flox, the No-Fuzz Game Backend](http://gamua.com/flox) 12 | 13 | How to use the ActionScript 3 SDK 14 | --------------------------------- 15 | 16 | Just by **starting up Flox**, you will already generate several interesting analytics charts in the web interface. 17 | 18 | Flox.init("gameID", "gameKey", "1.0"); 19 | 20 | With **Events**, you can collect more finegrained data about how players are using your game. Pass custom properties to get a nice visualization of the details. 21 | 22 | Flox.logEvent("GameStarted"); 23 | Flox.logEvent("MenuNavigation", { from: "MainMenu", to: "SettingsMenu" }); 24 | 25 | To **send and retrieve scores**, first set up a leaderboard in the web interface. Using its ID as an identifier, you are good to go. 26 | 27 | Flox.postScore("default", 999, "Johnny"); 28 | Flox.loadScores("default", TimeScope.ALL_TIME, 29 | function onComplete(scores:Array):void 30 | { 31 | trace("retrieved " + scores.length + " scores"); 32 | }, 33 | function onError(error:String, cachedScores:Array):void 34 | { 35 | trace("error loading scores: " + error); 36 | }); 37 | 38 | This is just the tip of the iceberg, though! Use Flox to store **custom Entities** and **query** them, make **Player Logins** via a simple **e-mail verification** or a **social network**, browse your game's **logs**, assign **custom permissions**, and much more. 39 | 40 | Where to go from here: 41 | ---------------------- 42 | 43 | * Visit [flox.cc](http://www.flox.cc) for more information about Flox. 44 | * Register and download the pre-compiled SDK to get started quickly. 45 | -------------------------------------------------------------------------------- /tests/src/com/gamua/flox/Constants.as: -------------------------------------------------------------------------------- 1 | package com.gamua.flox 2 | { 3 | import com.gamua.flox.utils.createURL; 4 | 5 | public class Constants 6 | { 7 | public static const LEADERBOARD_ID:String = "default"; 8 | private static var ServerConfig:XML; 9 | 10 | public static function initWithXML(xml:XML):void 11 | { 12 | ServerConfig = xml; 13 | } 14 | 15 | public static function get GAME_ID():String 16 | { 17 | return ServerConfig.Game.(@status == "enabled").ID; 18 | } 19 | 20 | public static function get GAME_KEY():String 21 | { 22 | return ServerConfig.Game.(@status == "enabled").Key; 23 | } 24 | 25 | public static function get BASE_URL():String 26 | { 27 | return ServerConfig.BaseURL; 28 | } 29 | 30 | public static function get ENABLED_HERO_KEY():String 31 | { 32 | return ServerConfig.Hero.(@status == "enabled").Key; 33 | } 34 | 35 | public static function get DISABLED_HERO_KEY():String 36 | { 37 | return ServerConfig.Hero.(@status == "disabled").Key; 38 | } 39 | 40 | public static function createGameUrl(...args):String 41 | { 42 | return createURL("games", GAME_ID, createURL(args)); 43 | } 44 | 45 | public static function createLeaderboardUrl(...args):String 46 | { 47 | return createURL("games", GAME_ID, "leaderboards", LEADERBOARD_ID, createURL(args)); 48 | } 49 | 50 | public static function initFlox(reportAnalytics:Boolean=false):void 51 | { 52 | Flox.traceLogs = false; 53 | Flox.reportAnalytics = reportAnalytics; 54 | Flox.initWithBaseURL(GAME_ID, GAME_KEY, Flox.VERSION, BASE_URL); 55 | } 56 | 57 | public static function initFloxForGameOverQuota():void 58 | { 59 | const gameID:String = ServerConfig.Game.(@status == "overQuota").ID; 60 | const gameKey:String = ServerConfig.Game.(@status == "overQuota").Key; 61 | 62 | Flox.traceLogs = false; 63 | Flox.reportAnalytics = false; 64 | Flox.initWithBaseURL(gameID, gameKey, Flox.VERSION, BASE_URL); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /tests/src/starling/unit/TestGuiFull.as: -------------------------------------------------------------------------------- 1 | package starling.unit 2 | { 3 | import com.gamua.flox.utils.formatString; 4 | 5 | import starling.display.Sprite; 6 | import starling.text.TextField; 7 | import starling.utils.Align; 8 | import starling.utils.Color; 9 | 10 | public class TestGuiFull extends TestGui 11 | { 12 | private static const LINE_HEIGHT:int = 10; 13 | private static const FONT_NAME:String = "mini"; 14 | private static const FONT_SIZE:int = -1; 15 | 16 | private var mWidth:int; 17 | private var mHeight:int; 18 | private var mLogLines:Sprite; 19 | private var mNumLogLines:int; 20 | private var mStatusInfo:TextField; 21 | 22 | public function TestGuiFull(testRunner:TestRunner, width:int, height:int) 23 | { 24 | super(testRunner); 25 | 26 | mWidth = width; 27 | mHeight = height; 28 | 29 | mStatusInfo = new TextField(width, LINE_HEIGHT, ""); 30 | mStatusInfo.format.setTo(FONT_NAME, FONT_SIZE, Color.WHITE); 31 | mStatusInfo.format.horizontalAlign = Align.RIGHT; 32 | addChild(mStatusInfo); 33 | 34 | mLogLines = new Sprite(); 35 | addChild(mLogLines); 36 | } 37 | 38 | override public function log(message:String, color:uint=0xffffff):void 39 | { 40 | super.log(message, color); 41 | 42 | var logLine:TextField = new TextField(mWidth, LINE_HEIGHT, message); 43 | logLine.format.setTo(FONT_NAME, FONT_SIZE, color); 44 | logLine.format.horizontalAlign = Align.LEFT; 45 | logLine.y = mNumLogLines * LINE_HEIGHT; 46 | mLogLines.addChild(logLine); 47 | mNumLogLines++; 48 | 49 | if (mNumLogLines * LINE_HEIGHT > mHeight) 50 | { 51 | mLogLines.removeChildAt(0); 52 | mLogLines.y -= LINE_HEIGHT; 53 | } 54 | } 55 | 56 | override public function assert(success:Boolean, message:String=null):void 57 | { 58 | super.assert(success, message); 59 | 60 | mStatusInfo.text = formatString("Passed {0} of {1} tests", successCount, testCount); 61 | mStatusInfo.format.color = (successCount == testCount) ? Color.GREEN : Color.RED; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/Authentication.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2013 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox 9 | { 10 | /** This class stores information about how the current player was authenticated. */ 11 | internal final class Authentication 12 | { 13 | private var mPlayerID:String; 14 | private var mType:String; 15 | private var mID:String; 16 | private var mToken:String; 17 | 18 | /** Create an Authentication instance with the given parameters. */ 19 | public function Authentication(playerID:String="unknown", 20 | type:String="guest", id:String=null, token:String=null) 21 | { 22 | mPlayerID = playerID; 23 | mType = type; 24 | mID = id; 25 | mToken = token; 26 | } 27 | 28 | /** Creates a duplicate of the authentication object. */ 29 | public function clone():Authentication 30 | { 31 | return new Authentication(mPlayerID, mType, mID, mToken); 32 | } 33 | 34 | // properties 35 | // since this class is saved in a SharedObject, everything has to be R/W! 36 | 37 | /** The player ID of the authenticated player. */ 38 | public function get playerId():String { return mPlayerID; } 39 | public function set playerId(value:String):void { mPlayerID = value; } 40 | 41 | /** The authentication type, which is one of the strings defined in the 42 | * 'AuthenticationType' class. */ 43 | public function get type():String { return mType; } 44 | public function set type(value:String):void { mType = value; } 45 | 46 | /** The authentication ID, which is the id of the player in the authentication realm 47 | * (e.g. a Facebook user ID). */ 48 | public function get id():String { return mID; } 49 | public function set id(value:String):void { mID = value; } 50 | 51 | /** The token that identifies the session within the authentication realm. */ 52 | public function get token():String { return mToken; } 53 | public function set token(value:String):void { mToken = value; } 54 | } 55 | } -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/utils/createUID.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2012 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox.utils 9 | { 10 | import flash.crypto.generateRandomBytes; 11 | import flash.utils.ByteArray; 12 | 13 | /** 14 | * Generates a UID (unique identifier) that can be used for Entity IDs. Per default, the 15 | * UID is 16 characters long and is based on ActionScript's "flash.crypto" random number 16 | * generator. 17 | * 18 | *

When you pass a seed, the String is instead based on the SHA-256 hash of the seed. 19 | * Each seed will always produce the same result. The maximum length of a seeded UID is 20 | * 43 characters (the full SHA-256 length).

21 | * 22 | *

Note: do not use this function for security-critical hashing; to limit the result to 23 | * an alphanumeric String, it uses the SHA-256 algorithm in a special way. If you need 24 | * perfect security, use the SHA256 class directly.

25 | */ 26 | public function createUID(length:int=16, seed:String=null):String 27 | { 28 | var bytes:ByteArray; 29 | var b64:String = null; 30 | 31 | if (length == 0) return ""; 32 | else if (seed) 33 | { 34 | if (length > 43) 35 | throw new Error("Maximum length of seeded UID is 43 characters"); 36 | 37 | while (b64 == null || b64.indexOf("/") != -1 || b64.indexOf("+") != -1) 38 | { 39 | b64 = SHA256.hashString(seed).replace("=", "").substr(0, length); 40 | seed += b64.charAt(0); 41 | } 42 | } 43 | else 44 | { 45 | bytes = flash.crypto.generateRandomBytes(length); 46 | b64 = Base64.encodeByteArray(bytes).substr(0, length); 47 | 48 | while (b64.indexOf("/") != -1) b64 = b64.replace("/", getRandomChar()); 49 | while (b64.indexOf("+") != -1) b64 = b64.replace("+", getRandomChar()); 50 | } 51 | 52 | return b64; 53 | } 54 | } 55 | 56 | const ALLOWED_CHARS:String = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 57 | const NUM_ALLOWED_CHARS:int = 62; 58 | 59 | function getRandomChar():String 60 | { 61 | return ALLOWED_CHARS.charAt(int(Math.random() * NUM_ALLOWED_CHARS)); 62 | } -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/Score.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2013 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox 9 | { 10 | import com.gamua.flox.utils.DateUtil; 11 | import com.gamua.flox.utils.formatString; 12 | 13 | /** Provides information about the value and origin of one posted score entry. */ 14 | public class Score 15 | { 16 | private var mPlayerId:String; 17 | private var mPlayerName:String; 18 | private var mValue:int; 19 | private var mDate:Date; 20 | private var mCountry:String; 21 | 22 | /** Create a new score instance with the given values. */ 23 | public function Score(playerId:String=null, playerName:String=null, 24 | value:int=0, date:Date=null, country:String=null) 25 | { 26 | mPlayerId = playerId ? playerId : "unknown"; 27 | mPlayerName = playerName ? playerName : "unknown"; 28 | mCountry = country ? country : "us"; 29 | mValue = value; 30 | mDate = date ? date : new Date(); 31 | } 32 | 33 | /** The ID of the player who posted the score. Note that this could be a guest player 34 | * unknown to the server. */ 35 | public function get playerId():String { return mPlayerId; } 36 | public function set playerId(value:String):void { mPlayerId = value; } 37 | 38 | /** The name of the player who posted the score. */ 39 | public function get playerName():String { return mPlayerName; } 40 | public function set playerName(value:String):void { mPlayerName = value; } 41 | 42 | /** The actual value/score. */ 43 | public function get value():int { return mValue; } 44 | public function set value(value:int):void { mValue = value; } 45 | 46 | /** The date at which the score was posted. */ 47 | public function get date():Date { return mDate; } 48 | public function set date(value:Date):void { mDate = value; } 49 | 50 | /** The country from which the score originated, in a two-letter country code. */ 51 | public function get country():String { return mCountry; } 52 | public function set country(value:String):void { mCountry = value; } 53 | 54 | /** Returns a description of the score. */ 55 | public function toString():String 56 | { 57 | return formatString('[Score playerName="{0}" value="{1}" country="{2}" date="{3}"]', 58 | mPlayerName, mValue, mCountry, DateUtil.toString(mDate)); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /tests/src/starling/unit/TestGui.as: -------------------------------------------------------------------------------- 1 | package starling.unit 2 | { 3 | import flash.utils.getTimer; 4 | 5 | import starling.display.Sprite; 6 | import starling.events.Event; 7 | import starling.utils.Color; 8 | 9 | public class TestGui extends Sprite 10 | { 11 | private var mTestRunner:TestRunner; 12 | private var mLoop:Boolean; 13 | private var mTestCount:int; 14 | private var mSuccessCount:int; 15 | private var mStartMoment:Number; 16 | private var mIsPaused:Boolean; 17 | 18 | public function TestGui(testRunner:TestRunner) 19 | { 20 | mTestRunner = testRunner; 21 | mTestRunner.logFunction = log; 22 | mTestRunner.assertFunction = assert; 23 | } 24 | 25 | public function start(loop:Boolean=false):void 26 | { 27 | mLoop = loop; 28 | mStartMoment = getTimer() / 1000; 29 | mIsPaused = false; 30 | addEventListener(Event.ENTER_FRAME, onEnterFrame); 31 | } 32 | 33 | public function stop():void 34 | { 35 | removeEventListener(Event.ENTER_FRAME, onEnterFrame); 36 | mTestRunner.resetRun(); 37 | } 38 | 39 | private function onEnterFrame(event:Event):void 40 | { 41 | if (mIsPaused) return; 42 | 43 | var status:String = mTestRunner.runNext(); 44 | 45 | if (status == TestRunner.STATUS_FINISHED) 46 | { 47 | var duration:int = getTimer() / 1000 - mStartMoment; 48 | stop(); 49 | 50 | log("Finished all tests!", Color.AQUA); 51 | log("Duration: " + duration + " seconds.", Color.AQUA); 52 | 53 | if (mLoop) start(true); 54 | else onFinished(); 55 | } 56 | } 57 | 58 | public function onFinished():void 59 | { 60 | // override in subclass 61 | } 62 | 63 | public function log(message:String, color:uint=0xffffff):void 64 | { 65 | trace(message); 66 | } 67 | 68 | public function assert(success:Boolean, message:String=null):void 69 | { 70 | mTestCount++; 71 | 72 | if (success) 73 | { 74 | mSuccessCount++; 75 | } 76 | else 77 | { 78 | message = message ? message : "Assertion failed."; 79 | log(" " + message, Color.RED); 80 | } 81 | } 82 | 83 | public function get testCount():int { return mTestCount; } 84 | public function get successCount():int { return mSuccessCount; } 85 | public function get isStarted():Boolean { return mStartMoment >= 0; } 86 | 87 | public function get isPaused():Boolean { return mIsPaused; } 88 | public function set isPaused(value:Boolean):void { mIsPaused = value; } 89 | } 90 | } -------------------------------------------------------------------------------- /tests/src/com/gamua/flox/HeroTest.as: -------------------------------------------------------------------------------- 1 | package com.gamua.flox 2 | { 3 | import com.gamua.flox.utils.CustomEntity; 4 | import com.gamua.flox.utils.HttpStatus; 5 | 6 | import starling.unit.UnitTest; 7 | 8 | public class HeroTest extends UnitTest 9 | { 10 | public override function setUp():void 11 | { 12 | Constants.initFlox(); 13 | Player.logout(); // create a new guest player for each test 14 | } 15 | 16 | public override function tearDown():void 17 | { 18 | Flox.shutdown(); 19 | } 20 | 21 | public function testHero(onComplete:Function):void 22 | { 23 | var guestID:String = Player.current.id; 24 | var entity:CustomEntity = new CustomEntity("top secret", 20); 25 | entity.publicAccess = Access.NONE; 26 | entity.save(onSaveComplete, onError); 27 | 28 | function onSaveComplete(savedEntity:Entity):void 29 | { 30 | Player.loginWithKey(Constants.ENABLED_HERO_KEY, onLoginComplete, onError); 31 | } 32 | 33 | function onLoginComplete(hero:Player):void 34 | { 35 | assert(hero.id != Constants.ENABLED_HERO_KEY); 36 | Entity.load(CustomEntity, entity.id, onLoadComplete, onError); 37 | } 38 | 39 | function onLoadComplete(loadedEntity:CustomEntity):void 40 | { 41 | assertEqual(loadedEntity.id, entity.id, "wrong entity returned"); 42 | assertEqual(loadedEntity.name, entity.name, "wrong name returned"); 43 | assertEqual(loadedEntity.age, entity.age, "wrong age returned"), 44 | 45 | loadedEntity.age = 100; 46 | loadedEntity.save(onUpdateComplete, onError); 47 | } 48 | 49 | function onUpdateComplete(savedEntity:CustomEntity):void 50 | { 51 | assertEqual(savedEntity.ownerId, guestID, "owner of entity changed"); 52 | assertEqual(savedEntity.age, 100, "wrong age saved"); 53 | entity.destroy(onDestroyComplete, onError); 54 | } 55 | 56 | function onDestroyComplete():void 57 | { 58 | onComplete(); 59 | } 60 | 61 | function onError(error:String):void 62 | { 63 | fail(error); 64 | onComplete(); 65 | } 66 | } 67 | 68 | public function testDisabledHero(onComplete:Function):void 69 | { 70 | Player.loginWithKey(Constants.DISABLED_HERO_KEY, onLoginComplete, onLoginError); 71 | 72 | function onLoginComplete():void 73 | { 74 | fail("disabled hero could log in!"); 75 | onComplete(); 76 | } 77 | 78 | function onLoginError(error:String, httpStatus:int):void 79 | { 80 | assertEqual(httpStatus, HttpStatus.BAD_REQUEST, "wrong http status"); 81 | onComplete(); 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /tests/src/starling/unit/TestGuiEx.as: -------------------------------------------------------------------------------- 1 | package starling.unit 2 | { 3 | import com.gamua.flox.utils.formatString; 4 | 5 | import flash.external.ExternalInterface; 6 | 7 | import starling.core.Starling; 8 | import starling.events.Touch; 9 | import starling.events.TouchEvent; 10 | import starling.events.TouchPhase; 11 | import starling.text.TextField; 12 | import starling.utils.Color; 13 | 14 | public class TestGuiEx extends TestGui 15 | { 16 | private static const LINE_HEIGHT:int = 20; 17 | private static const FONT_NAME:String = "mini"; 18 | private static const FONT_SIZE:int = -2; 19 | 20 | private var mHeader:TextField; 21 | private var mStatus:TextField; 22 | private var mFooter:TextField; 23 | 24 | public function TestGuiEx(testRunner:TestRunner, width:int, header:String) 25 | { 26 | super(testRunner); 27 | 28 | mHeader = new TextField(width, LINE_HEIGHT, header); 29 | mHeader.format.setTo(FONT_NAME, FONT_SIZE, Color.WHITE); 30 | addChild(mHeader); 31 | 32 | mStatus = new TextField(width, LINE_HEIGHT, "0 / 0"); 33 | mStatus.format.setTo(FONT_NAME, FONT_SIZE, Color.WHITE); 34 | mStatus.y = LINE_HEIGHT; 35 | addChild(mStatus); 36 | 37 | mFooter = new TextField(width, LINE_HEIGHT, ""); 38 | mFooter.format.setTo(FONT_NAME, FONT_SIZE, Color.WHITE); 39 | mFooter.y = 2 * LINE_HEIGHT; 40 | addChild(mFooter); 41 | 42 | addEventListener(TouchEvent.TOUCH, onTouch); 43 | } 44 | 45 | override public function onFinished():void 46 | { 47 | mFooter.text = "Done"; 48 | callExternalInterface("startNextTest"); 49 | } 50 | 51 | private function onTouch(event:TouchEvent):void 52 | { 53 | var touch:Touch = event.getTouch(this, TouchPhase.ENDED); 54 | if (touch && isStarted) 55 | { 56 | isPaused = !isPaused; 57 | mFooter.text = isPaused ? "paused" : ""; 58 | } 59 | } 60 | 61 | override public function log(message:String, color:uint=0xffffff):void 62 | { 63 | super.log(message, color); 64 | callExternalInterface("addLog", message, color); 65 | } 66 | 67 | override public function assert(success:Boolean, message:String=null):void 68 | { 69 | super.assert(success, message); 70 | 71 | mStatus.text = formatString("{0} / {1}", successCount, testCount); 72 | mStatus.format.color = (successCount == testCount) ? Color.GREEN : Color.RED; 73 | } 74 | 75 | private function callExternalInterface(method, ...args):void 76 | { 77 | if (ExternalInterface.available) 78 | { 79 | var url:String = Starling.current.nativeStage.loaderInfo.url; 80 | var swfName:String = url.split("/").pop().replace(/\?.*$/, "").slice(0, -4); 81 | args.unshift(method, swfName); 82 | ExternalInterface.call.apply(null, args); 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/utils/cloneObject.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2012 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox.utils 9 | { 10 | import flash.utils.getQualifiedClassName; 11 | 12 | /** Creates a deep copy of the object. 13 | * Beware: all complex data types will become mere 'Object' instances. Supported are only 14 | * primitive data types, arrays and objects. Any properties marked with "NonSerialized" 15 | * meta data will be ignored by this method. 16 | * 17 | *

Optionally, you can pass a 'filter' function. It will be called on any child object. 18 | * You can use this filter to create custom serializations. The following sample exchanges 19 | * every 'Date' instance with a custom string.

20 | * 21 | *
22 |      *  clone:Object = cloneObject(original, function(object:Object):Object
23 |      *      {
24 |      *          if (object is Date) return DateUtil.toString(object as Date);
25 |      *          else return null; // 'null' causes default behaviour
26 |      *      });
27 | */ 28 | public function cloneObject(object:Object, filter:Function=null):Object 29 | { 30 | if (filter != null) 31 | { 32 | var filteredClone:Object = filter(object); 33 | if (filteredClone) return filteredClone; 34 | } 35 | 36 | if (object is Number || object is String || object is Boolean || object == null) 37 | return object; 38 | else if (object is Array) 39 | { 40 | var array:Array = object as Array; 41 | var arrayClone:Array = []; 42 | var length:int = array.length; 43 | for (var i:int=0; i = new []; 27 | private static var sAccessedNames:Dictionary = new Dictionary(); 28 | 29 | public function SharedObjectPool() 30 | { 31 | throw new Error("This class cannot be instantiated."); 32 | } 33 | 34 | public static function startAutoCleanup(interval:Number=20):void 35 | { 36 | clearInterval(sIntervalID); 37 | sIntervalID = setInterval(clearUnusedObjects, interval * 1000); 38 | } 39 | 40 | public static function stopAutoCleanup():void 41 | { 42 | clearInterval(sIntervalID); 43 | } 44 | 45 | private static function clearUnusedObjects():void 46 | { 47 | for (var name:String in sPool) 48 | sPoolNames[sPoolNames.length] = name; 49 | 50 | for each (name in sPoolNames) 51 | if (!(name in sAccessedNames)) 52 | delete sPool[name]; 53 | 54 | sPoolNames.length = 0; 55 | sAccessedNames = new Dictionary(); 56 | } 57 | 58 | public static function getObject(name:String):SharedObject 59 | { 60 | var so:SharedObject = sPool[name]; 61 | 62 | if (so == null) 63 | { 64 | so = SharedObject.getLocal(name); 65 | sPool[name] = so; 66 | } 67 | 68 | sAccessedNames[name] = true; 69 | return so; 70 | } 71 | 72 | public static function clearObject(name:String):void 73 | { 74 | var so:SharedObject = getObject(name); 75 | so.clear(); 76 | 77 | delete sPool[name]; 78 | } 79 | 80 | public static function purgePool():void 81 | { 82 | sPool = new Dictionary(); 83 | } 84 | 85 | public static function flush():void 86 | { 87 | for each (var so:SharedObject in sPool) 88 | so.flush(); 89 | } 90 | 91 | public static function contains(name:String):Boolean { return name in sPool; } 92 | } 93 | } -------------------------------------------------------------------------------- /flox/build/ant/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Flox SWC Assembler 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 85 | 86 | -------------------------------------------------------------------------------- /tests/src/com/gamua/flox/AnalyticsTest.as: -------------------------------------------------------------------------------- 1 | package com.gamua.flox 2 | { 3 | import com.gamua.flox.events.QueueEvent; 4 | 5 | import starling.unit.UnitTest; 6 | 7 | public class AnalyticsTest extends UnitTest 8 | { 9 | // nothing to actually test here, because the analytics class just uploads the 10 | // data and can't retrieve it from the server. This test is solely an easy way to 11 | // step through the code. 12 | 13 | public function testLogTypes(onComplete:Function):void 14 | { 15 | var strings:Array = ["hugo", "berti", "sepp"]; 16 | var booleans:Array = [false, true]; 17 | var eventProperties:Object = { 18 | "int": int(Math.random() * 20), 19 | "number": Math.random() * 1000, 20 | "string": strings[int(Math.random() * strings.length)], 21 | "boolean": booleans[int(Math.random() * booleans.length)] 22 | }; 23 | 24 | Constants.initFlox(true); 25 | Flox.logInfo("This is the first {0} log", "info"); 26 | Flox.logWarning("This is a {0} log", "warning"); 27 | Flox.logError("Error"); 28 | Flox.logError("AnotherError", "Additional Information"); 29 | Flox.logError(new ArgumentError("ErrorMessage")); 30 | Flox.logEvent("Event"); 31 | Flox.logEvent("EventWithProperties", eventProperties); 32 | Flox.logEvent("EventWithSingleStringProperty", eventProperties.string); 33 | Flox.logEvent("EventWithSingleNumberProperty", eventProperties.number); 34 | Flox.logEvent("EventWithSingleBooleanProperty", eventProperties.boolean); 35 | Flox.logInfo("This is the last info log"); 36 | Flox.shutdown(); 37 | 38 | submitEmptySession(onComplete); 39 | } 40 | 41 | public function testLongLog(onComplete:Function):void 42 | { 43 | if (true) // we don't want to spam the server -- activate on demand 44 | { 45 | onComplete(); 46 | return; 47 | } 48 | 49 | var count:int = 10000; 50 | 51 | Constants.initFlox(true); 52 | Flox.logInfo("First log line of {0}", count); 53 | 54 | for (var i:int=0; i Running on " + Constants.BASE_URL); 67 | _testGui.start(); 68 | } 69 | 70 | private function loadConfig(onComplete:Function):void 71 | { 72 | var padding:int = 10; 73 | var stage:Stage = Starling.current.stage; 74 | var width:int = stage.stageWidth - 2*padding; 75 | var height:int = stage.stageHeight - 2*padding; 76 | var flashVars:Object = Starling.current.nativeStage.loaderInfo.parameters; 77 | 78 | if ("config" in flashVars) 79 | { 80 | _testGui = new TestGuiEx(_testRunner, width, Flox.VERSION); 81 | makeHttpRequest(HttpMethod.GET, flashVars["config"], null, 82 | onDownloadComplete, onDownloadError); 83 | } 84 | else 85 | { 86 | _testGui = new TestGuiFull(_testRunner, width, height); 87 | onDownloadComplete(XML(new serverConfig())); 88 | } 89 | 90 | _testGui.x = _testGui.y = padding; 91 | addChild(_testGui); 92 | 93 | function onDownloadComplete(config:String):void 94 | { 95 | Constants.initWithXML(XML(config)); 96 | onComplete(); 97 | } 98 | 99 | function onDownloadError(error:String):void 100 | { 101 | if (ExternalInterface.available) 102 | ExternalInterface.call("alert", "Could not load config file"); 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /tests/.actionScriptProperties: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/src/starling/unit/TestRunner.as: -------------------------------------------------------------------------------- 1 | package starling.unit 2 | { 3 | import flash.utils.describeType; 4 | import flash.utils.getQualifiedClassName; 5 | 6 | import starling.utils.StringUtil; 7 | 8 | public class TestRunner 9 | { 10 | public static const STATUS_FINISHED:String = "finished"; 11 | public static const STATUS_RUNNING:String = "running"; 12 | public static const STATUS_WAITING:String = "waiting"; 13 | 14 | private var mTests:Array; 15 | private var mLogFunction:Function; 16 | private var mAssertFunction:Function; 17 | private var mCurrentTestIndex:int; 18 | private var mWaiting:Boolean; 19 | 20 | public function TestRunner() 21 | { 22 | mTests = []; 23 | mCurrentTestIndex = 0; 24 | mWaiting = false; 25 | 26 | mLogFunction = trace; 27 | mAssertFunction = function(success:Boolean, message:String=null):void 28 | { 29 | if (success) trace("Success!"); 30 | else trace(message ? message : "Assertion failed."); 31 | }; 32 | } 33 | 34 | public function add(testClass:Class):void 35 | { 36 | var typeInfo:XML = describeType(testClass); 37 | var methodNames:Array = []; 38 | 39 | for each (var method:XML in typeInfo.factory.method) 40 | if (method.@name.toLowerCase().indexOf("test") == 0) 41 | methodNames.push(method.@name.toString()); 42 | 43 | methodNames.sort(); 44 | 45 | for each (var methodName:String in methodNames) 46 | mTests.push([testClass, methodName]); 47 | } 48 | 49 | public function runNext():String 50 | { 51 | if (mWaiting) return STATUS_WAITING; 52 | if (mCurrentTestIndex == mTests.length) return STATUS_FINISHED; 53 | 54 | mWaiting = true; 55 | var testData:Array = mTests[mCurrentTestIndex++]; 56 | runTest(testData[0], testData[1], onComplete); 57 | return mWaiting ? STATUS_WAITING : STATUS_RUNNING; 58 | 59 | function onComplete():void 60 | { 61 | mWaiting = false; 62 | } 63 | } 64 | 65 | public function resetRun():void 66 | { 67 | mCurrentTestIndex = 0; 68 | } 69 | 70 | private function runTest(testClass:Class, methodName:String, onComplete:Function):void 71 | { 72 | var className:String = getQualifiedClassName(testClass).split("::").pop(); 73 | logFunction(StringUtil.format("{0}.{1} ...", className, methodName)); 74 | 75 | var test:UnitTest = new testClass() as UnitTest; 76 | test.assertFunction = mAssertFunction; 77 | 78 | setUp(); 79 | 80 | function setUp():void 81 | { 82 | test.setUp(); 83 | test.setUpAsync(run); 84 | } 85 | 86 | function run():void 87 | { 88 | var method:Function = test[methodName]; 89 | var async:Boolean = method.length != 0; 90 | if (async) 91 | { 92 | method(tearDown); 93 | } 94 | else 95 | { 96 | method(); 97 | tearDown(); 98 | } 99 | } 100 | 101 | function tearDown():void 102 | { 103 | test.tearDown(); 104 | test.tearDownAsync(onComplete); 105 | } 106 | } 107 | 108 | public function get assertFunction():Function { return mAssertFunction; } 109 | public function set assertFunction(value:Function):void { mAssertFunction = value; } 110 | 111 | public function get logFunction():Function { return mLogFunction; } 112 | public function set logFunction(value:Function):void { mLogFunction = value; } 113 | } 114 | } -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/utils/HttpStatus.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2012 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox.utils 9 | { 10 | /** Provides a list of HTTP status codes that are returned by the Flox server. */ 11 | public final class HttpStatus 12 | { 13 | /** @private */ 14 | public function HttpStatus() { throw new Error("This class cannot be instantiated."); } 15 | 16 | /** We don't know the actual HTTP status. */ 17 | public static const UNKNOWN:int = 0; 18 | 19 | /** Everything is OK. */ 20 | public static const OK:int = 200; 21 | 22 | /** The request has been accepted for processing, but processing has not been completed. */ 23 | public static const ACCEPTED:int = 202; 24 | 25 | /** Everything is OK, but there's nothing to return. */ 26 | public static const NO_CONTENT:int = 204; 27 | 28 | /** An If-None-Match precondition failed in a GET request. */ 29 | public static const NOT_MODIFIED:int = 304; 30 | 31 | /** The request is missing information, e.g. parameters. */ 32 | public static const BAD_REQUEST:int = 400; 33 | 34 | /** Used by 'Player.loginWithEmailAndPassword' to indicate that a confirmation mail 35 | * has been sent to the player. */ 36 | public static const UNAUTHORIZED:int = 401; 37 | 38 | /** An authentication error occurred. */ 39 | public static const FORBIDDEN:int = 403; 40 | 41 | /** The requested resource is not available. */ 42 | public static const NOT_FOUND:int = 404; 43 | 44 | /** The request timed out. */ 45 | public static const REQUEST_TIMEOUT:int = 408; 46 | 47 | /** An If-Match precondition failed in a PUT/POST request. */ 48 | public static const PRECONDITION_FAILED:int = 412; 49 | 50 | /** The user has sent too many requests in a given amount of time. */ 51 | public static const TOO_MANY_REQUESTS:int = 429; 52 | 53 | /** Something unexpected happened within server code. */ 54 | public static const INTERNAL_SERVER_ERROR:int = 500; 55 | 56 | /** Accessing this resource has not yet been implemented */ 57 | public static const NOT_IMPLEMENTED:int = 501; 58 | 59 | /** The server is down for maintenance. */ 60 | public static const SERVICE_UNAVAILABLE:int = 503; 61 | 62 | /** Indicates if a status code depicts a success or a failure. */ 63 | public static function isSuccess(status:int):Boolean 64 | { 65 | return status > 0 && status < 400; 66 | } 67 | 68 | /** Indicates if an error might go away if the request is tried again (i.e. the server 69 | * was not reachable or there was a network error). */ 70 | public static function isTransientError(status:int):Boolean 71 | { 72 | return status == UNKNOWN || status == SERVICE_UNAVAILABLE || status == REQUEST_TIMEOUT; 73 | } 74 | 75 | /** Returns a string representation of the given status code. */ 76 | public static function toString(status:int):String 77 | { 78 | switch (status) 79 | { 80 | case OK: return "ok"; 81 | case ACCEPTED: return "accepted"; 82 | case NO_CONTENT: return "noContent"; 83 | case NOT_MODIFIED: return "notModified"; 84 | case BAD_REQUEST: return "badRequest"; 85 | case UNAUTHORIZED: return "unAuthorized"; 86 | case FORBIDDEN: return "forbidden"; 87 | case NOT_FOUND: return "notFound"; 88 | case REQUEST_TIMEOUT: return "requestTimeout"; 89 | case PRECONDITION_FAILED: return "preconditionFailed"; 90 | case TOO_MANY_REQUESTS: return "tooManyRequest"; 91 | case INTERNAL_SERVER_ERROR: return "internalServerError"; 92 | case NOT_IMPLEMENTED: return "notImplemented"; 93 | case SERVICE_UNAVAILABLE: return "serviceUnavailable"; 94 | default: return "unknown"; 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /tests/src/com/gamua/flox/RestServiceTest.as: -------------------------------------------------------------------------------- 1 | package com.gamua.flox 2 | { 3 | import com.gamua.flox.events.QueueEvent; 4 | import com.gamua.flox.utils.HttpMethod; 5 | import com.gamua.flox.utils.HttpStatus; 6 | 7 | import starling.unit.UnitTest; 8 | 9 | public class RestServiceTest extends UnitTest 10 | { 11 | public override function setUp():void 12 | { 13 | Constants.initFlox(); 14 | } 15 | 16 | public override function tearDown():void 17 | { 18 | Flox.shutdown(); 19 | } 20 | 21 | public function testGetStatus(onComplete:Function):void 22 | { 23 | Flox.service.request(HttpMethod.GET, "", null, onRequestComplete, onRequestError); 24 | 25 | function onRequestComplete(body:Object, httpStatus:int):void 26 | { 27 | assertNotNull(body); 28 | assertEqual(body.status, "ok"); 29 | onComplete(); 30 | } 31 | 32 | function onRequestError(error:String, httpStatus:int):void 33 | { 34 | fail("Could not get server status: " + error); 35 | onComplete(); 36 | } 37 | } 38 | 39 | // not yet supported by Flox Server 40 | // public function testNonExistingMethod(onComplete:Function):void 41 | // { 42 | // requestNonExistingPath(".does-no-exist", onComplete); 43 | // } 44 | 45 | public function testGetNonExistingEntity(onComplete:Function):void 46 | { 47 | requestNonExistingPath("entities/.player/does-not-exist", onComplete); 48 | } 49 | 50 | private function requestNonExistingPath(path:String, onComplete:Function):void 51 | { 52 | Flox.service.request(HttpMethod.GET, path, null, onRequestComplete, onRequestError); 53 | 54 | function onRequestComplete(body:Object, httpStatus:int):void 55 | { 56 | fail("Server should have returned error"); 57 | onComplete(); 58 | } 59 | 60 | function onRequestError(error:String, httpStatus:int):void 61 | { 62 | assertEqual(HttpStatus.NOT_FOUND, httpStatus, "wrong http status: " + httpStatus); 63 | onComplete(); 64 | } 65 | } 66 | 67 | public function testQueueEvent(onComplete:Function):void 68 | { 69 | Flox.addEventListener(QueueEvent.QUEUE_PROCESSED, onQueueProcessed); 70 | Flox.service.requestQueued(HttpMethod.GET, ""); 71 | 72 | function onQueueProcessed(event:Object):void 73 | { 74 | Flox.removeEventListener(QueueEvent.QUEUE_PROCESSED, onQueueProcessed); 75 | onComplete(); 76 | } 77 | } 78 | 79 | public function testGameOverQuota(onComplete:Function):void 80 | { 81 | Flox.shutdown(); 82 | Constants.initFloxForGameOverQuota(); 83 | 84 | Player.current.save(onSaveComplete, onSaveError); 85 | 86 | function onSaveComplete(body:Object, httpStatus:int):void 87 | { 88 | fail("game that's over quota allowed to save entity"); 89 | onComplete(); 90 | } 91 | 92 | function onSaveError(error:String, httpStatus:int):void 93 | { 94 | assertEqual(HttpStatus.TOO_MANY_REQUESTS, httpStatus, 95 | "wrong http status on over-quota game"); 96 | 97 | onComplete(); 98 | } 99 | } 100 | 101 | public function testServerTime(onComplete:Function):void 102 | { 103 | Flox.getTime(onRequestComplete, onRequestError); 104 | 105 | function onRequestComplete(time:Date):void 106 | { 107 | var diff:Number = Math.abs(new Date().time - time.time); 108 | assert(diff < 10000, "there's something wrong with the server time"); 109 | onComplete(); 110 | } 111 | 112 | function onRequestError(error:String):void 113 | { 114 | fail("Could not fetch server time: " + error); 115 | onComplete(); 116 | } 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /flox/.actionScriptProperties: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/src/starling/unit/UnitTest.as: -------------------------------------------------------------------------------- 1 | package starling.unit 2 | { 3 | import flash.utils.describeType; 4 | import flash.utils.getQualifiedClassName; 5 | 6 | public class UnitTest 7 | { 8 | private var mAssertFunction:Function; 9 | 10 | public function UnitTest() 11 | { } 12 | 13 | public function setUp():void 14 | { } 15 | 16 | public function setUpAsync(onComplete:Function):void 17 | { 18 | onComplete(); 19 | } 20 | 21 | public function tearDown():void 22 | { } 23 | 24 | public function tearDownAsync(onComplete:Function):void 25 | { 26 | onComplete(); 27 | } 28 | 29 | protected function assert(condition:Boolean, message:String=null):void 30 | { 31 | if (mAssertFunction != null) 32 | mAssertFunction(condition, message); 33 | } 34 | 35 | protected function assertFalse(condition:Boolean, message:String=null):void 36 | { 37 | assert(!condition, message); 38 | } 39 | 40 | protected function assertEqual(objectA:Object, objectB:Object, message:String=null):void 41 | { 42 | assert(objectA == objectB, message); 43 | } 44 | 45 | protected function assertEqualObjects(objectA:Object, objectB:Object, message:String=null):void 46 | { 47 | assert(compareObjects(objectA, objectB), message); 48 | } 49 | 50 | protected function assertEquivalent(numberA:Number, numberB:Number, 51 | message:String=null, e:Number=0.0001):void 52 | { 53 | assert(numberA - e < numberB && numberA + e > numberB, message); 54 | } 55 | 56 | protected function assertNull(object:Object, message:String=null):void 57 | { 58 | assert(object == null, message); 59 | } 60 | 61 | protected function assertNotNull(object:Object, message:String=null):void 62 | { 63 | assert(object != null, message); 64 | } 65 | 66 | protected function fail(message:String):void 67 | { 68 | assert(false, message); 69 | } 70 | 71 | protected function succeed(message:String):void 72 | { 73 | assert(true, message); 74 | } 75 | 76 | internal function get assertFunction():Function { return mAssertFunction; } 77 | internal function set assertFunction(value:Function):void { mAssertFunction = value; } 78 | 79 | // helpers 80 | 81 | private function compareObjects(objectA:Object, objectB:Object):Boolean 82 | { 83 | if (objectA is int || objectA is uint || objectA is Number || objectA is Boolean) 84 | return objectA === objectB; 85 | else if (objectA is Date && objectB is Date) 86 | return objectA.time - 500 < objectB.time && objectA.time + 500 > objectB.time; 87 | else 88 | { 89 | var nameA:String = getQualifiedClassName(objectA); 90 | var nameB:String = getQualifiedClassName(objectB); 91 | var prop:String; 92 | 93 | if (nameA != nameB) return false; 94 | 95 | if (objectA is Array || nameA.indexOf("__AS3__.vec::Vector.") == 0) 96 | { 97 | if (objectA.length != objectB.length) return false; 98 | 99 | for (var i:int=0; ifunction(index:int, metaData:Object):Boolean; */ 83 | public function filter(callback:Function):void 84 | { 85 | mIndex.data.elements = mIndex.data.elements.filter( 86 | function (element:Object, index:int, array:Array):Boolean 87 | { 88 | var keep:Boolean = callback(index, element.meta); 89 | if (!keep) SharedObjectPool.getObject(element.name).clear(); 90 | return keep; 91 | }); 92 | } 93 | 94 | private function getHead(removeHead:Boolean):Object 95 | { 96 | var elements:Array = mIndex.data.elements; 97 | if (elements.length == 0) return null; 98 | 99 | var name:String = elements[elements.length-1].name; 100 | var sharedObject:SharedObject = SharedObjectPool.getObject(name); 101 | var head:Object = sharedObject.data.value; 102 | 103 | if (head == null) 104 | { 105 | // shared object was deleted! remove object and try again 106 | elements.pop(); 107 | head = getHead(removeHead); 108 | } 109 | else 110 | { 111 | if (removeHead) 112 | { 113 | elements.pop(); 114 | sharedObject.clear(); 115 | } 116 | } 117 | 118 | return head; 119 | } 120 | 121 | /** Returns the number of elements in the queue. */ 122 | public function get length():int { return mIndex.data.elements.length; } 123 | 124 | /** Returns the name of the queue as it was provided in the constructor. */ 125 | public function get name():String { return mName; } 126 | } 127 | } -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/PersistentStore.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2012 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox 9 | { 10 | import com.gamua.flox.utils.cloneObject; 11 | import com.gamua.flox.utils.createUID; 12 | 13 | import flash.net.SharedObject; 14 | 15 | /** A data store that uses SharedObjects to save its contents to the disk. Objects are 16 | * serialized using the AMF format, so be sure to use either primitive objects or classes to 17 | * provide an empty constructor and r/w properties. */ 18 | internal class PersistentStore 19 | { 20 | private var mName:String; 21 | private var mIndex:SharedObject; 22 | 23 | /** Create a persistent store with a certain name. If the name was already used in a 24 | * previous session, the existing store is restored. */ 25 | public function PersistentStore(name:String) 26 | { 27 | mName = name; 28 | mIndex = SharedObjectPool.getObject(mName); 29 | } 30 | 31 | /** Saves an object with a certain key. If the key is already occupied, the previous 32 | * object and its metadata are overwritten. */ 33 | public function setObject(key:String, value:Object, metaData:Object=null):void 34 | { 35 | var info:Object = mIndex.data[key]; 36 | var name:String = createUID(); 37 | 38 | // If the object already exists we delete it and save 'value' under a new name. 39 | // This avoids problems if only one of the two SharedObjects can be saved. 40 | 41 | if (info && info.name) 42 | SharedObjectPool.clearObject(info.name); 43 | 44 | info = metaData ? cloneObject(metaData) : {}; 45 | info.name = name; 46 | mIndex.data[key] = info; 47 | 48 | var sharedObject:SharedObject = SharedObjectPool.getObject(name); 49 | sharedObject.data.value = value; 50 | } 51 | 52 | /** Removes an object from the store. */ 53 | public function removeObject(key:String):void 54 | { 55 | var info:Object = mIndex.data[key]; 56 | if (info) 57 | { 58 | SharedObjectPool.getObject(info.name).clear(); 59 | delete mIndex.data[key]; 60 | } 61 | } 62 | 63 | /** Retrieves an object with a certain key, or null if it's not part of the store. */ 64 | public function getObject(key:String):Object 65 | { 66 | var info:Object = mIndex.data[key]; 67 | if (info) return SharedObjectPool.getObject(info.name).data.value; 68 | else return null; 69 | } 70 | 71 | /** Indicates if an object with a certain key is part of the store. */ 72 | public function containsKey(key:String):Boolean 73 | { 74 | return key in mIndex.data; 75 | } 76 | 77 | /** Store certain meta data with the object. Meta Data can be accessed without having 78 | * to load the stored object from disk. Keep it small! */ 79 | public function setMetaData(objectKey:String, metaDataName:String, metaDataValue:Object):void 80 | { 81 | var info:Object = mIndex.data[objectKey]; 82 | if (info == null) throw new Error("object key not recognized"); 83 | if (metaDataName == "name") throw new Error("'name' is reserved for internal use"); 84 | info[metaDataName] = metaDataValue; 85 | } 86 | 87 | /** Retrieve specific meta data from a certain object. */ 88 | public function getMetaData(objectKey:String, metaDataName:String):Object 89 | { 90 | var info:Object = mIndex.data[objectKey]; 91 | if (info) return info[metaDataName]; 92 | else return null; 93 | } 94 | 95 | /** Removes all objects from the store, restoring all occupied disk storage. */ 96 | public function clear():void 97 | { 98 | var key:String; 99 | var keys:Array = []; 100 | 101 | for (key in mIndex.data) 102 | keys.push(key); 103 | 104 | for each (key in keys) 105 | { 106 | var info:Object = mIndex.data[key]; 107 | if (info && info.name) 108 | { 109 | var so:SharedObject = SharedObjectPool.getObject(info.name); 110 | if (so) so.clear(); 111 | } 112 | delete mIndex.data[key]; 113 | } 114 | } 115 | 116 | /** Saves the current state of the store to the disk. */ 117 | public function flush():void 118 | { 119 | mIndex.flush(); 120 | } 121 | 122 | /** Returns the name of the store as it was provided in the constructor. */ 123 | public function get name():String { return mName; } 124 | } 125 | } -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/utils/DateUtil.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2012 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox.utils 9 | { 10 | /** A static helper class providing methods to convert a Date to a String or to parse a String 11 | * to get a Date object. */ 12 | public class DateUtil 13 | { 14 | /** @private */ 15 | public function DateUtil() { throw new Error("This class cannot be instantiated."); } 16 | 17 | /** Parses dates that conform to the xs:DateTime format. */ 18 | public static function parse(str:String):Date 19 | { 20 | if (str == null) return null; 21 | var finalDate:Date; 22 | 23 | try 24 | { 25 | var dateStr:String = str.substring(0, str.indexOf("T")); 26 | var timeStr:String = str.substring(str.indexOf("T")+1, str.length); 27 | var dateArr:Array = dateStr.split("-"); 28 | var year:Number = Number(dateArr.shift()); 29 | var month:Number = Number(dateArr.shift()); 30 | var date:Number = Number(dateArr.shift()); 31 | 32 | var multiplier:Number; 33 | var offsetHours:Number; 34 | var offsetMinutes:Number; 35 | var offsetStr:String; 36 | 37 | if (timeStr.indexOf("Z") != -1) 38 | { 39 | multiplier = 1; 40 | offsetHours = 0; 41 | offsetMinutes = 0; 42 | timeStr = timeStr.replace("Z", ""); 43 | } 44 | else if (timeStr.indexOf("+") != -1) 45 | { 46 | multiplier = 1; 47 | offsetStr = timeStr.substring(timeStr.indexOf("+")+1, timeStr.length); 48 | offsetHours = Number(offsetStr.substring(0, offsetStr.indexOf(":"))); 49 | offsetMinutes = Number(offsetStr.substring(offsetStr.indexOf(":")+1, offsetStr.length)); 50 | timeStr = timeStr.substring(0, timeStr.indexOf("+")); 51 | } 52 | else // offset is - 53 | { 54 | multiplier = -1; 55 | offsetStr = timeStr.substring(timeStr.indexOf("-")+1, timeStr.length); 56 | offsetHours = Number(offsetStr.substring(0, offsetStr.indexOf(":"))); 57 | offsetMinutes = Number(offsetStr.substring(offsetStr.indexOf(":")+1, offsetStr.length)); 58 | timeStr = timeStr.substring(0, timeStr.indexOf("-")); 59 | } 60 | var timeArr:Array = timeStr.split(":"); 61 | var hour:Number = Number(timeArr.shift()); 62 | var minutes:Number = Number(timeArr.shift()); 63 | var secondsArr:Array = (timeArr.length > 0) ? String(timeArr.shift()).split(".") : null; 64 | var seconds:Number = (secondsArr != null && secondsArr.length > 0) ? Number(secondsArr.shift()) : 0; 65 | var milliseconds:Number = (secondsArr != null && secondsArr.length > 0) ? 1000*parseFloat("0." + secondsArr.shift()) : 0; 66 | var utc:Number = Date.UTC(year, month-1, date, hour, minutes, seconds, milliseconds); 67 | var offset:Number = (((offsetHours * 3600000) + (offsetMinutes * 60000)) * multiplier); 68 | finalDate = new Date(utc - offset); 69 | 70 | if (finalDate.toString() == "Invalid Date") 71 | throw new Error("This date does not conform to W3CDTF."); 72 | } 73 | catch (e:Error) 74 | { 75 | var eStr:String = "Unable to parse the string [" +str+ "] into a date. "; 76 | eStr += "The internal error was: " + e.toString(); 77 | throw new Error(eStr); 78 | } 79 | return finalDate; 80 | } 81 | 82 | /** Returns a date string formatted according to the xs:DateTime format. */ 83 | public static function toString(d:Date):String 84 | { 85 | if (d == null) return null; 86 | 87 | var date:Number = d.getUTCDate(); 88 | var month:Number = d.getUTCMonth(); 89 | var hours:Number = d.getUTCHours(); 90 | var minutes:Number = d.getUTCMinutes(); 91 | var seconds:Number = d.getUTCSeconds(); 92 | var milliseconds:Number = d.getUTCMilliseconds(); 93 | 94 | var sb:String = d.fullYearUTC.toString(); 95 | sb += "-"; 96 | if (month + 1 < 10) sb += "0"; 97 | sb += month + 1; 98 | sb += "-"; 99 | if (date < 10) sb += "0"; 100 | sb += date; 101 | sb += "T"; 102 | if (hours < 10) sb += "0"; 103 | sb += hours; 104 | sb += ":"; 105 | if (minutes < 10) sb += "0"; 106 | sb += minutes; 107 | sb += ":"; 108 | if (seconds < 10) sb += "0"; 109 | sb += seconds; 110 | if (milliseconds > 0) 111 | { 112 | sb += "."; 113 | if (milliseconds < 100) sb += "0"; 114 | if (milliseconds < 10) sb += "0"; 115 | sb += milliseconds; 116 | } 117 | sb += "Z"; //instead of: sb += "-00:00"; 118 | 119 | return sb; 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Flox: Changelog 2 | =============== 3 | 4 | version 1.4 - 2018-06-24 5 | ------------------------ 6 | 7 | * added support for storing Score instances as shared objects 8 | * added workaround for rare problem with inconsistent query index 9 | * added some new unit tests 10 | * now using name of HTTP status as error message as fallback (instead of 'unknown') 11 | * now ignoring 'ACTIVATE' event if there hasn't been a DEACTIVATE before 12 | * updated IDEA modules to use AIR 30 13 | * updated unit tests for Starling 2.x 14 | * updated Ant build-file 15 | * fixed documentation typos 16 | 17 | version 1.3 - 2014-11-28 18 | ------------------------ 19 | 20 | * added 'EMAIL-AND-PASSWORD' authentication 21 | * added Entity conflict handling (via 'onConflict' method) 22 | * added IntelliJ IDEA modules and project file 23 | * optimized internally used 'execute' method by avoiding any allocations 24 | * fixed: time-out status code is now regarded as 'transient' error 25 | 26 | version 1.2.1 - 2014-05-20 27 | -------------------------- 28 | 29 | * removed special handling of guest player refresh (caused problems when using a hero) 30 | 31 | version 1.2 - 2014-04-11 32 | ------------------------ 33 | 34 | * updated some REST API calls for updated url schemes 35 | * optimized and simplified 'execute' utility function 36 | * removed 'Player.authId' for security reasons 37 | * now logging warning when flushing of shared objects fails 38 | * fixed body compression detection in some error responses 39 | * fixed wrong event listener removal (thanks to Abogadro!) 40 | 41 | version 1.1.2 - 2014-01-09 42 | -------------------------- 43 | 44 | * added 'Flox.getTime' method which fetches the current server time 45 | * added checks for 'Flox.init' method arguments 46 | * now avoiding problems in cache when index SO is saved succesfully, but actual data is not. 47 | (In that case, the cache returned an old object. Now, it returns 'null', which is safer.) 48 | * making a request while a login is in process now ends up in 'onError' 49 | (previously, this threw an exception) 50 | 51 | version 1.1.1 - 2013-11-18 52 | -------------------------- 53 | 54 | * now preventing any service requests while a login is in progress 55 | (which is a bad practice and may lead to request errors in some cases) 56 | * added additional null-reference check in Entity handling 57 | (could cause exception in special cases) 58 | 59 | version 1.1 - 2013-11-14 60 | ------------------------ 61 | 62 | * added 'friends' scope for leaderboards (just pass the playerIDs you're interested in) 63 | * added 'Flox.useSecureConnection' property 64 | * changed leaderboard REST API 65 | 66 | version 1.0.2 - 2013-11-04 67 | -------------------------- 68 | 69 | * changed the maximum number of scores returned by 'Flox.loadScores' from 50 to 200 70 | * several updates of the API documentation 71 | 72 | version 1.0.1 - 2013-10-11 73 | -------------------------- 74 | 75 | * added missing 'keep-as3-metadata' options to SWC file 76 | * added information about 'Date'-type support into Entity API documentation 77 | 78 | version 1.0 - 2013-10-02 79 | ------------------------ 80 | 81 | * added 'orderBy' property to Query 82 | * added 'constraints' property to Query 83 | * added 'SHA256' utility class 84 | * added optional 'length' and 'seed' arguments to 'createUID' function 85 | * made 'Base64' utility class public 86 | * made cache & queue more reliable by manually flushing shared objects in memory on DEACTIVATE 87 | * now trimming redundant put-requests from request queue 88 | 89 | version 0.9 - 2013-08-22 90 | ------------------------ 91 | 92 | * If a game is suspended for more than 15 minutes, a new session will be started when the 93 | application is re-activated. This makes analytics much more accurate, especially on Android. 94 | * SWC file now contains ASDoc information 95 | 96 | version 0.8 - 2013-08-05 97 | ------------------------ 98 | 99 | * now enforcing that 'Flox.playerClass' points to class that extends Player 100 | * refreshing guest players now works even if the guest has not yet been saved at the server 101 | * optimized overall performance and removed temporary object creations in Base64 class 102 | 103 | version 0.7.1 - 2013-07-23 104 | -------------------------- 105 | 106 | * now updating 'createdAt' and 'updatedAt' on Entities with data from server 107 | * added some API documentation updates 108 | 109 | version 0.7 - 2013-06-26 110 | ------------------------ 111 | 112 | * added optional [NonSerialized] meta data for entity properties 113 | * preventing backup of shared objects on iCloud for iOS 114 | 115 | version 0.6 - 2013-06-04 116 | ------------------------ 117 | 118 | * added serialization of public members in Entities 119 | * added some more API documentation 120 | * prohibiting change of certain properties in the Player class 121 | 122 | version 0.5 - 2013-04-18 123 | ------------------------ 124 | 125 | * added 'Query' class for SQL-like entity retrieval 126 | * added 'firstStartTime' to Analytics report 127 | * changed 'onComplete' argument in 'loadScores' to 'Array', for consistency with Query class 128 | * more robust error handling when service cache is corrupted 129 | 130 | version 0.4.1 - 2013-03-14 131 | -------------------------- 132 | 133 | * removed static save and refresh methods 134 | 135 | version 0.4 - 2013-03-13 136 | ------------------------ 137 | 138 | * added 'Entity' class for storing arbitrary objects on the server 139 | * added Entity permissions ('publicAccess' property) 140 | * added 'Player' class for basic player management 141 | * added Player authentication methods (GUEST, KEY, EMAIL) 142 | * added 'Flox.flushLocalData' 143 | * added SharedObject pooling for better performance 144 | * added 'QueueEvent', dispatched on service queue processing 145 | * added ETag handling 146 | * now processing entity queue before any other request is executed 147 | 148 | version 0.3 - 2012-10-31 149 | ------------------------ 150 | 151 | * added Leaderboards 152 | * added 'Flox.reportAnalytics' property 153 | * enhanded 'Flox.logError' method 154 | 155 | 156 | version 0.2 - 2012-10-11 157 | ------------------------ 158 | 159 | * added event properties 160 | * fixed small issues 161 | 162 | 163 | version 0.1 - 2012-09-04 164 | ------------------------ 165 | 166 | * first public version 167 | * analytics 168 | -------------------------------------------------------------------------------- /tests/src/com/gamua/flox/UtilsTest.as: -------------------------------------------------------------------------------- 1 | package com.gamua.flox 2 | { 3 | import com.gamua.flox.utils.DateUtil; 4 | import com.gamua.flox.utils.SHA256; 5 | import com.gamua.flox.utils.cloneObject; 6 | import com.gamua.flox.utils.createUID; 7 | import com.gamua.flox.utils.createURL; 8 | import com.gamua.flox.utils.setTimeout; 9 | 10 | import flash.utils.getTimer; 11 | 12 | import starling.unit.UnitTest; 13 | 14 | public class UtilsTest extends UnitTest 15 | { 16 | public function testCreateUrl():void 17 | { 18 | assertEqual("http://www.gamua.com/test", createURL("http://www.gamua.com/", "/test")); 19 | assertEqual("http://www.gamua.com/test", createURL("http://www.gamua.com", "test")); 20 | assertEqual("a/b/c", createURL("a/", "/b/", "/c")); 21 | 22 | // empty string 23 | assertEqual("a/b", createURL("a", "", "b")); 24 | 25 | // null string 26 | assertEqual("a/b", createURL("a", null, "b")); 27 | 28 | // slash at start and/or end must remain 29 | assertEqual("/a/b/c/", createURL("/a/", "/b/", "/c/")); 30 | } 31 | 32 | public function testDateToString():void 33 | { 34 | var ms:Number = Date.UTC(2012, 8, 3, 14, 36, 2, 9); 35 | var date:Date = new Date(ms); 36 | assertEqual("2012-09-03T14:36:02.009Z", DateUtil.toString(date)); 37 | 38 | date.milliseconds = 88; 39 | assertEqual("2012-09-03T14:36:02.088Z", DateUtil.toString(date)); 40 | 41 | date.milliseconds = 123; 42 | assertEqual("2012-09-03T14:36:02.123Z", DateUtil.toString(date)); 43 | } 44 | 45 | public function testCloneObject():void 46 | { 47 | var object:Object = { 48 | "integer": 15, 49 | "number": 1.5, 50 | "boolean": true, 51 | "complex": { "one": 1, "two": { value: 2 } }, 52 | "array": [ "hugo", false, { dunno: [1, 2, 3] } ] 53 | }; 54 | 55 | var clone:Object = cloneObject(object); 56 | 57 | assert(object != clone); 58 | assertEqualObjects(object, clone); 59 | 60 | var integer:int = 15; 61 | var integerClone:Object = cloneObject(integer); 62 | 63 | assertEqual(integer, integerClone); 64 | } 65 | 66 | public function testCloneObjectWithFilter():void 67 | { 68 | var date:Date = new Date(2013, 1, 1); 69 | var object:Object = { 70 | "integer": 15, 71 | "number": 1.5, 72 | "date": date 73 | }; 74 | 75 | var clone:Object = cloneObject(object, function(object:*):* 76 | { 77 | if (object is Date) return DateUtil.toString(object as Date); 78 | else return null; 79 | }); 80 | 81 | assertEqual(DateUtil.toString(date), clone.date); 82 | } 83 | 84 | public function testCloneObjectWithNonSerializedMetaData():void 85 | { 86 | var object:CustomObject = new CustomObject(); 87 | object.publicVar = 1; 88 | object.publicNonSerializedMember = 2; 89 | object.publicNonSerializedProperty = 3; 90 | 91 | var clone:Object = cloneObject(object); 92 | 93 | assertEqual(clone.publicVar, object.publicVar); 94 | assertFalse("publicNonSerializedMember" in clone); 95 | assertFalse("publicNonSerializedProperty" in clone); 96 | } 97 | 98 | public function testCreateUID():void 99 | { 100 | var length:int; 101 | var uid:String; 102 | var seed:String; 103 | 104 | for (length=0; length<43; ++length) 105 | { 106 | uid = createUID(length); 107 | assertEqual(length, uid.length, "UID does not have the right length"); 108 | } 109 | 110 | for (length=0; length<43; ++length) 111 | { 112 | seed = Math.random().toString(); 113 | uid = createUID(length, seed); 114 | assertEqual(length, uid.length, "UID does not have the right length"); 115 | assertEqual(uid, createUID(length, seed), "UIDs with identical seeds differ"); 116 | } 117 | } 118 | 119 | public function testSetTimeout(onComplete:Function):void 120 | { 121 | var startMoment:int = getTimer(); 122 | setTimeout(onTimeout, 500, 1, "two", 3); 123 | 124 | function onTimeout(a:int, b:String, c:int):void 125 | { 126 | var endMoment:int = getTimer(); 127 | assertEquivalent(endMoment - startMoment, 500, "wrong timeout duration", 100); 128 | assertEqual(1, a); 129 | assertEqual("two", b); 130 | assertEqual(3, c); 131 | onComplete(); 132 | } 133 | } 134 | 135 | public function testSHA256():void 136 | { 137 | var string1:String = "Victor"; 138 | var string2:String = "Victory"; 139 | 140 | var sha1:String = SHA256.hashString(string1); 141 | var sha2:String = SHA256.hashString(string2); 142 | var sha1b:String = SHA256.hashString(string1); 143 | var sha2b:String = SHA256.hashString(string2); 144 | 145 | assertEqual(sha1, "bADm/26mvMasEi7DFOsWdJyojwN2Ct+3Gp3mV1J8Y5Z=", "wrong SHA"); 146 | assertEqual(sha2, "LptaDE5q6wiRzCyONGv51Va9VZAydEG/2ij982/020P=", "wrong SHA"); 147 | 148 | assertEqual(sha1, sha1b, "SHA produced different results from the same String"); 149 | assertEqual(sha2, sha2b, "SHA produced different results from the same String"); 150 | } 151 | } 152 | } 153 | 154 | class CustomObject 155 | { 156 | private var privateVar:int; 157 | public var publicVar:int; 158 | 159 | [NonSerialized] 160 | public var publicNonSerializedMember:int; 161 | 162 | [NonSerialized] 163 | public function get publicNonSerializedProperty():int { return privateVar; } 164 | public function set publicNonSerializedProperty(value:int):void { privateVar = value; } 165 | 166 | public function CustomObject() 167 | { 168 | privateVar = publicVar = publicNonSerializedMember = 0; 169 | } 170 | } -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/GameSession.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Flox AS3 4 | // Copyright 2012 Gamua OG. All Rights Reserved. 5 | // 6 | // ================================================================================================= 7 | 8 | package com.gamua.flox 9 | { 10 | import com.gamua.flox.utils.DateUtil; 11 | import com.gamua.flox.utils.HttpMethod; 12 | 13 | import flash.system.Capabilities; 14 | import flash.utils.clearInterval; 15 | import flash.utils.setInterval; 16 | 17 | /** A Game Session contains the Analytics data of one game. */ 18 | internal class GameSession 19 | { 20 | private var mGameVersion:String; 21 | private var mFirstStartTime:Date; 22 | private var mStartTime:Date; 23 | private var mDuration:int; 24 | private var mLog:Array; 25 | private var mNumErrors:int; 26 | private var mIntervalID:uint; 27 | 28 | /** Do not call this constructor directly, but create sessions via the static 29 | * 'start' method instead. */ 30 | public function GameSession(lastSession:GameSession=null, gameVersion:String=null) 31 | { 32 | mFirstStartTime = lastSession ? lastSession.firstStartTime : new Date(); 33 | mGameVersion = gameVersion; 34 | mStartTime = new Date(); 35 | mDuration = 0; 36 | mLog = []; 37 | mNumErrors = 0; 38 | mIntervalID = 0; 39 | } 40 | 41 | /** Starts a new session and closes the previous one. This will send the analytics of 42 | * both sessions to the server (including log entries of the last session). 43 | * @returns the new GameSession. */ 44 | public static function start(gameID:String, gameVersion:String, installationID:String, 45 | reportAnalytics:Boolean, lastSession:GameSession=null):GameSession 46 | { 47 | var newSession:GameSession = new GameSession(lastSession, gameVersion); 48 | var resolution:String = Capabilities.screenResolutionX + "x" + 49 | Capabilities.screenResolutionY; 50 | 51 | var data:Object = { 52 | installationId: installationID, 53 | startTime: DateUtil.toString(newSession.mStartTime), 54 | gameVersion: gameVersion, 55 | languageCode: Capabilities.language, 56 | deviceInfo: { 57 | resolution: resolution, 58 | os: Capabilities.os, 59 | flashPlayerType: Capabilities.playerType, 60 | flashPlayerVersion: Capabilities.version 61 | } 62 | }; 63 | 64 | if (lastSession) 65 | { 66 | lastSession.pause(); 67 | data.firstStartTime = DateUtil.toString(lastSession.firstStartTime); 68 | data.lastStartTime = DateUtil.toString(lastSession.startTime); 69 | data.lastDuration = lastSession.duration; 70 | data.lastLog = lastSession.log; 71 | } 72 | 73 | if (reportAnalytics) 74 | Flox.service.requestQueued(HttpMethod.POST, "logs", data); 75 | 76 | newSession.start(); 77 | return newSession; 78 | } 79 | 80 | /** (Re)starts the timer reporting the duration of a session. This is done automatically 81 | * by the static 'start' method as well. */ 82 | public function start():void 83 | { 84 | if (mIntervalID == 0) 85 | mIntervalID = setInterval(function():void { ++mDuration }, 1000); 86 | } 87 | 88 | /** Stops the timer that reports the duration of a session. */ 89 | public function pause():void 90 | { 91 | clearInterval(mIntervalID); 92 | mIntervalID = 0; 93 | } 94 | 95 | // logging 96 | 97 | /** Adds a log of type 'info'. */ 98 | public function logInfo(message:String):void 99 | { 100 | addLogEntry("info", { message: message }); 101 | } 102 | 103 | /** Adds a log of type 'warning'. */ 104 | public function logWarning(message:String):void 105 | { 106 | addLogEntry("warning", { message: message }); 107 | } 108 | 109 | /** Adds a log of type 'error'. */ 110 | public function logError(name:String, message:String=null, stacktrace:String=null):void 111 | { 112 | addLogEntry("error", { name: name, message: message, stacktrace: stacktrace }); 113 | mNumErrors++; 114 | } 115 | 116 | /** Adds a log of type 'event', with an optional dictionary of additional data. */ 117 | public function logEvent(name:String, properties:Object=null):void 118 | { 119 | var entry:Object = { name: name }; 120 | if (properties !== null) entry.properties = properties; 121 | addLogEntry("event", entry); 122 | } 123 | 124 | private function addLogEntry(type:String, entry:Object):void 125 | { 126 | entry.type = type; 127 | entry.time = DateUtil.toString(new Date()); 128 | mLog.push(entry); 129 | } 130 | 131 | // properties 132 | // since this class is saved in a SharedObject, everything has to be R/W! 133 | 134 | /** The exact time the session was started. */ 135 | public function get startTime():Date { return mStartTime; } 136 | public function set startTime(value:Date):void { mStartTime = value; } 137 | 138 | /** The time the very first session was started on this device. */ 139 | public function get firstStartTime():Date { return mFirstStartTime || new Date(); } 140 | public function set firstStartTime(value:Date):void { mFirstStartTime = value; } 141 | 142 | /** The duration of the session in seconds. */ 143 | public function get duration():int { return mDuration; } 144 | public function set duration(value:int):void { mDuration = value; } 145 | 146 | /** An array of all log entries in chronological order. */ 147 | public function get log():Array { return mLog; } 148 | public function set log(value:Array):void { mLog = value; } 149 | 150 | /** The number of reported errors (via the 'logError' method). */ 151 | public function get numErrors():int { return mNumErrors; } 152 | public function set numErrors(value:int):void { mNumErrors = value; } 153 | 154 | /** The game version of this session. */ 155 | public function get gameVersion():String { return mGameVersion; } 156 | public function set gameVersion(value:String):void { mGameVersion = value; } 157 | } 158 | } -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/utils/Base64.as: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a modified version of the Base64 class from the AS3 Crypto Library: 3 | * -> http://code.google.com/p/as3crypto/ 4 | * 5 | * 6 | * Base64 - 1.1.0 7 | * 8 | * Copyright (c) 2006 Steve Webster 9 | * 10 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 11 | * this software and associated documentation files (the "Software"), to deal in 12 | * the Software without restriction, including without limitation the rights to 13 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 14 | * the Software, and to permit persons to whom the Software is furnished to do so, 15 | * subject to the following conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be included in all 18 | * copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 22 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 23 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 24 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | */ 27 | package com.gamua.flox.utils 28 | { 29 | import flash.utils.ByteArray; 30 | 31 | /** Utility class to encode and decode data from and to Base64 format. */ 32 | public class Base64 33 | { 34 | private static const BASE64_CHARS:String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 35 | 36 | /** helper objects */ 37 | private static var sOutputBuilder:ByteArray = new ByteArray(); 38 | private static var sDataBuffer:Vector. = new []; 39 | private static var sOutputBuffer:Vector. = new []; 40 | 41 | /** Encodes a given String in Base64 format. */ 42 | public static function encode(data:String):String 43 | { 44 | // Convert string to ByteArray 45 | var bytes:ByteArray = new ByteArray(); 46 | bytes.writeUTFBytes(data); 47 | 48 | // Return encoded ByteArray 49 | return encodeByteArray(bytes); 50 | } 51 | 52 | /** Encodes a given ByteArray into a Base64 representation. */ 53 | public static function encodeByteArray(data:ByteArray):String 54 | { 55 | // Initialise output 56 | var output:String; 57 | 58 | // Rewind ByteArray 59 | data.position = 0; 60 | sDataBuffer[2] = sDataBuffer[1] = sDataBuffer[0] = 0; 61 | 62 | // while there are still bytes to be processed 63 | while (data.bytesAvailable > 0) 64 | { 65 | var numBytes:int = data.bytesAvailable >= 3 ? 3: data.bytesAvailable; 66 | 67 | // Create new data buffer and populate next 3 bytes from data 68 | for (var i:uint = 0; i < numBytes; ++i) 69 | sDataBuffer[i] = data.readUnsignedByte(); 70 | 71 | // Convert to data buffer Base64 character positions and 72 | // store in output buffer 73 | sOutputBuffer[0] = ( sDataBuffer[0] & 0xfc) >> 2; 74 | sOutputBuffer[1] = ((sDataBuffer[0] & 0x03) << 4) | ((sDataBuffer[1]) >> 4); 75 | sOutputBuffer[2] = ((sDataBuffer[1] & 0x0f) << 2) | ((sDataBuffer[2]) >> 6); 76 | sOutputBuffer[3] = sDataBuffer[2] & 0x3f; 77 | 78 | // If data buffer was short (i.e not 3 characters) then set 79 | // end character indexes in data buffer to index of '=' symbol. 80 | // This is necessary because Base64 data is always a multiple of 81 | // 4 bytes and is basses with '=' symbols. 82 | for (var j:uint = numBytes; j < 3; j++) 83 | sOutputBuffer[int(j + 1)] = 64; 84 | 85 | // Loop through output buffer and add Base64 characters to 86 | // encoded data string for each character. 87 | for (var k:uint = 0; k < 4; k++) 88 | sOutputBuilder.writeUTFBytes(BASE64_CHARS.charAt(sOutputBuffer[k])); 89 | } 90 | 91 | // Read output string 92 | sOutputBuilder.position = 0; 93 | output = sOutputBuilder.readUTFBytes(sOutputBuilder.length); 94 | 95 | // Clear temporary buffers 96 | sOutputBuilder.length = sOutputBuffer.length = sDataBuffer.length = 0; 97 | 98 | // Return encoded data 99 | return output; 100 | } 101 | 102 | /** Decodes a given Base64-String into the String it was created from. */ 103 | public static function decode(data:String):String 104 | { 105 | // Decode data to ByteArray 106 | var bytes:ByteArray = decodeToByteArray(data); 107 | 108 | // Convert to string and return 109 | return bytes.readUTFBytes(bytes.length); 110 | } 111 | 112 | /** Decodes a given Base64-String into the ByteArray it represents. If you pass an 113 | * 'output' ByteArray to the function, the result will be saved into that. */ 114 | public static function decodeToByteArray(data:String, output:ByteArray=null):ByteArray 115 | { 116 | var dataLength:int = data.length; 117 | 118 | // Initialise output ByteArray for decoded data 119 | if (output != null) output.length = 0; 120 | else output = new ByteArray(); 121 | 122 | // While there are data bytes left to be processed 123 | for (var i:uint = 0; i < dataLength; i += 4) 124 | { 125 | // Populate data buffer with position of Base64 characters for 126 | // next 4 bytes from encoded data 127 | for (var j:uint = 0; j < 4 && i + j < dataLength; j++) 128 | sDataBuffer[j] = BASE64_CHARS.indexOf(data.charAt(i + j)); 129 | 130 | // Decode data buffer back into bytes 131 | sOutputBuffer[0] = (sDataBuffer[0] << 2) + ((sDataBuffer[1] & 0x30) >> 4); 132 | sOutputBuffer[1] = ((sDataBuffer[1] & 0x0f) << 4) + ((sDataBuffer[2] & 0x3c) >> 2); 133 | sOutputBuffer[2] = ((sDataBuffer[2] & 0x03) << 6) + sDataBuffer[3]; 134 | 135 | // Add all non-padded bytes in output buffer to decoded data 136 | for (var k:uint = 0; k < 3; k++) 137 | { 138 | if (sDataBuffer[int(k+1)] == 64) break; 139 | output.writeByte(sOutputBuffer[k]); 140 | } 141 | } 142 | 143 | // Rewind decoded data ByteArray 144 | output.position = 0; 145 | 146 | // Clear temporary buffers 147 | sOutputBuffer.length = sDataBuffer.length = 0; 148 | 149 | // Return decoded data 150 | return output; 151 | } 152 | 153 | /** @private */ 154 | public function Base64() 155 | { 156 | throw new Error("Base64 class is static container only"); 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /tests/src/com/gamua/flox/AccessTest.as: -------------------------------------------------------------------------------- 1 | package com.gamua.flox 2 | { 3 | import com.gamua.flox.utils.CustomEntity; 4 | import com.gamua.flox.utils.HttpStatus; 5 | import com.gamua.flox.utils.createUID; 6 | 7 | import starling.unit.UnitTest; 8 | 9 | public class AccessTest extends UnitTest 10 | { 11 | private static const KEY_1:String = "key1"; 12 | private static const KEY_2:String = "key2"; 13 | 14 | public override function setUp():void 15 | { 16 | Constants.initFlox(); 17 | Player.logout(); 18 | } 19 | 20 | public override function tearDown():void 21 | { 22 | Flox.shutdown(); 23 | } 24 | 25 | public function testModificationWithAccessNone(onComplete:Function):void 26 | { 27 | makeModificationTest(Access.NONE, onComplete); 28 | } 29 | 30 | public function testModificationWithAccessRead(onComplete:Function):void 31 | { 32 | makeModificationTest(Access.READ, onComplete); 33 | } 34 | 35 | public function testModificationWithAccessReadWrite(onComplete:Function):void 36 | { 37 | makeModificationTest(Access.READ_WRITE, onComplete); 38 | } 39 | 40 | public function makeModificationTest(access:String, onComplete:Function):void 41 | { 42 | var entity:CustomEntity = null; 43 | Player.loginWithKey(KEY_1, onLoginPlayer1Complete, onError); 44 | 45 | function onLoginPlayer1Complete(player:Player):void 46 | { 47 | assertEqual(AuthenticationType.KEY, player.authType); 48 | 49 | entity = new CustomEntity("Gandalf", int(Math.random() * 1000)); 50 | entity.publicAccess = access; 51 | entity.save(onEntitySaved, onError); 52 | } 53 | 54 | function onEntitySaved(entity:CustomEntity):void 55 | { 56 | Player.loginWithKey(KEY_2, onLoginPlayer2Complete, onError); 57 | } 58 | 59 | function onLoginPlayer2Complete(player:Player):void 60 | { 61 | Entity.load(CustomEntity, entity.id, onEntityLoadComplete, onEntityLoadError); 62 | } 63 | 64 | function onEntityLoadComplete(entity:CustomEntity):void 65 | { 66 | if (access == Access.NONE) 67 | { 68 | fail("Could load entity that was not publicly accessible"); 69 | onComplete(); 70 | } 71 | else if (access == Access.READ || access == Access.READ_WRITE) 72 | { 73 | entity.name = "Saruman"; 74 | entity.save(onEntitySaveComplete, onEntitySaveError); 75 | } 76 | } 77 | 78 | function onEntitySaveComplete():void 79 | { 80 | if (access == Access.READ) 81 | fail("Could save READ-only entity"); 82 | 83 | onComplete(); 84 | } 85 | 86 | function onEntitySaveError(error:String):void 87 | { 88 | if (access == Access.READ_WRITE) 89 | fail("Could not modify READ_WRITE entity: " + error); 90 | 91 | onComplete(); 92 | } 93 | 94 | function onEntityLoadError(error:String, httpStatus:int, cachedEntity:Entity):void 95 | { 96 | if (access == Access.NONE) 97 | assertFalse(HttpStatus.isTransientError(httpStatus)); 98 | else 99 | fail("Could not load entity with '" + access + "' access: " + error); 100 | 101 | onComplete(); 102 | } 103 | 104 | function onError(error:String, httpStatus:int):void 105 | { 106 | fail("Entity handling failed: " + error); 107 | onComplete(); 108 | } 109 | } 110 | 111 | public function testDestructionWithAccessNone(onComplete:Function):void 112 | { 113 | makeDestructionTest(Access.NONE, onComplete); 114 | } 115 | 116 | public function testDestructionWithAccessRead(onComplete:Function):void 117 | { 118 | makeDestructionTest(Access.READ, onComplete); 119 | } 120 | 121 | public function testDestructionWithAccessReadWrite(onComplete:Function):void 122 | { 123 | makeDestructionTest(Access.READ_WRITE, onComplete); 124 | } 125 | 126 | public function makeDestructionTest(access:String, onComplete:Function):void 127 | { 128 | Player.logout(); // login new guest 129 | 130 | var entity:CustomEntity = new CustomEntity("Sauron", int(Math.random() * 1000)); 131 | entity.publicAccess = access; 132 | entity.save(onEntitySaved, onError); 133 | 134 | function onEntitySaved(entity:CustomEntity):void 135 | { 136 | Player.logout(); // login new guest 137 | Entity.destroy(CustomEntity, entity.id, onDestroyComplete, onDestroyError); 138 | } 139 | 140 | function onDestroyComplete():void 141 | { 142 | if (access != Access.READ_WRITE) 143 | fail("could destroy entity even though access rights were " + access); 144 | 145 | onComplete(); 146 | } 147 | 148 | function onDestroyError(error:String, httpStatus:int):void 149 | { 150 | if (access == Access.READ_WRITE) 151 | fail("could not destroy entity even though access rights were " + access); 152 | 153 | onComplete(); 154 | } 155 | 156 | function onError(error:String, httpStatus:int):void 157 | { 158 | fail("Entity handling failed: " + error); 159 | onComplete(); 160 | } 161 | } 162 | 163 | public function testChangeOwnership(onComplete:Function):void 164 | { 165 | var name:String = createUID(); 166 | var entity:Entity = new CustomEntity(name, 32); 167 | entity.publicAccess = Access.NONE; 168 | entity.save(onSaveComplete, onError); 169 | 170 | function onSaveComplete():void 171 | { 172 | entity.ownerId = createUID(); 173 | entity.save(onSave2Complete, onError); 174 | } 175 | 176 | function onSave2Complete():void 177 | { 178 | var query:Query = new Query(CustomEntity, "name == ?", name); 179 | query.find(onQueryComplete, onError); 180 | } 181 | 182 | function onQueryComplete(entities:Array):void 183 | { 184 | assertEqual(0, entities.length, "received result from query, but shouldn't"); 185 | onComplete(); 186 | } 187 | 188 | function onError(error:String):void 189 | { 190 | fail(error); 191 | onComplete(); 192 | } 193 | } 194 | } 195 | } 196 | 197 | import com.gamua.flox.Player; 198 | 199 | class CustomPlayer extends Player 200 | { 201 | private var mLastName:String; 202 | 203 | public function CustomPlayer(lastName:String="unknown") 204 | { 205 | mLastName = lastName; 206 | } 207 | 208 | public function get lastName():String { return mLastName; } 209 | public function set lastName(value:String):void { mLastName = value; } 210 | } -------------------------------------------------------------------------------- /flox/src/com/gamua/flox/utils/SHA256.as: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a modified version of the SHA256 class from the AS3 Crypto Library: 3 | * -> http://code.google.com/p/as3crypto/ 4 | * 5 | * 6 | * Copyright (c) 2007 Henri Torgemane 7 | * All Rights Reserved. 8 | * 9 | * MD5, SHA1, and SHA256 are derivative works (http://pajhome.org.uk/crypt/md5/) 10 | * Those are Copyright (c) 1998-2002 Paul Johnston & Contributors (paj@pajhome.org.uk) 11 | * 12 | * SHA256 is a derivative work of jsSHA2 (http://anmar.eu.org/projects/jssha2/) 13 | * jsSHA2 is Copyright (c) 2003-2004 Angel Marin (anmar@gmx.net) 14 | * 15 | * Base64 is copyright (c) 2006 Steve Webster (http://dynamicflash.com/goodies/base64) 16 | * 17 | * Redistribution and use in source and binary forms, with or without modification, 18 | * are permitted provided that the following conditions are met: 19 | * 20 | * Redistributions of source code must retain the above copyright notice, this list 21 | * of conditions and the following disclaimer. Redistributions in binary form must 22 | * reproduce the above copyright notice, this list of conditions and the following 23 | * disclaimer in the documentation and/or other materials provided with the distribution. 24 | * 25 | * Neither the name of the author nor the names of its contributors may be used to endorse 26 | * or promote products derived from this software without specific prior written permission. 27 | * 28 | * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, 29 | * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY 30 | * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. 31 | * 32 | * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, 33 | * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER 34 | * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF 35 | * THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT 36 | * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 37 | * 38 | */ 39 | package com.gamua.flox.utils 40 | { 41 | import flash.utils.ByteArray; 42 | import flash.utils.Endian; 43 | 44 | /** Utility class providing SHA-256 hashing. */ 45 | public class SHA256 46 | { 47 | private static const HASH_SIZE:int = 32; 48 | 49 | private static const K:Array = [ 50 | 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 51 | 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 52 | 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 53 | 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 54 | 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 55 | 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 56 | 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 57 | 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 58 | 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 59 | 0xc67178f2]; 60 | 61 | private static const H:Array = [ 62 | 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 63 | 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 64 | ]; 65 | 66 | /** Helper variables. */ 67 | private static const sInputBytes:ByteArray = new ByteArray(); 68 | private static const sOutputBytes:ByteArray = new ByteArray(); 69 | 70 | /** @private */ 71 | public function SHA256() 72 | { 73 | throw new Error("SHA256 is static container only"); 74 | } 75 | 76 | /** Creates the SHA-256 hash of the String and returns its Base64 representation. */ 77 | public static function hashString(src:String):String 78 | { 79 | var result:String; 80 | sInputBytes.writeUTFBytes(src); 81 | hash(sInputBytes, sOutputBytes); 82 | result = Base64.encodeByteArray(sOutputBytes); 83 | sInputBytes.length = sOutputBytes.length = 0; 84 | return result; 85 | } 86 | 87 | /** Creates the SHA-256 hash of the String. If you pass an 'output' ByteArray to the 88 | * function, the result will be saved into that. */ 89 | public static function hash(src:ByteArray, output:ByteArray=null):ByteArray 90 | { 91 | if (output == null) output = new ByteArray(); 92 | else output.position = 0; 93 | 94 | var savedLength:uint = src.length; 95 | var savedEndian:String = src.endian; 96 | var len:uint = savedLength * 8; 97 | var a:Array = []; 98 | 99 | src.endian = Endian.BIG_ENDIAN; 100 | 101 | // pad to nearest int. 102 | while (src.length % 4 != 0) 103 | src[src.length] = 0; 104 | 105 | // convert ByteArray to an array of uint 106 | src.position = 0; 107 | 108 | for (var i:uint=0; i> 5] |= 0x80 << (24 - len % 32); 129 | x[((len + 64 >> 9) << 4) + 15] = len; 130 | 131 | var w:Array = []; 132 | var a:uint = H[0]; 133 | var b:uint = H[1]; 134 | var c:uint = H[2]; 135 | var d:uint = H[3]; 136 | var e:uint = H[4]; 137 | var f:uint = H[5]; 138 | var g:uint = H[6]; 139 | var h:uint = H[7]; 140 | 141 | for (var i:uint=0, xl:uint=x.length; i < xl; i += 16) 142 | { 143 | var olda:uint = a; 144 | var oldb:uint = b; 145 | var oldc:uint = c; 146 | var oldd:uint = d; 147 | var olde:uint = e; 148 | var oldf:uint = f; 149 | var oldg:uint = g; 150 | var oldh:uint = h; 151 | 152 | for (var j:uint=0; j<64; j++) 153 | { 154 | if (j<16) 155 | w[j] = x[i+j] || 0; 156 | else 157 | { 158 | var s0:uint = rrol(w[j-15], 7) ^ rrol(w[j-15], 18) ^ (w[j-15] >>> 3); 159 | var s1:uint = rrol(w[j- 2], 17) ^ rrol(w[j- 2], 19) ^ (w[j- 2] >>> 10); 160 | w[j] = w[j - 16] + s0 + w[j-7] + s1; 161 | } 162 | 163 | var t2:uint = (rrol(a, 2) ^ rrol(a, 13) ^ rrol(a, 22)) + ((a&b) ^ (a&c) ^ (b&c)); 164 | var t1:uint = h + (rrol(e, 6) ^ rrol(e, 11) ^ rrol(e, 25)) + ((e&f) ^ (g&~e)) + K[j] + w[j]; 165 | 166 | h = g; 167 | g = f; 168 | f = e; 169 | e = d + t1; 170 | d = c; 171 | c = b; 172 | b = a; 173 | a = t1 + t2; 174 | } 175 | 176 | a += olda; 177 | b += oldb; 178 | c += oldc; 179 | d += oldd; 180 | e += olde; 181 | f += oldf; 182 | g += oldg; 183 | h += oldh; 184 | } 185 | 186 | return [ a, b, c, d, e, f, g, h ]; 187 | } 188 | 189 | /** Bitwise rotate a 32-bit number to the right. */ 190 | private static function rrol(num:uint, cnt:uint):uint 191 | { 192 | return (num << (32-cnt)) | (num >>> cnt); 193 | } 194 | } 195 | } -------------------------------------------------------------------------------- /tests/src/com/gamua/flox/ScoreTest.as: -------------------------------------------------------------------------------- 1 | package com.gamua.flox 2 | { 3 | import com.gamua.flox.events.QueueEvent; 4 | 5 | import flash.events.Event; 6 | 7 | import starling.unit.UnitTest; 8 | 9 | public class ScoreTest extends UnitTest 10 | { 11 | public override function setUp():void 12 | { 13 | Constants.initFlox(); 14 | } 15 | 16 | public override function tearDown():void 17 | { 18 | Flox.shutdown(); 19 | } 20 | 21 | public function testConstruction():void 22 | { 23 | var now:Date = new Date(); 24 | var score:Score = new Score(); 25 | 26 | assertNotNull(score.playerId); 27 | assertNotNull(score.playerName); 28 | assertNotNull(score.country); 29 | assertEqual(0, score.value); 30 | assertEquivalent(now.time, score.date.time, "wrong default date", 100); 31 | 32 | score = new Score("id", "name", 123, now, "at"); 33 | 34 | assertEqual("id", score.playerId); 35 | assertEqual("name", score.playerName); 36 | assertEqual("at", score.country); 37 | assertEqual(123, score.value); 38 | assertEqual(now, score.date); 39 | } 40 | 41 | public function testProperties():void 42 | { 43 | var now:Date = new Date(); 44 | var score:Score = new Score(); 45 | 46 | score.playerId = "123"; 47 | assertEqual("123", score.playerId); 48 | 49 | score.playerName = "playerName"; 50 | assertEqual("playerName", score.playerName); 51 | 52 | score.country = "at"; 53 | assertEqual("at", score.country); 54 | 55 | score.value = 123; 56 | assertEqual(123, score.value); 57 | 58 | score.date = now; 59 | assertEqual(now, score.date); 60 | } 61 | 62 | public function testSubmitScores():void 63 | { 64 | var score:int = Math.random() * 1000; 65 | Flox.postScore(Constants.LEADERBOARD_ID, score, "hugo"); 66 | } 67 | 68 | public function testJsonName(onComplete:Function):void 69 | { 70 | var leaderboardID:String = "json"; 71 | var date:Date = new Date(); 72 | var score:int = int(date.time / 10000); 73 | var data:Object = { name: "hugo", score: score }; 74 | 75 | Flox.addEventListener(QueueEvent.QUEUE_PROCESSED, onQueueProcessed); 76 | Flox.postScore(leaderboardID, score, JSON.stringify(data)); 77 | 78 | function onQueueProcessed(event:Event):void 79 | { 80 | Flox.removeEventListener(QueueEvent.QUEUE_PROCESSED, onQueueProcessed); 81 | Flox.loadScores(leaderboardID, TimeScope.ALL_TIME, onScoresLoaded, onScoresError); 82 | } 83 | 84 | function onScoresLoaded(scores:Array):void 85 | { 86 | assert(scores[0].value == score); 87 | onComplete(); 88 | } 89 | 90 | function onScoresError(error:String):void 91 | { 92 | fail("Could not load score with JSON player name: " + error); 93 | onComplete(); 94 | } 95 | } 96 | 97 | public function testRetrieveScores(onComplete:Function):void 98 | { 99 | var serverScores:Array; 100 | 101 | Flox.clearCache(); 102 | Flox.loadScores(Constants.LEADERBOARD_ID, TimeScope.ALL_TIME, 103 | onScoresLoaded, onScoresError); 104 | 105 | function onScoresLoaded(scores:Array):void 106 | { 107 | assertNotNull(scores, "retrieved 'null' scores"); 108 | serverScores = scores; 109 | 110 | // now try again and get scores from cache 111 | Flox.service.alwaysFail = true; 112 | Flox.loadScores(Constants.LEADERBOARD_ID, TimeScope.ALL_TIME, 113 | onCacheScoresLoaded, onCacheScoresError); 114 | } 115 | 116 | function onScoresError(error:String):void 117 | { 118 | fail("Could not get scores from server: " + error); 119 | onComplete(); 120 | } 121 | 122 | function onCacheScoresLoaded(scores:Array):void 123 | { 124 | fail("Received scores even though 'alwaysFail' is enabled"); 125 | onComplete(); 126 | } 127 | 128 | function onCacheScoresError(error:String, cachedScores:Array):void 129 | { 130 | assertNotNull(cachedScores, "retrieved 'null' scores"); 131 | assertEqualObjects(cachedScores, serverScores); 132 | onComplete(); 133 | } 134 | } 135 | 136 | public function testPostAndLoadScoresWithDifferentNames(onComplete:Function):void 137 | { 138 | var highscore:int; 139 | var leaderboardID:String = Constants.LEADERBOARD_ID; 140 | Flox.postScore(leaderboardID, 100, "Tony"); 141 | Flox.loadScores(leaderboardID, TimeScope.THIS_WEEK, onLoadScoresComplete, onError); 142 | 143 | function onLoadScoresComplete(scores:Array):void 144 | { 145 | assert(scores.length > 0, "didn't receive any score"); 146 | highscore = scores[0].value; 147 | 148 | Flox.postScore(leaderboardID, highscore + 1, "Tony"); 149 | Flox.postScore(leaderboardID, highscore + 1, "Tina"); 150 | 151 | Flox.loadScores(leaderboardID, TimeScope.THIS_WEEK, 152 | onLoadMoreScoresComplete, onError); 153 | } 154 | 155 | function onLoadMoreScoresComplete(scores:Array):void 156 | { 157 | var tony:Score = scores[0].playerName == "Tony" ? scores[0] : scores[1]; 158 | var tina:Score = scores[0].playerName == "Tina" ? scores[0] : scores[1]; 159 | 160 | assertEqual(tony.value, tina.value, "wrong scores"); 161 | assertEqual(highscore + 1, tony.value, "wrong score"); 162 | assertEqual(tony.playerName, "Tony", "wrong name"); 163 | assertEqual(tina.playerName, "Tina", "wrong name"); 164 | assertEqual(2, tina.country.length, "wrong country code"); 165 | assertEqual(2, tony.country.length, "wrong country code"); 166 | onComplete(); 167 | } 168 | 169 | function onError(error:String):void 170 | { 171 | fail("error loading leaderboard: " + error); 172 | onComplete(); 173 | } 174 | } 175 | 176 | public function testOffline(onComplete:Function):void 177 | { 178 | Flox.service.alwaysFail = true; 179 | Flox.clearCache(); 180 | Flox.loadScores(Constants.LEADERBOARD_ID, TimeScope.ALL_TIME, 181 | onScoresLoaded, onScoresError); 182 | 183 | function onScoresLoaded(scores:Array):void 184 | { 185 | fail("Received scores even though 'alwaysFail' is enabled"); 186 | onComplete(); 187 | } 188 | 189 | function onScoresError(error:String, cachedScores:Array):void 190 | { 191 | assertNull(cachedScores); 192 | onComplete(); 193 | } 194 | } 195 | 196 | public function testLeaderboardOfFriends(onComplete:Function):void 197 | { 198 | var leaderboardID:String = Constants.LEADERBOARD_ID; 199 | var playerIDs:Array = []; 200 | var values:Array = [100, 80, 60]; 201 | var names:Array = ["first", "second", "third"]; 202 | 203 | for (var i:int=0; iBefore you can make a query, you have to create indices that match the query. You can 20 | * do that in the Flox online interface. An index has to contain all the properties that are 21 | * referenced in the constraints.

22 | * 23 | *

Here is an example of how you can execute a Query with Flox. This query requires 24 | * an index containing both "level" and "score" properties.

25 | *
 26 |      *  var query:Query = new Query(Player);
 27 |      *  query.where("level == ? AND score > ?", "tutorial", 500);
 28 |      *  query.find(function onComplete(players:Array):void
 29 |      *  {
 30 |      *      // the 'players' array contains all players in the 'tutorial' level
 31 |      *      // with a score higher than 500. 
 32 |      *  },
 33 |      *  function onError(error:String):void
 34 |      *  {
 35 |      *      trace("something went wrong: " + error);
 36 |      *  });
37 | */ 38 | public class Query 39 | { 40 | private var mClass:Class; 41 | private var mOffset:int; 42 | private var mLimit:int; 43 | private var mConstraints:String; 44 | private var mOrderBy:String; 45 | 46 | /** Create a new query that will search within the given Entity type. Optionally, you 47 | * pass the constraints in the same way as in the "where" method. */ 48 | public function Query(entityClass:Class, constraints:String=null, ...args) 49 | { 50 | mClass = entityClass; 51 | mOffset = 0; 52 | mLimit = 50; 53 | 54 | if (constraints) 55 | { 56 | args.unshift(constraints); 57 | where.apply(this, args); 58 | } 59 | } 60 | 61 | /** You can narrow down the results of the query with an SQL like where-clause. The 62 | * constraints string supports the following comparison operators: 63 | * "==, >, >=, <, <=, !=". 64 | * You can combine constraints using "AND" and "OR"; construct logical groups with 65 | * round brackets. 66 | * 67 | *

To simplify creation of the constraints string, you can use questions marks ("?") 68 | * as placeholders. They will be replaced one by one with the additional parameters you 69 | * pass to the method, while making sure their format is correct (e.g. it surrounds 70 | * Strings with quotations marks). Here is an example:

71 | *
 72 |          *  query.where("name == ? AND score > ?", "thomas", 500); 
 73 |          *  // -> name == "thomas" AND score > 500
74 | * 75 | *

Use the 'IN'-operator to check for inclusion within a list of possible values:

76 | *
 77 |          *  query.where("name IN ?", ["alfa", "bravo", "charlie"]);
 78 |          *  // -> name IN ["alfa", "bravo", "charlie"]
79 | * 80 | *

Note that subsequent calls to this method will replace preceding constraints.

81 | * 82 | * @returns the final constraints-string that will be passed to the server. 83 | */ 84 | public function where(constraints:String, ...args):String 85 | { 86 | var regEx:RegExp = /\?/g; 87 | var match:Object; 88 | var lastIndex:int = -1; 89 | mConstraints = ""; 90 | 91 | while ((match = regEx.exec(constraints)) != null) 92 | { 93 | if (args.length == 0) throw new ArgumentError("Incorrect number of placeholders"); 94 | 95 | var arg:* = args.shift(); 96 | if (arg is Date) arg = DateUtil.toString(arg); 97 | 98 | mConstraints += constraints.substr(lastIndex + 1, match.index - lastIndex - 1); 99 | mConstraints += JSON.stringify(arg); 100 | lastIndex = match.index; 101 | } 102 | 103 | mConstraints += constraints.substr(lastIndex + 1); 104 | return mConstraints; 105 | } 106 | 107 | /** Executes the query and passes the list of results to the "onComplete" callback. 108 | * Don't forget to create appropriate indices for your queries! 109 | * 110 | * @param onComplete a callback with the form: 111 | *
onComplete(entities:Array):void;
112 | * @param onError a callback with the form: 113 | *
onError(error:String, httpStatus:int):void;
114 | */ 115 | public function find(onComplete:Function, onError:Function):void 116 | { 117 | var entities:Array = []; 118 | var numResults:int = -1; 119 | var numLoaded:int = 0; 120 | var abort:Boolean = false; 121 | 122 | var path:String = createURL("entities", type); 123 | var data:Object = { where: mConstraints, offset: mOffset, limit: mLimit }; 124 | 125 | if (mOrderBy) data.orderBy = mOrderBy; 126 | 127 | Flox.service.request(HttpMethod.POST, path, data, onRequestComplete, onError); 128 | 129 | function onRequestComplete(body:Object, httpStatus:int):void 130 | { 131 | var results:Array = body as Array; 132 | numResults = results ? results.length : 0; 133 | 134 | for (var i:int=0; ionComplete(entityIDs:Array):void; 208 | * @param onError a callback with the form: 209 | *
onError(error:String, httpStatus:int):void;
210 | */ 211 | public function findIDs(onComplete:Function, onError:Function):void 212 | { 213 | var numResults:int = -1; 214 | var path:String = createURL("entities", type); 215 | var data:Object = { where: mConstraints, offset: mOffset, limit: mLimit }; 216 | 217 | if (mOrderBy) data.orderBy = mOrderBy; 218 | 219 | Flox.service.request(HttpMethod.POST, path, data, onRequestComplete, onError); 220 | 221 | function onRequestComplete(body:Object):void 222 | { 223 | var entityIDs:Array = []; 224 | var results:Array = body as Array; 225 | numResults = results ? results.length : 0; 226 | 227 | for (var i:int=0; iDo not create player instances yourself; instead, always use the player objects returned 20 | * by 'Player.current'. A guest player is created automatically for you on first start 21 | * (as a guest player).

22 | * 23 | *

The current player is automatically persisted, i.e. when you close and restart your game, 24 | * the same player will be logged in automatically.

25 | * 26 | *

In many games, you'll want to use a custom player subclass, so that you can add custom 27 | * properties. To do that, register your player class before starting Flox.

28 | *
 29 |      *  Flox.playerClass = CustomPlayer;
30 | * 31 | *

When you've done that, you can get your player anytime with this code:

32 | *
 33 |      *  var player:CustomPlayer = Player.current as CustomPlayer;
34 | */ 35 | [Type(".player")] 36 | public class Player extends Entity 37 | { 38 | private var mAuthType:String; 39 | 40 | /** Don't call this method directly; use the 'Player.login' methods instead. */ 41 | public function Player() 42 | { 43 | super.ownerId = this.id; 44 | super.publicAccess = Access.READ; 45 | } 46 | 47 | /** Log in a player with the given authentication information. If you pass no 48 | * parameters, a new guest will be logged in; the 'Flox.currentPlayer' parameter will 49 | * immediately reference that player. 50 | * 51 | *

Flox requires that there's always a player logged in. Thus, there is no 'logout' 52 | * method. If you want to remove the reference to the current player, just call 53 | * 'login' again (without arguments). The previous player will then be replaced 54 | * by a new guest.

55 | * 56 | * @param authType The type of authentication you want to use. 57 | * @param authId The id of the player in its authentication system. 58 | * @param authToken The token you received from the player's authentication system. 59 | * @param onComplete function onComplete(currentPlayer:Player):void; 60 | * @param onError function onError(error:String, httpStatus:int):void; 61 | */ 62 | public static function login( 63 | authType:String="guest", authId:String=null, authToken:String=null, 64 | onComplete:Function=null, onError:Function=null):void 65 | { 66 | var authData:Object = { authType: authType, authId: authId, authToken: authToken }; 67 | loginWithAuthData(authData, onComplete, onError); 68 | } 69 | 70 | private static function loginWithAuthData(authData:Object, onComplete:Function=null, 71 | onError:Function=null):void 72 | { 73 | Flox.checkInitialized(); 74 | var previousAuthentication:Authentication = Flox.authentication; 75 | 76 | if (authData.authType == AuthenticationType.GUEST) 77 | { 78 | var player:Player = new Flox.playerClass(); 79 | player.authType = AuthenticationType.GUEST; 80 | 81 | onAuthenticated(player); 82 | } 83 | else 84 | { 85 | if (current.authType == AuthenticationType.GUEST) 86 | authData.id = current.id; 87 | 88 | Flox.service.request(HttpMethod.POST, "authenticate", authData, 89 | onRequestComplete, onRequestError); 90 | 91 | Flox.authentication = null; // prevent any new requests while login is in process! 92 | } 93 | 94 | function onRequestComplete(body:Object, httpStatus:int):void 95 | { 96 | // authToken may be overridden (e.g. so that password is not stored locally) 97 | if (body.authToken) authData.authToken = body.authToken; 98 | 99 | var id:String = body.id; 100 | var type:String = getType(Flox.playerClass); 101 | var player:Player = Entity.fromObject(type, id, body.entity) as Player; 102 | onAuthenticated(player); 103 | } 104 | 105 | function onRequestError(error:String, httpStatus:int):void 106 | { 107 | Flox.authentication = previousAuthentication; 108 | execute(onError, error, httpStatus); 109 | } 110 | 111 | function onAuthenticated(player:Player):void 112 | { 113 | Flox.clearCache(); 114 | Flox.currentPlayer = player; 115 | Flox.authentication = new Authentication(player.id, 116 | authData.authType, authData.authId, authData.authToken); 117 | 118 | execute(onComplete, player); 119 | } 120 | } 121 | 122 | /** Logs the current player out and immediately creates a new guest player. 123 | * (In Flox, 'Player.current' will always return a player object.) */ 124 | public static function logout():void 125 | { 126 | // we always need an active player! The login method, called without arguments, 127 | // will create a new one for us. 128 | 129 | login(); 130 | } 131 | 132 | /** Log in a player with just a single 'key' string. The typical use-case of this 133 | * authentication is to combine Flox with other APIs that have their own user database 134 | * (e.g. Mochi, Kongregate, GameCenter, etc). */ 135 | public static function loginWithKey(key:String, onComplete:Function, onError:Function):void 136 | { 137 | login(AuthenticationType.KEY, key, null, onComplete, onError); 138 | } 139 | 140 | /** Log in a player with his e-mail address. 141 | * 142 | *
  • If this is the first time this e-mail address is used, the current guest player 143 | * will be converted into a player with auth-type "EMAIL".
  • 144 | *
  • When the player tries to log in with the same address on another device, 145 | * he will get an e-mail with a confirmation link, and the login will fail until the 146 | * player clicks on that link.
147 | * 148 | *

In case of an error, the HTTP status tells you if a confirmation mail was sent: 149 | * "HttpStatus.FORBIDDEN" means that the mail was sent; "HttpStatus.TOO_MANY_REQUESTS" 150 | * means that a mail has already been sent within the last 15 minutes.

151 | * 152 | * @param email The e-mail address of the player trying to log in. 153 | * @param onComplete function onComplete(currentPlayer:Player):void; 154 | * @param onError function onError(error:String, httpStatus:int, confirmationMailSent:Boolean):void;*/ 155 | public static function loginWithEmail(email:String, 156 | onComplete:Function, onError:Function):void 157 | { 158 | login(AuthenticationType.EMAIL, email, Flox.installationID, onComplete, onLoginError); 159 | 160 | function onLoginError(error:String, httpStatus:int):void 161 | { 162 | execute(onError, error, httpStatus, httpStatus == HttpStatus.FORBIDDEN); 163 | } 164 | } 165 | 166 | /** Log in a player with his e-mail address and a password. 167 | * 168 | *

Depending on the 'loginOnly' parameter, this method can also be used to sign up 169 | * a previously unknown player. Once an e-mail address is confirmed, a login will only 170 | * work with the correct password.

171 | * 172 | *
    173 | *
  • If the e-mail + password combination is correct, the player will be logged in — 174 | * regardless of the 'loginOnly' setting.
  • 175 | *
  • If 'loginOnly = true' and the mail address is unknown or the password is wrong, 176 | * the method will yield an error with HttpStatus.FORBIDDEN.
  • 177 | *
  • If 'loginOnly = false' and the mail address is used for the first time, the player 178 | * receives a confirmation mail and the method yields an error with 179 | * HttpStatus.UNAUTHORIZED.
  • 180 | *
  • If 'loginOnly = false' and the mail address was not yet confirmed, or if the 181 | * mail address was already registered with a different password, the method will 182 | * yield an error with HttpStatus.FORBIDDEN.
  • 183 | *
184 | * 185 | *

If the player forgets the password, you can let him acquire a new one with the 186 | * 'resetEmailPassword' method.

187 | * 188 | * @param email The e-mail address of the player trying to log in or sign up. 189 | * @param password The password of the player trying to log in or sign up. 190 | * @param loginOnly If true, the email/password combination must already exist. 191 | * If false, an e-mail address that was used for the first time will 192 | * trigger a confirmation e-mail. 193 | * @param onComplete function onComplete(currentPlayer:Player):void; 194 | * @param onError function onError(error:String, httpStatus:int, confirmationMailSent:Boolean):void; 195 | */ 196 | public static function loginWithEmailAndPassword( 197 | email:String, password:String, loginOnly:Boolean, 198 | onComplete:Function, onError:Function):void 199 | { 200 | var authData:Object = 201 | { 202 | authType: AuthenticationType.EMAIL_AND_PASSWORD, 203 | authId: email, 204 | authToken: password, 205 | loginOnly: loginOnly 206 | }; 207 | 208 | loginWithAuthData(authData, onComplete, onLoginError); 209 | 210 | function onLoginError(error:String, httpStatus:int):void 211 | { 212 | execute(onError, error, httpStatus, httpStatus == HttpStatus.UNAUTHORIZED); 213 | } 214 | } 215 | 216 | /** Causes the server to send a password-reset e-mail to the player's e-mail address. If 217 | * that address is unknown to the server, it will yield an error with HttpStatus.NOT_FOUND. 218 | * 219 | * @param onComplete function onComplete():void; 220 | * @param onError function onError(error:String, httpStatus:int):void; 221 | */ 222 | public static function resetEmailPassword(email:String, onComplete:Function, 223 | onError:Function):void 224 | { 225 | Flox.service.request(HttpMethod.POST, "resetPassword", { email: email }, 226 | onComplete, onError); 227 | } 228 | 229 | /** The current local player. */ 230 | public static function get current():Player 231 | { 232 | return Flox.currentPlayer; 233 | } 234 | 235 | /** The type of authentication the player used to log in. */ 236 | public function get authType():String { return mAuthType; } 237 | public function set authType(value:String):void 238 | { 239 | if (mAuthType != null && mAuthType != value) 240 | throw new IllegalOperationError("Cannot change the authentication type of a Player entity."); 241 | else 242 | mAuthType = value; 243 | } 244 | 245 | /** @private */ 246 | public override function set publicAccess(value:String):void 247 | { 248 | if (value != Access.READ) 249 | throw new IllegalOperationError("Cannot change access rights of a Player entity."); 250 | else 251 | super.publicAccess = value; 252 | } 253 | } 254 | } --------------------------------------------------------------------------------