spookygames / gdx-gameservices

Game services wrapper for libgdx.
MIT License
7 stars 2 forks source link

Initialization of Playtomic #1

Closed Lucidarme closed 6 years ago

Lucidarme commented 6 years ago

Hello, I installed this gdx-gameservices to use Playtomic on all my plateform (Android, iOS and HTML). But I don't understand how initialize playtomic. I saw on https://playtomic.org/api/android.html that I need to do Playtomic.initialize(String publickey, String privatekey, String apiurl);. Have I something to put on Android / iOS / HTML folder or only in the core? Thank you

arthurtemple commented 6 years ago

That's right, this is not specified in the readme, and should.

Initialize your handler as such and you should be good to go:

  // Create handler
PlaytomicServicesHandler playtomic = new PlaytomicServicesHandler();

// Initialize keys
playtomic.setServer("url_of_playtomic_server");  // "apiurl" from the docs
playtomic.setPublicKey("public_key");  // "publicKey"
playtomic.setPrivateKey("private_key");  // "privateKey"

// Optional: set source for leaderboards
playtomic.setPlayerSource("my_platform_source");

This piece of code can be inserted into your core project. The optional part may be inserted from a platform-specific project or defined from Gdx.app.getType()

Lucidarme commented 6 years ago

Thank you for your quick answer, now it make sence. I have another question: When I try to login, I don't have any result. No error message and no success neither. I looked at my database and I saw a collection for leaderboards, achivements but not for players. Is that normal?? Anyway, here is some code:

My server is set up with the playtomic server api and run on port 3000. This is my game class:

public class MyGame extends Game{
    public static PlaytomicServicesHandler connectionHandler;
    public MyGame(){
        super();
        // Create handler
        PlaytomicServicesHandler playtomic = new PlaytomicServicesHandler();
        // Initialize keys
        playtomic.setServer("http://xx.xx.xx.xx:3000/");  // "apiurl" from the docs
        playtomic.setPublicKey("publickey");  // "publicKey"
        playtomic.setPrivateKey("privatekey");  // "privateKey"
        SpeedRun2Game.connectionHandler = playtomic;
    }
}

And I try to login like that:

final PlaytomicServicesHandler myConnectionHandler = game.connectionHandler;
myConnectionHandler.setPlayerId("playerid");
myConnectionHandler.setPlayerName("playername");
myConnectionHandler.login(new ServiceCallback<Void>() {
            @Override
            public void onSuccess(Void result, ServiceResponse response) {
                // Get player id and name
                Gdx.app.log("gdx-gameservices", "Welcome back! [" + myConnectionHandler.getPlayerId() + "] " + myConnectionHandler.getPlayerName());
            }
            @Override
            public void onFailure(ServiceResponse response) {
                Gdx.app.error("gdx-gameservices", "Unable to login: " + response.getErrorMessage());
            }
});

Thanks for help

EDIT : Damn I solved it, in fact the login class send nothing in case of success:


@Override
    public void login(ServiceCallback<Void> callback) {
        if (playerId == null) {
            // Send back dummy response
            ServiceResponse dummyResponse = new PlaytomicResponse(false, 1);
            callback.onFailure(dummyResponse);
        } else {
            // TODO Kinda ping server, is all
        }
}

But why there is no player collection in database?

Sorry for the disturbance and thanks again

arthurtemple commented 6 years ago

Regarding login let me add that Playtomic server supports a REST API, so there is no session per se. Instead, every request carries login information along the way. As such, I think the login() method should call should trigger success when playerId is not null.

The Playtomic handler was the first one to come up and lacks a bit of coherence with regard to the common interfaces, I must admit. I'll see to this as soon as I can free up some time. PRs from your side would be reviewed and merged of course.

Now concerning the player collection, I'm afraid I did not understand what you actually meant. Could you give more information about what you are concretely trying to achieve?

Lucidarme commented 6 years ago

