Storage Collections Versioning

Hello everyone,
Recently we started to do some changes in our game, that made some of the saved collections structures. Not fully obsolete but some of the fields had change.
For example like this: [Json data]
example:
Collection : character_inventory
Key : character_access

In create time :
{
“character1_isUnlocked”: false,
“character2_isUnlocked”: true,
“character3_isUnlocked”: false,
“character4_isUnlocked”: true,
“character5_character”: false,
“character6_isUnlocked”: false,
“character7_isUnlocked”: false,
“character8_isUnlocked”: false,
“character9_isUnlocked”: true
}

And after game Updates : we Introduced new character so I changed collection json as:

After Update time :
{
“character1_isUnlocked”: false,
“character2_isUnlocked”: true,
“character3_isUnlocked”: false,
“character4_isUnlocked”: true,
“character5_character”: false,
“character6_isUnlocked”: false,
“character7_isUnlocked”: false,
“character8_isUnlocked”: false,
“character9_isUnlocked”: true,
“character10_isUnlocked”: false
}

In create time , I will init these type collections in

const afterCreate: nkruntime.AfterHookFunction<
nkruntime.Session,
nkruntime.AuthenticateEmailRequest

= function (
ctx: nkruntime.Context,
logger: nkruntime.Logger,
nk: nkruntime.Nakama,
data: nkruntime.Session,
req: nkruntime.AuthenticateEmailRequest
){
if (data.created) {
let user_id = ctx.userId;
let character_access = {ABOVE JSON}
let character_inventory: nkruntime.StorageWriteRequest = [
{
collection: “character_inventory”,
key: “character_access”,
userId: user_id,
value: JSON.parse(character_access),
},
];
try {
nk.storageWrite(character_inventory);
} catch (error) {
logger.info(“error”);
}
else
return; // if account is not new return
};

[In create time Json] at that time authenticating users had character1…character9 in collection.

After the update I will introduced new “character10” field or add a new object for the wallet and user collections…
Server code updates and Collection change, Fields add … It will effect to Newly authenticated user,

how will I migrate or add this collection and wallets fields and changes to old user also…

I don’t know we can call this as storage collection versioning, I feel like that or otherwise I need to change the flow?

One simple solution here is to simply assume that if the key does not exist in the user’s storage object then they do not have the character. This will allow you to add as many characters as you like later down the line and the logic will work the same.

e.g. for a player who has unlocked character 1, 2 and 5 their object will look as follows:

{
  "character1_isUnlocked": true,
  "character2_isUnlocked": true,
  "character5_isUnlocked": true
}

Here you use the absence of the key as an implicit false value.

This avoid the needs for migrating your data and once the logic has been added to the game server/client existing players with the current storage object structure will continue to work as normal (they won’t have character 10).

Where possible it is best to try and avoid breaking the backwards compatibility of your data structures. I would not recommend pre-planning a data migration system.

That’s Okay, but you see…
I updated the collection of character_inventory to add new field with new character name: example as follows
{
“character1_isUnlocked”: false,
“character2_isUnlocked”: true,
“character3_isUnlocked”: false,
“character4_isUnlocked”: true,
“character5_character”: false,
“character6_isUnlocked”: false,
“character7_isUnlocked”: false,
“character8_isUnlocked”: false,
“character9_isUnlocked”: true,

“character10_isUnlocked”: false,
“character11_isUnlocked”: false,
}

“character10_isUnlocked” and “character11_isUnlocked” are new character or fields.
this storage change will effect to the newly authenticated client as per above code.

How will i update this to old clients also? @tom

I understand that character10_isUnlocked is a new field. My proposal is that it shouldn’t matter for old clients. Your server or client logic can check for the existence of the key character10_isUnlocked and if it doesn’t exist they can safely assume the player hasn’t unlocked the character yet (i.e. the value is implicitly false).

When the player eventually unlocks the character, you simply add the key with a value of true to their character_inventory storage object (or update the value to true if the key does exist).

To expand on this concept further, let’s assume we’re working with a standard player’s inventory where they can store items such as a sword, shield, potion etc.

You may represent the player’s current inventory like this:

{
  "items": [
    { "id": "sword", "quantity": 1 },
    { "id": "potion", "quantity": 10 }
  ]
}

In this example the player is assumed to not have the shield item yet because it doesn’t exist in their inventory storage object.

The same concept can be applied to your character unlock mechanic.

OK, then
what about the storage. after the game update I need to update new wallet field.? @tom

And also in my logic some of other collections like avatars or skins of the user.
That is actually i done initially in server and after game starts , All the Skins and avatars in the game will loaded to client with corresponding key values like true or false.
how will i change this collections. [ To add new avatar and skin to the game! Asset will add in client side and the index or item is want to add in server for init , according to this game starts all the avatars will load to client]

In this scenario I would advise having a storage object that is defined as a form of global (public read / no write) configuration that clients can access and update their local version accordingly.

For example, you could create a configuration object that lived in a config collection that looked like this:

{
  "availableCharacters": ["character1", "character2", "character3", ... ],
  "availableSkins": ["skin1", "skin2", "skin3", ...],
  "availableAvatars": [...],
}

The client could read this configuration file and update the UI appropriately. When you want to add a new character to the game, you add it to the availableCharacters array. The client will then read this the next time it launches.

This way you do not need to explicitly keep a record up to date for every player in your game to determine what they don’t have, and instead you only update their records when they do have something.

Thank you for the advice.

OK I will try this implementation as per you said @tom

What about the player wallet - updating of field ?

No problem.

Player wallet should act in a similar way. If your game client is checking for the existence of a player’s gems amount but their wallet looks like this:

{
  "coins": 12,
  "stars": 5
}

Then the client should assume the player has 0 gems. You only need to add the gems key to their wallet once the player actually receives some.

If you are adding a new virtual currency to your game and you want every player to start with some (e.g. 100 gems) then you can do this with an After Hook when they next authenticate.

@tom Can you given an example for adding the wallet new key?
i only see like this let changeset = { gems: gemChange};
to change the values of gem field

The changeset is all you need. Nakama is smart enough to add or update the value in the player’s wallet appropriately.

You mean In changeset i can add the new field also?
Like old wallet :
{
“coins”: 12,
“stars”: 5
}
To add new wallet data : let changeset = { gems: 0};
{
“coins”: 12,
“stars”: 5,
“gems”:0
}

Yes, that’s correct.

Issue . here. I can’t solve this logic