Skip to content

Commit

Permalink
Add Cloud Save features (#61)
Browse files Browse the repository at this point in the history
* allot AsyncHttp to request misc urls

* exclude swfs in simple mode

* add CloudSaves

* add SaveSlotList and ResultType

* remove useless callback args

* documentation and readibility

* more documentation

* simplify load, rename saveData to contents

* fix loadAllFiles

* readme
  • Loading branch information
Geokureli authored Aug 11, 2022
1 parent 94f6708 commit 2d601b1
Show file tree
Hide file tree
Showing 14 changed files with 678 additions and 73 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ which allows you make postScore and getScores calls directly on the board.

**Note:** ScoreBoard instances persist across multiple requestScoreBoards calls, but a ScoreBoard's score instances do not

### CloudSaves
Similarly to medals and scoreboards CloudSaves have `NG.core.saveSlots` which is populated by `NG.core.requestSaveSlots`.
On top of the normal [SaveSlot properties](http://www.newgrounds.io/help/objects/#SaveSlot), each saveSlot will have a
readonly `contents` field that is null until you call `load` on that SaveSlot instance (`load()` will throw an error
if there is no save in that slot, check this using `isEmpty()`). You can also call `save(mySaveContents)` or `clear()` on
SaveSlots.

**Note:** `NG.core.requestSaveSlots` has an optional `loadFiles` arg that will load all the SaveSlot's contents as well
as the slot info. Additionally, you can call `NG.core.saveSlots.loadList` instead of `NG.core.requestSaveSlots`.

## Calling Components and Handling Results
You can talk to the NG.io server directly, but NG.core won't automatically handle
the response for you (unlike NG.core.requestMedals()). All of the component calls are
Expand Down
2 changes: 1 addition & 1 deletion lib/Source/io/newgrounds/Call.hx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ class Call<T:ResultBase>

_core.markCallPending(this);

AsyncHttp.send(_core, Json.stringify(data), onData, onHttpError, onStatus);
AsyncNGCall.send(_core, Json.stringify(data), onData, onHttpError, onStatus);
}

/** Adds the call to the queue */
Expand Down
36 changes: 29 additions & 7 deletions lib/Source/io/newgrounds/NG.hx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@ package io.newgrounds;
#if ng_lite
typedef NG = NGLite; //TODO: test and make lite UI
#else
import io.newgrounds.utils.Dispatcher;
import io.newgrounds.objects.Error;
import io.newgrounds.objects.events.Result.SessionResult;
import io.newgrounds.objects.events.Result.MedalListResult;
import io.newgrounds.objects.events.Result.ScoreBoardResult;
import io.newgrounds.objects.events.Result.LoadSlotsResult;
import io.newgrounds.objects.events.ResultType;
import io.newgrounds.objects.events.Response;
import io.newgrounds.objects.User;
import io.newgrounds.objects.Medal;
import io.newgrounds.objects.Session;
import io.newgrounds.objects.SaveSlot;
import io.newgrounds.objects.ScoreBoard;
import io.newgrounds.objects.Session;
import io.newgrounds.utils.Dispatcher;
import io.newgrounds.utils.SaveSlotList;
#if (openfl < "4.0.0")
import openfl.utils.JNI;
#else
import lime.system.JNI;
#end
import haxe.ds.IntMap;
import haxe.Timer;

/**
Expand Down Expand Up @@ -51,15 +54,17 @@ class NG extends NGLite {

return _session.passportUrl;
}
public var medals(default, null):IntMap<Medal>;
public var scoreBoards(default, null):IntMap<ScoreBoard>;
public var medals(default, null):Map<Int, Medal>;
public var scoreBoards(default, null):Map<Int, ScoreBoard>;
public var saveSlots(default, null):SaveSlotList;

// --- EVENTS

public var onLogin(default, null):Dispatcher;
public var onLogOut(default, null):Dispatcher;
public var onMedalsLoaded(default, null):Dispatcher;
public var onScoreBoardsLoaded(default, null):Dispatcher;
public var onSaveSlotsLoaded(default, null):Dispatcher;

// --- MISC

Expand All @@ -83,6 +88,9 @@ class NG extends NGLite {
onLogOut = new Dispatcher();
onMedalsLoaded = new Dispatcher();
onScoreBoardsLoaded = new Dispatcher();
onSaveSlotsLoaded = new Dispatcher();

saveSlots = new SaveSlotList(this);

attemptingLogin = sessionId != null;

Expand Down Expand Up @@ -404,7 +412,7 @@ class NG extends NGLite {

if (medals == null) {

medals = new IntMap<Medal>();
medals = new Map();

for (medalData in response.result.data.medals) {

Expand Down Expand Up @@ -463,7 +471,7 @@ class NG extends NGLite {

if (scoreBoards == null) {

scoreBoards = new IntMap<ScoreBoard>();
scoreBoards = new Map();

for (boardData in response.result.data.scoreboards) {

Expand All @@ -478,6 +486,20 @@ class NG extends NGLite {
onScoreBoardsLoaded.dispatch();
}

// -------------------------------------------------------------------------------------------
// CLOUD SAVES
// -------------------------------------------------------------------------------------------

/**
* Loads the info for each cloud save slot, including the last save time and size
* @param loadFiles If true, each slot's save file is also loaded.
* @param callback Whether the request was successful, or an error message
**/
inline public function requestSaveSlots(loadFiles = false, ?callback:ResultType->Void):Void {

saveSlots.loadList(callback);
}

// -------------------------------------------------------------------------------------------
// HELPERS
// -------------------------------------------------------------------------------------------
Expand Down
63 changes: 63 additions & 0 deletions lib/Source/io/newgrounds/components/CloudSaveComponent.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.newgrounds.components;

import io.newgrounds.objects.events.Result.SaveSlotResult;
import io.newgrounds.objects.events.Result.LoadSlotsResult;
import io.newgrounds.NGLite;

/** Handles loading and saving of game states. */
class CloudSaveComponent extends Component {

/**
* Pass into the default value to allow null values.
*
* TODO: find less hacky solution
**/
static var allowNull:Dynamic = {};

public function new (core:NGLite){ super(core); }

/**
* Deletes all data from a save slot.
*
* @param id The slot number
**/
public function clearSlot(id:Int):Call<SaveSlotResult> {

return new Call<SaveSlotResult>(_core, "CloudSave.clearSlot", true)
.addComponentParameter("id", id);
}

/**
* Returns a specific saveslot object.
*
* @param id The slot number
**/
public function loadSlot(id:Int):Call<SaveSlotResult> {

return new Call<SaveSlotResult>(_core, "CloudSave.loadSlot", true)
.addComponentParameter("id", id);
}

/**
* Returns a list of saveslot objects.
*
* @param id The slot number
**/
public function loadSlots():Call<LoadSlotsResult> {

return new Call<LoadSlotsResult>(_core, "CloudSave.loadSlots", true);
}

/**
* Deletes all data from a save slot.
*
* @param data The data you want to save
* @param id The slot number
**/
public function setData(data:String, id:Int):Call<SaveSlotResult> {

return new Call<SaveSlotResult>(_core, "CloudSave.setData", true)
.addComponentParameter("data", data, allowNull)
.addComponentParameter("id", id);
}
}
22 changes: 15 additions & 7 deletions lib/Source/io/newgrounds/components/ComponentList.hx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,31 @@ class ComponentList {

var _core:NGLite;

// --- COMPONENTS
public var medal : MedalComponent;
/** Used to get and validate information associated with your app, including user sessions. */
public var app : AppComponent;
/** Handles loading and saving of game states. */
public var cloudSave : CloudSaveComponent;
/** Handles logging of custom events. */
public var event : EventComponent;
public var scoreBoard: ScoreBoardComponent;
public var loader : LoaderComponent;
/** Provides information about the gateway server. */
public var gateway : GatewayComponent;
/** This class handles loading various URLs and tracking referral stats. */
public var loader : LoaderComponent;
/** Handles loading and unlocking of medals. */
public var medal : MedalComponent;
/** Handles loading and posting of high scores and scoreboards. */
public var scoreBoard: ScoreBoardComponent;

public function new(core:NGLite) {

_core = core;

medal = new MedalComponent (_core);
app = new AppComponent (_core);
cloudSave = new CloudSaveComponent (_core);
event = new EventComponent (_core);
scoreBoard = new ScoreBoardComponent(_core);
loader = new LoaderComponent (_core);
gateway = new GatewayComponent (_core);
loader = new LoaderComponent (_core);
medal = new MedalComponent (_core);
scoreBoard = new ScoreBoardComponent(_core);
}
}
7 changes: 7 additions & 0 deletions lib/Source/io/newgrounds/components/LoaderComponent.hx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ package io.newgrounds.components;
import io.newgrounds.objects.events.Result;
import io.newgrounds.NGLite;

/**
* This class handles loading various URLs and tracking referral stats.
*
* Note: These calls do not return any JSON packets (unless the redirect param is set to false).
* Instead, they redirect to the appropriate URL. These calls should be executed in a browser
* window vs using AJAX or any other internal loaders.
*/
class LoaderComponent extends Component {

public function new (core:NGLite){ super(core); }
Expand Down
140 changes: 140 additions & 0 deletions lib/Source/io/newgrounds/objects/SaveSlot.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package io.newgrounds.objects;

import io.newgrounds.utils.AsyncHttp;
import io.newgrounds.objects.events.Result.SaveSlotResult;
import io.newgrounds.objects.events.ResultType;
import io.newgrounds.objects.events.Response;

/**
* Contains information about a CloudSave slot.
**/
typedef RawSaveSlot = {

/** A date and time (in ISO 8601 format) representing when this slot was last saved. */
var datetime :Null<String>;
/** The slot number. */
var id :Int;
/** The size of the save data in bytes. */
var size :Int;
/** A unix timestamp representing when this slot was last saved. */
var timestamp:Int;
/** The URL containing the actual save data for this slot, or null if this slot has no data. */
var url :Null<String>;
}

/**
* Contains information about a CloudSave slot.
*
* This is helper class that lets you call methods directly on the slot object, rather than
* calling generic calls using the slot's id number.
**/
class SaveSlot extends Object<RawSaveSlot>
{
/** A date and time (in ISO 8601 format) representing when this slot was last saved. */
public var datetime(get, never):Null<String>;

/** The slot number. */
public var id(get, never):Int;

/** The size of the save data in bytes. */
public var size(get, never):Int;

/** A unix timestamp representing when this slot was last saved. */
public var timestamp(get, never):Int;

/** The URL containing the actual save data for this slot, or null if this slot has no data. */
public var url(get, never):Null<String>;

inline function get_datetime () return _data.datetime;
inline function get_id () return _data.id;
inline function get_size () return _data.size;
inline function get_timestamp() return _data.timestamp;
inline function get_url () return _data.url;

/** The contents of this slot's save file. Will be null until `load` is called. **/
public var contents(default, null):Null<String>;

public function new(core:NGLite, data:RawSaveSlot = null) {

super(core, data);
}

/**
* Saves the supplied data to the cloud save slot
*
* @param data The data to save to the slot
* @param callback Called when the data is saved with the new value.
* Tells whether the server call was successful.
*
* @throws Exception if data is null
*/
public function save(data:String, ?callback:(ResultType)->Void) {

if (data == null)
throw "cannot save null to a SaveSlot";

_core.calls.cloudSave.setData(data, id)
.addDataHandler((response)->setContentsOnSlotFetch(response, data, callback))
.send();
}

/**
* Clears cloud save slot
*
* @param callback Called when the data is cleared.
* Tells whether the server call was successful.
*/
public function clear(?callback:(ResultType)->Void) {

_core.calls.cloudSave.clearSlot(id)
.addDataHandler((response)->setContentsOnSlotFetch(response, null, callback))
.send();
}

function setContentsOnSlotFetch
( response:Response<SaveSlotResult>
, contents:Null<String>
, ?callback:(ResultType)->Void
) {

// Always have a non-null callback to avoid having to null check everywhere
if (callback == null)
callback = (_)->{};

if (response.success && response.result.success) {

this.contents = contents;
parse(response.result.data.slot);
}

callback(Success);
}

/**
* Loads the save slot's file contents
*
* @param callback Called when the save file is loaded.
* Returns the contents, is successful, otherwise returns an error.
*/
public function load(?callback:(SaveSlotResultType)->Void) {

if (isEmpty())
throw 'Cannot load from an empty SaveSlot, id:$id';

// TODO: load data (async)
AsyncHttp.send(url, null,
(s)->
{
contents = s;
callback(Success(contents));
onUpdate.dispatch();
},
(error)->callback(Error(error))
);
}

/** Whether any data has been saved to this slot. */
inline public function isEmpty():Bool return url == null;
}

typedef SaveSlotResultType = TypedResultType<Null<String>>;
Loading

0 comments on commit 2d601b1

Please sign in to comment.