• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!

Legacy GM Creating local leaderboards

P

ProjectPurityTeam

Guest
Well, I use Steam leaderboars in my game to upload high scores of the players, but I would like to show these leaderboards inside the game. In this way, players do not have to exit the game to check in what rank they are after beating a record. However, I read Steam API Documentation, and tried it with this example code:

var async_id = ds_map_find_value(async_load, "id");
if async_id == score_get
{
var entries = ds_map_find_value(async_load, "entries");
var map = json_decode(entries);
if ds_map_exists(map, "default")
{
ds_map_destroy(map);
exit;
}
else
{
var list = ds_map_find_value(map, "entries");
var len = ds_list_size(list);
var entry;
for(var i = 0; i < len; i++; )
{
entry = ds_list_find_value(list, i );
steam_name = ds_map_find_value(entry, "name");
steam_score = ds_map_find_value(entry, "score");
steam_rank = ds_map_find_value(entry, "rank");
ds_map_destroy(entry);
}
ds_list_destroy(list)
}
ds_map_destroy(map)
}

I know that to make leaderboards in my game, I will need to download entries in Steam leaderboards and then show them in the screen. However, I do not understand at all this example code with the explanation in Yoyo Games site. Code has to be in a async Steam event or something like that, and entries information is stored in different arrays. If someone knows other way of doing this or undesrtands the code could you help me?
 

Phil Strahl

Member
Hi there!

Yeah, Steam async can be a pain in the butt to figure out, so I'll share my implementation of the code with you, with lots of comments so you know what's going on.

Code:
  // This goes all into the steam_async_event

  // First of all, you should check the returned ds_map for the value of the key "event_type", because it should be a string, "leaderboard_download".
  var async_type = ds_map_find_value(async_load, "event_type");
  switch async_type
  {
    case "leaderboard_download":
    {
    var async_id = ds_map_find_value(async_load, "id");
 
    // check if the id of the returned ds_map matches the one from the request,
    // so you can be sure you got the right one. You do this, by checking the value at the key "id".
    if (async_id == global.steam_request_id)
    {

        // We can resume to reading the entries. They are are stored as value  under the key "entries".
        var entries = ds_map_find_value(async_load, "entries");
     
        // It's a long string and it's in JSON format, so basically it's another data structure in the returned data structure.
        var map     = json_decode(entries);
     
        // If it holds "default", we know that there are no entries in the leaderboard, so either it's empty
        // or something went wrong
        if ds_map_exists(map, "default")
        {
          ds_map_destroy(map);
          // show error message
          show_debug_message("Leaderboard Async Download Failed");
          global.steam_list_done = true; // sets a flag so the rest of my game knows that this async event has finished.
          global.steam_list_success = false; // another flag, so your program knows that the steam async event finished unsuccessfully
          exit;
        }
        else
        {
          // Let's continue to parsing the entries.
          // They are nested as yet another data structure, this time it's a ds_list of ds_maps. I know, right? Just roll with it.
          var list   = ds_map_find_value(map, "entries");
          var entry;
          var val;
          var debug_str = "";
     
          // To access an entry in this list, you need the list position of a given entry
          // so we store it to use in a for loop:
          var count = ds_list_size(list)-1;
     
          for(var i = 0; i <= count; i++;)
          {
             // from the list, we pick the next ds_map at position i:
              entry = ds_list_find_value(list, i);

              // now let's extract the values and store them in local variables for rank, name, and score
              polled_rank  = ds_map_find_value(entry, "rank");
              polled_name  = ds_map_find_value(entry, "name");
              polled_score = ds_map_find_value(entry, "score");
               
              // debug what we got
              debug_str = polled_name +", "+string(polled_score)+", "+string(polled_rank)
              show_debug_message(scr+"Entry "+string(i)+": "+debug_str);

              //  destroy this entry's ds_map once we're done
              ds_map_destroy(entry);

              // Here you should do something with what you read from the current entry,
              // e.g. I suggest to fill them into three arrays which you can read later to draw your own
              // high-score table, something like this
           
              // THIS IS JUST AN EXAMPLE
              hiscores_name[i]  = polled_name;
              hiscores_rank[i]  = polled_rank;
              hiscores_score[i] = polled_score;
           
          } // end of "map has leaderboard data"

          // cleanups
          ds_list_destroy(list)
          ds_map_destroy(map)
       
          global.steam_list_done = true;    // sets a flag so the rest of my game knows that this async event has finished.
          global.steam_list_success = true; // lets other parts of your program know, that the async_evend finished successfully
          exit;
        } // end of "map has leaderboard entries"
      } // end of "async_id matches the id of the request"
    } break; // end of "case leaderboard download"
  } // end of "switch event_type
Because that's an async event, the other parts in your game should check if it ran successfully (that's the purpose of the global.steam_list_done and global.steam_list_success flags)

