Trouble Retrieving Lua Server Stored Data on Godot Client

Hi there!

I’m currently trying to build a system that generates shareable match_ids (match codes, as I’m calling them) that allow players to join games using the code for my Lua/Godot game. I’m doing it in a manner suggested here; therefore I have a function that is called on the creation of a match that generates a match code, uses it as the key for storing the actual match_id in a collection, and then returns both the code and the match_id to the player. The main functions for this are:

local function create_match()
     local match_id = nakama.match_create("match_control", {})

     local payload = {
          ["match_id"] = match_id,
          ["match_code"] = create_match_code(match_id)
     }

     return nakama.json_encode(payload)
end

-- Create match_code, store match_code,match_id as a key,value relationship
-- and return match_id
function create_match_code(match_id)
     -- Keep trying it until a code that works is found
     local count = 0

     while count < 30 do     
          local code = generate_match_code()

          local data = {
               ["match_id"] = match_id
          }

          local object = { collection = "match_codes", 
               key = code, value = { data }}

          local success = nakama.storage_write({object})

          if success then
               return code
          end

          count = count + 1
     end

     error("Game code could not be created")
end

Where I’m having trouble with is retrieving that match_id with the code. I’ve tried doing it both with a Lua RPC that the Godot client calls, and directly from the client using read_storage_objects_async(). Both just return empty dictionaries with no objects. I think it might have something to do with my lack of definition of user_id’s in my read and write, but that is necessary as any player should be able to look this data up, right?

Below are my attempts in Lua and Godot:

Lua RPC attempt

-- Takes a match_code and returns its associated match_id
local function match_code_to_match_id(context, payload)
     local match_code = nakama.json_decode(payload).match_code

     local object_id = { collection = "match_codes",
          key = match_code }

     local objects, err = nakama.storage_read({object_id})

     if objects then
          nakama.logger_info("Value: "..serialize_table(objects[1].value))
          return nakama.json_encode(objects[1].value)
     end

     error(err)
end
var response : NakamaAPI.ApiRpc = await _client.rpc_async(_session, \
		"match_code_to_match_id", JSON.stringify(payload))

Pure Godot attempt

var response : NakamaAPI.ApiStorageObjects = await _client.read_storage_objects_async(_session, \
		[NakamaStorageObjectId.new("match_codes", match_code)])

Hey @districtjackson a few things:

I’d avoid looping 30 times to do the storage write. I’d just do it once and if there is a network error between the server and database you can always have the user try again.

I would definitely pass a user ID to your storage write. This will scope the storage object to the user which will help to avoid concurrent writes to the system user. Concurrent writes will cause match codes to get overwritten, or cause write rejections if you pass a version string.

When the data is scoped the a user, the server can still look it up for any player, regardless of who trigged the RPC. The storage object can accept permissions that affect which client can directly look up the storage object, though.

The last thing to check is that the storage object exists in your Nakama console before it’s requested.

Thanks for the reply!

I’d avoid looping 30 times to do the storage write. I’d just do it once and if there is a network error between the server and database you can always have the user try again.

I actually did that not in worry of network errors, but rather in a seemingly misguided attempt to ensure the write found a non-duplicate key. I was under the impression that storage_write would not overwrite data, so I definitely need to change that.

When the data is scoped the a user, the server can still look it up for any player, regardless of who trigged the RPC.

But scoping it to a user would require the server (or the client calling the function) to know who initially created a match, though, right? If so, that’s something I can’t do, as I want the user to just be able to join a match through verbal or text communication of the code.

The last thing to check is that the storage object exists in your Nakama console before it’s requested.

Definitely need to remember that the API Explorer tools exist; it shows that all of the storage objects have a value of null. So now I need to figure out why that is, when the match_id is definitely being returned out to the client.

One other thing to consider is that you can create a match by name. This may be a simpler approach than managing a storage object. For example, you will also need to remove expired codes from your storage under your current approach which will require additional logic.

I think I’ve found the create_match_async() function you are referring to in the Godot addon code, but what exactly does creating a match by name do for me? Can other players then join the match by name?

…you will also need to remove expired codes from your storage under your current approach which will require additional logic.

I do have some logic already in my match_handler to handle a match removing itself from the storage collection, though I am not sure of its efficacy.

Also, know matter what I attempt to write as the value for my storage_write(), null is returned, even if its just a static string. Am I formatting the storage object wrong or something?

local payload = {
          --["match_id"] = match_id
          "test"
     }

local objects = {{ collection = "match_codes", 
          key = code, user_id = user_id, value = { payload }},}

local success = nakama.storage_write(objects)

Hey @districtjackson creating a match by name lets other players then join the match by name. So there would be no need to handle converting a match ID to a shareable code – the match name itself could be the code. Other users pass that code to CreateMatch() and be able to join the match that way.

As for your existing approach, I suggest using pcall on your storage write to get the error message, for example:

local success, result_or_error = pcall(nakama.storage_write, objects)
if success then
    -- Operation succeeded, proceed with your logic
    -- 'result_or_error' will contain whatever 'nakama.storage_write' returns on success
else
    -- Operation failed, handle the error
    print("Error:", result_or_error)
end

I finally got my approach working, though never necessarily found out what allowed storage_write() to finally start accepting my supplied value. It might have just needed to be defined in the JSON-esque style shown below.

local payload = {
          ["match_id"] = match_id
     }

local objects = {{ ["collection"] = "match_codes", 
          ["key"] = code, ["value"] = payload}}

Now I need to figure out if I want to stick to this approach or use the one with match names like you described.

Thanks for all your help!

Glad you’ve resolved the issue, for future reference, please have a look at the Lua’s runtime error handling, it may help you getting a more descriptive reason on why something is failing.

The server logs may also provide more information when something isn’t quite working as you’d expect.

Happy coding.

1 Like

I appreciate the advice, but (in case anyone else has this issue in the future) my problem wasn’t failing the call or spitting out an error of any kind. As long as the value was wrapped up in some sort of table, it would “work”, but only store null.