Collection issue

Hi,

So I tried to write an object to add to the player’s collection, following the doc example here :

var saveGame = "{ \"progress\": 50 }";
var myStats = "{ \"skill\": 24 }";
var objectIds = await client.WriteStorageObjectsAsync(session, new WriteStorageObject
{
  Collection = "saves",
  Key = "savegame",
  Value = saveGame
}, new WriteStorageObject
{
  Collection = "stats",
  Key = "skills",
  Value = myStats
});
Debug.LogFormat("Successfully stored objects: [{0}]", string.Join(",\n   ", objectIds));

Unity refuses to compile and throws me the two following errors :

Assets/Scripts/Network/NakamaClient.cs(470,72): error CS1503: Argument 2: cannot convert from ‘Nakama.WriteStorageObject’ to ‘Nakama.IApiWriteStorageObject

Assets/Scripts/Network/NakamaClient.cs(475,12): error CS1503: Argument 3: cannot convert from ‘Nakama.WriteStorageObject’ to ‘Nakama.RetryConfiguration’

Thanks

Hello @atv.

The function is being called incorrectly, namely the last parameter and note that the function isn’t a variadic function.

The second argument can be either an instance of an object new WriteStorageObject or an array of instances of objects new[]{ new WriteStorageObject{...}, new WriteStorageObject{...}, ...} .

Documentation: .NET/Unity - Heroic Labs Documentation

Yes, but as I said, I just copy pasted from the documentation.

How would I do server side in TS to check if a player has a storage object and if he does not to create a new one for one with default value in it ? Only on server, I don’t want the client to dictate what should be in the object

Thank you for pointing out that inconsistency in the documentation.

Instead of checking on read operations, you can use after hook pattern to the initialize the setup of the user, as described here: Initialize new users - Heroic Labs Documentation

Thanks, but it’s only in Lua and Go from what I see in the docs, there’s no equivalent in TS ?

That page doesn’t have snippets in TS but you can check the page for that feature here: Basics - Heroic Labs Documentation

I’ve checked the link you sent, I still do not understand how to write in TS how to check if a user has already a collection and if not, how to create a new one for him or if he does have collection, send its content to the client.

Actually, I don’t even know if in order to save player in game currency and game cards that he can use later if I need to create a collection, a wallet or use its metadata or a combinaison of these.

Hi again,
I’m sorry but I still need answer on this as I’m late on a client deadline here.

The doc page for metadata is returning
NoSuchKey
The specified key does not exist.

https://heroiclabs.com/docs/nakama/client-libraries/server-framework/recipes/updating-user-metadata

And same for collection or wallets, how to simply check server side if a user has a collection and return it or create a new one with defaults value if he does not have one ?

This page only covers lua and go, I need TS :

This page only gives me how to make an rpc function from client to server, not what I’m looking for to check the existence of a collection of a user and how to return it.

There is also nothing on to modify the object only by the server in TS

Hi @atv

Have you checked this Function Reference - Heroic Labs Documentation ?

It lists all functions available within Nakama on the server with examples, including how to modify collections and wallets.

Yes I ended up there and I checked it.

I got this RPC function that the client calls on login to see if there is a collection

let rpcCheckForUserCollection: nkruntime.RpcFunction =function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) 
{
   let storageObject:nkruntime.StorageReadRequest=
   {
       collection:"UserCollection",
       key:"Cards",
       userId:ctx.userId,

   }

   if(storageObject==null)
   {
       logger.debug("Storage object is considered to be null");
   }
   else
   {
       logger.debug("It seems there is something");
   }

    
    logger.debug(storageObject.collection,storageObject.key,storageObject.userId)
}

There is no collection for the moment but for some reason, the storageObject is not null, it shows me the “it seems there is something” debug message.

If I try this instead :

let rpcCheckForUserCollection: nkruntime.RpcFunction =function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) 
{
    logger.debug("Check for user collection rpc called");

    let user_id =ctx.userId;

    let result: nkruntime.StorageObjectList = {};
    try {
        let result = nk.storageList(user_id, "collection", 10);
    } catch (error) {
        // Handle error
        logger.debug("Error in listing user objects");
    }

    if(result==null)
    {
        logger.debug("No object in storage");
    }
    
    result.objects?.forEach(r => {
        logger.info('Storage object: %s', JSON.stringify(r));
    });


}