I did a racing game and I would like to set up a multiplatform leaderboard. I'm just looking for backup, list, and rank functions. I did some tests yesterday and saw that the backup function worked well as well as the lists. I still have to modify some points since I try to achieve a decreasing leaderboad. But I am unable to recapture the rank of a player. The getPlayerScore () function always returns a rank of 1. If I want to get the rank of a player, I have to get the entire list of the leaderboard, and then look for my player, which is not very efficient. I'll try to work on it, but it sounds hard for me.

About the player collection, I thought that playtomic contained a user collection in order to calculate the number of player, store information about players, such as emails, or id. But it is true that this is not necessary for the proper functioning of the api.

arthurtemple commented 6 years ago

Method getPlayerScore() results into an API call to saveAndList (https://github.com/playtomic/apiserver/blob/master/api/leaderboards.js#L200), submitting a dummy score of 0 and getting back the result (a list containing one entry). I recall now meeting this very bug of rank 1. Bugfix required running a custom version of Playtomic as backend which was not possible for us at the time. I think there is no other way around from our side right now than what you already achieved.

For the player collection, I can confirm there is none. If I'm not wrong Playtomic was built to work upon existing user bases and does not support one as such. One typical Playtomic setup features 8 collections: achievements, achievements_players, games, gamevars, leaderboard_bans, leaderboard_scores, playerlevel_bans, playerlevel_levels.

Lucidarme commented 6 years ago

I changed the backend as you suggest and now I can get the rank easly. But on getPlayerScore(), I have another approach, I use getScores() instead of saveAndList(). If you are curious, here is my solution.

PlaytomicServicesHandler:


        @Override
    public void getPlayerScore(String leaderboardId, LeaderboardOptions options, final ServiceCallback<LeaderboardEntry> callback) {
        if (options == null)
            options = new LeaderboardOptions();
                // itemsPerPage = 0 is important, it will be change by a big number in the server side
        options.itemsPerPage = 0;
                //A boolean is added on the options. The backend will return only player score when this boolean is true.
        options.onlyPlayerScore = true;

        getScores(leaderboardId, options, new ServiceCallback<Iterable<LeaderboardEntry>>() {
            @Override
            public void onSuccess(Iterable<LeaderboardEntry> result, ServiceResponse response) {
                LeaderboardEntry playerEntry = null;
                for (LeaderboardEntry entry : result) {
                    playerEntry = entry;
                    break;  // There should only be one
                }

                if (playerEntry == null) {
                    // No proper entry == failure
                    onFailure(response);
                } else {
                    if (callback != null)
                        callback.onSuccess(playerEntry, response);
                }
            }
            @Override
            public void onFailure(ServiceResponse response) {
                if (callback != null)
                    callback.onFailure(response);
            }
        });
    }

    @Override
    public void getScores(String leaderboardId, LeaderboardOptions options, final ServiceCallback<Iterable<LeaderboardEntry>> callback) {

        checkConnection();

        JsonObjectBuilder b = this.builder;

        b.newObject()
        .add("table", leaderboardId)
        .add("playerid", playerId)
        .add("onlyPlayerScore", options.onlyPlayerScore)
        ;

        String action = "list";

        if (options == null || options.sort == null) {
            b.add("lowest", true);
        } else {
            switch (options.sort) {
            case Top:
            default:
                b.add("lowest", true);
                break;
            case Bottom:
                b.add("lowest", false);
                break;
            case CenteredOnPlayer:
                // TODO
                break;
            }
        }
        int perPage = options.itemsPerPage;
        if (perPage > 0)
            b.add("perpage", perPage);

//      // Listing options
//      private ObjectMap<String, ?> filters;
//      private int page;
//      private Array<String> friendsList;

        network.send("leaderboards", action, b.build(), PlaytomicLeaderboardEntry[].class, new ServiceCallback<PlaytomicLeaderboardEntry[]>() {

            @Override
            public void onSuccess(PlaytomicLeaderboardEntry[] result, ServiceResponse response) {
                callback.onSuccess(new Array<LeaderboardEntry>(result), response);
            }

            @Override
            public void onFailure(ServiceResponse response) {
                callback.onFailure(response);
            }
        });
    }

And on the server side, I changed the "list" function on leaderboards.js:

list: function(options, callback) {

        options.page = options.page || 1;
/***** The page must be bigger than the leaderboard table ****/
        options.perpage = options.perpage || 2000000;
        options.highest = options.lowest !== true;

        var query = {
            filter: {
                publickey: options.publickey,
                table: options.table
            },
            limit: options.perpage,
            skip: (options.page - 1) * options.perpage,
            sort: {}
        };

        // per-source website or device scores, websites
        // get truncated to domain.com
        if(options.source) {
            query.filter.source = options.source.indexOf("://") > -1 ? utils.baseurl(options.source) : options.source;
        }

        if(options.filters && Object.keys(options.filters).length > 0) {
            query.filter.fields = {};
            for(var x in options.filters) {
                query.filter.fields[x] = options.filters[x];
            }
        }

        // filtering for playerids
        var playerids = options.friendslist || [];
/******* I added a condition playerids.length != 0 because we have a playerid in options*******/
        if(playerids.length != 0 && options.playerid && !options.excludeplayerid) {
            playerids.push(options.playerid);
        }

        if(playerids.length > 1) {
            query.filter.playerid = { $in: playerids };
        } else if(playerids.length == 1) {
            query.filter.playerid = playerids[0];
        }

        // date mode
        options.mode = (options.mode || "alltime").toLowerCase();

        switch(options.mode) {
            case "today":
                query.filter.date = {"$gte": datetime.now - (24 * 60 * 60)};
                break;

           case "last7days":
               query.filter.date = {"$gte": (datetime.now - (7 * 24 * 60 * 60))};
                break;

            case "last30days":
                query.filter.date = {"$gte": (datetime.now - (30 * 24 * 60 * 60))};
                break;
        }

        // sorting
        if(options.mode == "newest") {
            query.sort = "-date";
        } else {
            query.sort = options.highest ? "-points" : "points";
        }

        // the scores
        db.LeaderboardScore.find(query.filter).sort(query.sort).limit(query.limit).skip(query.skip).exec(function(error, scores){

            if(error) {
                callback("unable to load scores: " + error + " (api.leaderboards.list:99)", errorcodes.GeneralError);
                return;
            }

            db.LeaderboardScore.count(query.filter, function(error, numscores) {

                if(error) {
                    callback("unable to count scores: " + error + " (api.leaderboards.list:104)", errorcodes.GeneralError);
                    return;
                }

                scores = scores || [];
                scores = clean(scores, query.skip + 1);
/************** If we want only the player score, we iterate over the array and push only the playerid one *****/
                if(options.onlyPlayerScore){
                    var playerScore = {};
                    for(var i=0; i<scores.length; i++){
                        if(scores[i].playerid == options.playerid){
                            playerScore = scores[i];
                            break;
                        }
                    }
                    scores = [];
                    scores.push(playerScore);
                }
                callback(null, errorcodes.NoError, numscores, scores);
            });
        });
    }

Now, in saveAndList function, I only call submit and then getPlayerScore:

public  void saveAndList(final String leaderboardId, long score, final LeaderboardOptions options, final ServiceCallback<LeaderboardEntry> callback){
    submitScore(leaderboardId, score, new ServiceCallback<Void>() {
        @Override
        public void onSuccess(Void result, ServiceResponse response) {
            getPlayerScore(leaderboardId, options, callback);
        }

        @Override
        public void onFailure(ServiceResponse response) {
            callback.onFailure(response);
        }
     });
}
arthurtemple commented 6 years ago

Thanks for the details! Playtomic does not seem very active right now but maybe people there would accept a PR for this ;-)