Issue with retrieving more than 20 data from Unity Game Services using pagination

Hello everyone,

I’m encountering an issue when trying to retrieve more than 20 data from Unity Game Services (UGS) using pagination. I’m developing an application where I need to access a large amount of data stored in UGS through Unity3D’s Cloud Save feature. However, despite trying to implement pagination as specified in the UGS API documentation, I always end up getting only the first 20 data.

I’ve tried various ways of implementing pagination, such as checking if there are more pages available using the “nextPage” key in the API response, but it seems like this check doesn’t work correctly. I always end up getting only the first 20 data and cannot access the rest of the stored data.

Here’s my current code:

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using Newtonsoft.Json;
using System.Linq;
using System.Threading.Tasks;

public class Program
{
    public static async Task Main(string[] args)
    {
        // Clase para autenticación de Bearer
        class BearerAuth
        {
            private readonly string _token;

            public BearerAuth(string token)
            {
                _token = token;
            }

            public void AddTokenToRequest(HttpRequestMessage request)
            {
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
            }
        }

        // Identificadores de proyecto y entorno de UGS
        var projectid = "********-****-****-****-************";
        var environmentid = "********-****-****-****-************";

        // URL para generar token
        var obtain_token_url = $"https://services.api.unity.com/auth/v1/token-exchange?projectId={projectid}&environmentId={environmentid}";

        // Datos de permiso generados en UGS
        var serviceid = "********-****-****-****-************";
        var servicepass = "************";

        // Realizar solicitud para obtener token
        var client = new HttpClient();
        var tokenRequestBody = new Dictionary<string, string>
        {
            { "grant_type", "client_credentials" },
            { "client_id", serviceid },
            { "client_secret", servicepass }
        };
        var tokenRequestContent = new FormUrlEncodedContent(tokenRequestBody);
        var tokenResponse = await client.PostAsync(obtain_token_url, tokenRequestContent);
        tokenResponse.EnsureSuccessStatusCode();
        var tokenContent = await tokenResponse.Content.ReadAsStringAsync();
        var tokenData = JsonConvert.DeserializeObject<dynamic>(tokenContent);
        var myToken = tokenData.accessToken;

        // ID del jugador de UGS
        var playerid = "********-****-****-****-************";

        // URL de la API para obtener los datos del jugador
        var obtain_player_data_base = $"https://cloud-save.services.api.unity.com/v1/data/projects/{projectid}/players/{playerid}/items";

        // Lista para almacenar todos los datos del jugador
        var all_player_data = new List<dynamic>();

        // Variable para el número de página
        var page = 1;

        // Realizar solicitudes para obtener todos los datos de todas las páginas
        while (true)
        {
            // Realizar solicitud GET a la API
            var request = new HttpRequestMessage(HttpMethod.Get, $"{obtain_player_data_base}?page={page}");
            var bearerAuth = new BearerAuth((string)myToken);
            bearerAuth.AddTokenToRequest(request);
            var response = await client.SendAsync(request);
            response.EnsureSuccessStatusCode();
            var responseData = await response.Content.ReadAsStringAsync();
            var data = JsonConvert.DeserializeObject<dynamic>(responseData);

            // Obtener los resultados y agregarlos a la lista
            all_player_data.AddRange(data.results);

            // Verificar si hay más páginas
            if (data.nextPage == null)
            {
                break;
            }

            // Incrementar el número de página para la próxima solicitud
            page++;
        }

        // Eliminar columnas no deseadas
        var deleteColumns = new List<string> { "writeLock", "modified.date", "created.date" };
        foreach (var column in deleteColumns)
        {
            all_player_data.RemoveAll(item => item.ContainsKey(column));
        }

        // Imprimir los datos
        foreach (var item in all_player_data)
        {
            Console.WriteLine(item);
        }
    }
}

Hello,

The first thing I need ask is, why are you building your own web-request?

You can use the CloudSave SDK: Unity SDK sample

You can get it from the package manager easily:

FWIW, this is what the SDK does internally:

async Task<Dictionary<string, Item>> LoadWithErrorHandlingAsync(IAccessClassOptions options, ISet<string> keys = null)
{
    return await m_ErrorHandler.RunWithErrorHandling(async() =>
    {
        var result = new Dictionary<string, Item>();
        Response<GetItemsResponse> response;
        string lastAddedKey = null;
        do
        {
            response = await m_PlayerDataApiClient.LoadAsync(keys, lastAddedKey, options.AccessClass, options.PlayerId);
            var items = response.Result.Results;
            if (items.Count > 0)
            {
                foreach (var item in items)
                {
                    result[item.Key] = new Item(item);
                }

                lastAddedKey = items[items.Count - 1].Key;
            }
        }
        while (!string.IsNullOrEmpty(response.Result.Links.Next));

        return result;
    });
}

However, If I’m reading between the lines, you have a custom application that will be using CloudSave or some other dedicated server?

In that case, I will refer to the admin and client specs:
Admin: Unity Services Web API docs
Client: Unity Services Web API docs

Both of their pagination operations, as well as the sample code from the SDK above, use “after” not “pages”, so Im guessing that’s why.

Alternatively, if you want, I describe here: Server-side support for managing player economies the way to generate an API client from the specification, which will help you bootstrap a lot better.

However, more details on your use-case will go a long way in my capacity to help you.

Cheers!

Gab