So after this bit of code ran successfully, the arrays hiscores_name, hiscores_rank, and hiscores_score should be filled with the data from Steamworks. Again, since this is an async event, really make sure to check whether these are not empty or uninitialized, e.g. have your leaderbord display something like "Downloading Data..." until global.steam_list_done and global.steam_list_success are true.

It took a while to wrap my head around it, so I drew myself a little diagram to help me visualize it, maybe it's helpful to you as well:

Code:
"async_load" [ds_map]
   |
   +-- "event_type" : "leaderboard_download" [string]
   |
   +-- "id" : (the id value of the calling function) [integer]
   |
   +-- "status" : "0" (= success) or "-1" (= failure) [integer]
   |
   +-- "lb_name" : "someName" [string] (name of the leaderboard as defined in Steamworks)
   |
   +-- "num_entries" : "4" [integer]
   |
   +-- "entries" : [string / ds_map as JSON]
                   |
                   +-- "entries" : [ds_list]
                        |
                        +-- 0 : [ds_map]
                        |   |
                        |   +-- "name" : "Player 1" [string]
                        |   |
                        |   +-- "rank" : "5" [integer]
                        |   |
                        |   +-- "score" : "5000" [integer]
                        |
                        +-- 1 : [ds_map]
                        |   |
                        |   +-- "name" : "Player 2" [string]
                        |   |
                        |   +-- "rank" : "6" [integer]
                        |   |
                        |   +-- "score" : "4999" [integer]
                        |
                        +-- 2 : [ds_map]
                        |   |
                        |   +-- "name" : "Player 3" [string]
                        |   |
                        |   +-- "rank" : "7" [integer]
                        |   |
                        |   +-- "score" : "4998" [integer]
                        | 
                        +-- 3 : [ds_map]
                            |
                            +-- "name" : "Player 4" [string]
                            |
                            +-- "rank" : "8" [integer]
                            |
                            +-- "score" : "4997" [integer]
Hope this helps you and others. Cheers!
 
P

ProjectPurityTeam

Guest
Hi there!

Yeah, Steam async can be a pain in the butt to figure out, so I'll share my implementation of the code with you, with lots of comments so you know what's going on.

Code:
  // This goes all into the steam_async_event

  // First of all, you should check the returned ds_map for the value of the key "event_type", because it should be a string, "leaderboard_download".
  var async_type = ds_map_find_value(async_load, "event_type");
  switch async_type
  {
    case "leaderboard_download":
    {
    var async_id = ds_map_find_value(async_load, "id");
 
    // check if the id of the returned ds_map matches the one from the request,
    // so you can be sure you got the right one. You do this, by checking the value at the key "id".
    if (async_id == global.steam_request_id)
    {

        // We can resume to reading the entries. They are are stored as value  under the key "entries".
        var entries = ds_map_find_value(async_load, "entries");
    
        // It's a long string and it's in JSON format, so basically it's another data structure in the returned data structure.
        var map     = json_decode(entries);
    
        // If it holds "default", we know that there are no entries in the leaderboard, so either it's empty
        // or something went wrong
        if ds_map_exists(map, "default")
        {
          ds_map_destroy(map);
          // show error message
          show_debug_message("Leaderboard Async Download Failed");
          global.steam_list_done = true; // sets a flag so the rest of my game knows that this async event has finished.
          global.steam_list_success = false; // another flag, so your program knows that the steam async event finished unsuccessfully
          exit;
        }
        else
        {
          // Let's continue to parsing the entries.
          // They are nested as yet another data structure, this time it's a ds_list of ds_maps. I know, right? Just roll with it.
          var list   = ds_map_find_value(map, "entries");
          var entry;
          var val;
          var debug_str = "";
    
          // To access an entry in this list, you need the list position of a given entry
          // so we store it to use in a for loop:
          var count = ds_list_size(list)-1;
    
          for(var i = 0; i <= count; i++;)
          {
             // from the list, we pick the next ds_map at position i:
              entry = ds_list_find_value(list, i);

              // now let's extract the values and store them in local variables for rank, name, and score
              polled_rank  = ds_map_find_value(entry, "rank");
              polled_name  = ds_map_find_value(entry, "name");
              polled_score = ds_map_find_value(entry, "score");
              
              // debug what we got
              debug_str = polled_name +", "+string(polled_score)+", "+string(polled_rank)
              show_debug_message(scr+"Entry "+string(i)+": "+debug_str);

              //  destroy this entry's ds_map once we're done
              ds_map_destroy(entry);

              // Here you should do something with what you read from the current entry,
              // e.g. I suggest to fill them into three arrays which you can read later to draw your own
              // high-score table, something like this
          
              // THIS IS JUST AN EXAMPLE
              hiscores_name[i]  = polled_name;
              hiscores_rank[i]  = polled_rank;
              hiscores_score[i] = polled_score;
          
          } // end of "map has leaderboard data"

          // cleanups
          ds_list_destroy(list)
          ds_map_destroy(map)
      
          global.steam_list_done = true;    // sets a flag so the rest of my game knows that this async event has finished.
          global.steam_list_success = true; // lets other parts of your program know, that the async_evend finished successfully
          exit;
        } // end of "map has leaderboard entries"
      } // end of "async_id matches the id of the request"
    } break; // end of "case leaderboard download"
  } // end of "switch event_type