I only have the first logger debug appearing, then nothing, not even the logger telling me there is no object (as I assume that if there are no object, result will return null …?)

If I modify the previous code but this time to see if the array result.objects?.length is equal to 0 (no object) nothing happens neither.

let rpcCheckForUserCollection: nkruntime.RpcFunction =function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) 
{
    logger.debug("Check for user collection rpc called");

    let user_id =ctx.userId;

    let result: nkruntime.StorageObjectList = {};
    try {
        let result = nk.storageList(user_id, "collection", 10);
    } catch (error) {
        // Handle error
        logger.debug("Error in listing user objects");
    }

    if(result.objects?.length==0)
    {
        logger.debug("No object in storage");
    }
    
    result.objects?.forEach(r => {
        logger.info('Storage object: %s', JSON.stringify(r));
    });
}

@atv There’s a bit to unpack here, so i’ll break it down into 1, 2, and 3 in order of your code snippets.

  1. You’re not reading any data from the database. let storageObject:nkruntime.StorageReadRequest is a read request definition, which you never execute, and then check if the read request itself is null, which it obviously is not as you’ve just declared and initialised it above.
  2. If I’m reading your code right, the result variable is re-declared inside the try-catch block. The outer variable is never used beyond the initial empty value you give it. So if(result==null) is always false, and the result contains nothing. Try changing let result = nk... to result = nk... inside your try block.
  3. Same problem as (2) above, you’re re-declaring your result variable rather than updating the value of the outer result.

Please note that while we do our best there’s only so much we can do to help with general code questions like this, we’re best placed to help with Nakama-specific questions instead.

The code snippets that I used are all coming from the examples in the documentation, the only thing I added are the if statements, so if there is a problem with repeating the result variable, it’s not my fault. I just copy and pasted the example code in the docs.

Again, I just want to check if a collection exist before either creating a new one or getting back the one that already exist.

Ok, I’ve managed to make it more or less work as I wanted.
Now, I have another question :
For the value, can I use it to represent the amount of a specific object the user has or is it not intented to be use this way ?

Like this :

if(result.objects?.length==0)
    {
        //If the user does not have collection, we create a new one for him/
        logger.debug("There is nothing in the player's collection");

        let userId =user_id;
        let newObjects: nkruntime.StorageWriteRequest[] = [
            { collection: "Cards", key: "RareCards", userId, value: {["RareCards"]:10}},
            
            ];
            
            try {
                nk.storageWrite(newObjects);
            } catch (error) {
                // Handle error
            }

    }

Also, I have this function on Unity to get the collection back, it works, but how would I parse the result to create each object ? Tried with a class, similar to the messages and opcode exchanges but it does not work.

  private async void AskForUserCollection()
    {
        const int limit = 100; // default is 10.
        var result = await client.ListUsersStorageObjectsAsync(session, "Cards", session.UserId, limit);
        
        Debug.LogFormat("List objects: {0}", result);
       


    }

I took example from here :

But there no explanation on how to then parse the collection and make an array of cards for the deck in that instance.

Tried also this function copy pasted from the documentation

var result = await client.ReadStorageObjectsAsync(session, new StorageObjectId {
  Collection = "saves",
  Key = "savegame",
  UserId = session.UserId
});
Debug.LogFormat("Read objects: [{0}]", string.Join(",\n  ", result.Objects));

But Unity refuses to compile :

error CS1503: Argument 2: cannot convert from ‘Nakama.StorageObjectId’ to ‘Nakama.IApiReadStorageObjectId

So I saw by checking the pirate panic example on git hub that there is an rpc function used to load the cards to the client.

