├── 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 |
4 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
13 |
14 |
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 |
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.
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 | }
--------------------------------------------------------------------------------