Because that's an async event, the other parts in your game should check if it ran successfully (that's the purpose of the global.steam_list_done and global.steam_list_success flags)

So after this bit of code ran successfully, the arrays hiscores_name, hiscores_rank, and hiscores_score should be filled with the data from Steamworks. Again, since this is an async event, really make sure to check whether these are not empty or uninitialized, e.g. have your leaderbord display something like "Downloading Data..." until global.steam_list_done and global.steam_list_success are true.

It took a while to wrap my head around it, so I drew myself a little diagram to help me visualize it, maybe it's helpful to you as well:

Code:
"async_load" [ds_map]
   |
   +-- "event_type" : "leaderboard_download" [string]
   |
   +-- "id" : (the id value of the calling function) [integer]
   |
   +-- "status" : "0" (= success) or "-1" (= failure) [integer]
   |
   +-- "lb_name" : "someName" [string] (name of the leaderboard as defined in Steamworks)
   |
   +-- "num_entries" : "4" [integer]
   |
   +-- "entries" : [string / ds_map as JSON]
                   |
                   +-- "entries" : [ds_list]
                        |
                        +-- 0 : [ds_map]
                        |   |
                        |   +-- "name" : "Player 1" [string]
                        |   |
                        |   +-- "rank" : "5" [integer]
                        |   |
                        |   +-- "score" : "5000" [integer]
                        |
                        +-- 1 : [ds_map]
                        |   |
                        |   +-- "name" : "Player 2" [string]
                        |   |
                        |   +-- "rank" : "6" [integer]
                        |   |
                        |   +-- "score" : "4999" [integer]
                        |
                        +-- 2 : [ds_map]
                        |   |
                        |   +-- "name" : "Player 3" [string]
                        |   |
                        |   +-- "rank" : "7" [integer]
                        |   |
                        |   +-- "score" : "4998" [integer]
                        |
                        +-- 3 : [ds_map]
                            |
                            +-- "name" : "Player 4" [string]
                            |
                            +-- "rank" : "8" [integer]
                            |
                            +-- "score" : "4997" [integer]
Hope this helps you and others. Cheers!
Thank you, It was very usefull! :D, and how can I download all entries? steam_download_scores(lb_name, start_idx, end_idx) this function needs an integer to set an end index but it depends in how large is leaderboard, so is there a variable or value to indicate that?
 

Phil Strahl

Member
Thank you, It was very usefull! :D, and how can I download all entries? steam_download_scores(lb_name, start_idx, end_idx) this function needs an integer to set an end index but it depends in how large is leaderboard, so is there a variable or value to indicate that?
You could download from rank 1 to to 9999999, but that may take a couple of seconds and you hardly be able to display the whole leaderboard. The good thing is that Steamworks only returns what's there, so you can't overshoot by polling 9999999 ranks, when there are only 10 entries in a leaderboard.

My suggestion is to download only what you're displaying, e.g. have the leaderboard show ranks 1-25 and offer to show the next entries, 26-50, 51-76 etc.


In my experience, players are primarily interested in the top-runners, how they compare to their friends, and who's the next in line to beat, so in my game, I used a hybrid of that: Top 10, player score with the next lower and higher scoring player, and a friends list:

fine-sweeper.png

Upon clicking "Classic mode", the new data gets polled fromSteamworks again, but from a different leaderboard which takes half a second or less.
 
P

ProjectPurityTeam

Guest
You could download from rank 1 to to 9999999, but that may take a couple of seconds and you hardly be able to display the whole leaderboard. The good thing is that Steamworks only returns what's there, so you can't overshoot by polling 9999999 ranks, when there are only 10 entries in a leaderboard.

My suggestion is to download only what you're displaying, e.g. have the leaderboard show ranks 1-25 and offer to show the next entries, 26-50, 51-76 etc.


In my experience, players are primarily interested in the top-runners, how they compare to their friends, and who's the next in line to beat, so in my game, I used a hybrid of that: Top 10, player score with the next lower and higher scoring player, and a friends list:

View attachment 14081

Upon clicking "Classic mode", the new data gets polled fromSteamworks again, but from a different leaderboard which takes half a second or less.
Thanks again! :D
 
Top