So I created a rpc function to do the same thing in unity :

 private async void AskForUserCollection()
    {
       

        string clientAsks = "Client asks to load its collection";
        string jsonFromClient = JsonUtility.ToJson(clientAsks);
      
        var payload = jsonFromClient;
        var rpcid = "loadUserCollection";

        var serverResponse = await client.RpcAsync(session, rpcid, payload);

        Debug.Log("server payload for loading user collection is :" + payload);
  }

On the server, this is the function :

let rpcLoadUserCollection: nkruntime.RpcFunction =function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string):string
{
    logger.debug("Load user collection RPC called");

    let user_id =ctx.userId; 

    let result: nkruntime.StorageObjectList = {};
    try {
       result = nk.storageList(user_id, "PowerCards", 10);
    } catch (error) {
        // Handle error
        logger.debug("Error in listing user objects");
    }

  
    
    result.objects?.forEach(r => {
        logger.info('Storage object: %s', JSON.stringify(r));
    });

    if(result.objects?.length==0)
    {
        logger.debug("For some reason, it still thinks we have no collection");
        
    }
    else
    {
        logger.debug("We do have a collection, let's send it to the client");
       
     
        

    }
    let debug =JSON.stringify(result.objects);

    logger.debug(debug);

    return JSON.stringify(debug);

}

Now, in the pirate panic example, I saw that the function used was the StoragerReadRequest, whereas here I want to use the Storage Object List, I also understand I need to define the temp debug value I used as an interface but with the storagereadrequest, I assume I can only read an object of a specific key but how do I get all object of the given collection, do I create an interface with all the value contained inside the result meaning the time created, the time modified, the user id …?

You can parse your JSON responses with the TinyJson library provided as part of Nakama (GitHub - zanders3/json: A really simple C# JSON Parser in 350 lines) or JsonUtility which comes with Unity engine.

But if you are only listing objects in a collection, you should call the API directly: https://heroiclabs.com/docs/nakama/concepts/collections/#list-objects (all types are defined here: https://github.com/heroiclabs/nakama-dotnet/blob/master/Nakama/ApiClient.gen.cs)

Yes I know.

I know I can use this

const int limit = 100; // default is 10.
var result = await client.ListUsersStorageObjectsAsync(session, "saves", session.UserId, limit);
Debug.LogFormat("List objects: {0}", result);

It’s just that I can’t parse the result variable not matter what I tried to do and I can’t the IApi
I have a specified cast is not valid error.

 IApiStorageObject objects = (IApiStorageObject)result;

So I tried something else as I think the problem that I have comes from the fact that I don’t understand the purpose of key and value in a collection.

Let’s say I have a collection of deck cards of different colors, blue, red, green and the player can have multiple instance of each like 2 blue cards, 5 red cards, 3 green cards.

Do I write the collection with three keys, blue, red, green and then use the value as the amount owned for each card ?

Or do I create one collection with one key with 3 values : blue : 2 , red:5 , green : 3 ?

I started with the first but I feel like in the pirate panic exemple it’s actually the second way of doing ?

So I tried this instead in my rpc function to get the collection :

let rpcLoadUserCollection: nkruntime.RpcFunction =function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string):string
{
    logger.debug("Load user collection RPC called");

     let storageReadReq: nkruntime.StorageReadRequest = {
        key: "PowerCards",
        collection: "PowerCards",
        userId: ctx.userId,
    }

    let objects: nkruntime.StorageObject[];
    try {
        objects = nk.storageRead([storageReadReq]);
    } catch(error) {
        logger.error('storageRead error:');
        throw error;
    }

    if (objects.length === 0) {
        throw Error('user cards storage object not found');
    }

    let storedCardCollection = objects[0].value as PowerCardsStorageInterface;
    let debug = JSON.stringify(storedCardCollection);
    logger.debug(debug);
    return JSON.stringify(storedCardCollection);




}

The power card interface is composed with a string and a number and I have a equivalent public class in unity to decode it.
The logger shows that the storedCardCollection is correctly parse to json with the correct content of its value but in unity, the object returned is empty.

I found the solution thanks to this post here :

For my particular case use, I decided to use the key as the name of the object and put inside the value the amount of that object and also its ID number as I need it for other things.

Works fine now.