Lua Runtime #

The game server allows you to load and run custom logic written in Lua. This is useful to implement game code you would not want to run on the client, or trust the client to provide unchecked inputs on.

You can think of this Nakama feature as similar to Lambda or Cloud Functions in other systems. A good use case is if you wanted to grant the user a reward each day that they play the game.

Unlike when writing your server logic in Go or TypeScript, there is no toolchain or other setup needed when writing your code in Lua. Lua is a powerful embeddable scripting language and does not need to be compiled or transpiled. This makes it a good choice if you want to get up and running quickly and easily.

You can learn more about how to write your Lua code in the official documentation.

Develop code #

You can find the full Lua Nakama module function reference here.

Before you begin, create a new folder for your project and open it in an editor of your choice (e.g. VS Code).

Start by creating a new folder called modules and inside create a new file called main.lua. The code below is a simple Hello World example which uses the "Logger" to write a message.

1
2
local nk = require("nakama")
nk.logger_info("Hello World!")

Restrictions #

Compatibility #

The Lua runtime is a Lua 5.1-compatible implementation with a small set of additional packages backported from newer versions - see available functions. For best results ensure your Lua modules and any 3rd party libraries are compatible with Lua 5.1.

Lua runtime code cannot use the Lua C API or extensions. Make sure your code and any 3rd party libraries are pure Lua 5.1.

The Lua virtual machine embedded in the server uses a restricted set of Lua standard library modules. This ensures the code sandbox cannot tamper with operating system input/output or the filesystem.

The list of available Lua modules are:

  • base module
  • math
  • string
  • table
  • bit32
  • Subset of os (only clock, difftime, date, and time functions)

Global state #

The Lua runtime code is executed in instanced contexts (VM pool). You cannot use global variables as a way to store state in memory or communicate with other Lua processes or function calls.

Single threaded #

The use of multi-threaded processing (coroutines) is not supported in the Nakama implementation of the Lua runtime.

Sandboxing #

The Lua runtime code is fully sandboxed and cannot access the filesystem, input/output devices, or spawn OS threads or processes.

This allows the server to guarantee that Lua modules cannot cause fatal errors - the runtime code cannot trigger unexpected client disconnects or affect the main server process.

Error handling #

Lua error handling uses raised errors rather than error return values. If you want to trap the error which occurs in the execution of a function you’ll need to execute it via pcall as a “protected call”.

1
2
3
4
5
6
7
8
9
local function will_error()
  error("This function will always throw an error!")
end

if pcall(will_error) then
  -- No errors with "will_error".
else
  -- Handle errors.
end

The function will_error uses the error function in Lua to throw an error with a reason message. The pcall will invoke the will_error function and trap any errors. We can then handle the success or error cases as needed. We recommend you use this pattern with your Lua code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
local nk = require("nakama")

local status, result = pcall(nk.users_get_username, {"22e9ed62"})
if (not status) then
  nk.logger_error(string.format("Error occurred: %q", result))
else
  for _, u in ipairs(result)
  do
    local message = string.format("id: %q, display name: %q", u.id, u.display_name)
    nk.logger_info(message) -- Will appear in logging output.
  end
end
If the server logger level is set to info (default level) or below, the server will return Lua stack traces to the client. This is useful for debugging but should be disabled for production.

Returning errors to the client #

When writing your own custom runtime code, you should ensure that any errors that occur when processing a request are passed back to the client appropriately. This means that the error returned to the client should contain a clear and informative error message and an appropriate HTTP status code.

Internally the Nakama runtime uses gRPC error codes and converts them to the appropriate HTTP status codes when returning the error to the client.

You can define the gRPC error codes as constants in your Lua module as shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
local error_codes = {
  OK                  = 0,  -- HTTP 200
  CANCELED            = 1,  -- HTTP 499
  UNKNOWN             = 2,  -- HTTP 500
  INVALID_ARGUMENT    = 3,  -- HTTP 400
  DEADLINE_EXCEEDED   = 4,  -- HTTP 504
  NOT_FOUND           = 5,  -- HTTP 404
  ALREADY_EXISTS      = 6,  -- HTTP 409
  PERMISSION_DENIED   = 7,  -- HTTP 403
  RESOURCE_EXHAUSTED  = 8,  -- HTTP 429
  FAILED_PRECONDITION = 9,  -- HTTP 400
  ABORTED             = 10, -- HTTP 409
  OUT_OF_RANGE        = 11, -- HTTP 400
  UNIMPLEMENTED       = 12, -- HTTP 501
  INTERNAL            = 13, -- HTTP 500
  UNAVAILABLE         = 14, -- HTTP 503
  DATA_LOSS           = 15, -- HTTP 500
  UNAUTHENTICATED     = 16  -- HTTP 401
}

Below is an example of how you would return appropriate errors both in an RPC call ands in a Before Hook.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
nk.register_rpc(function(context, payload)
  -- ... check if a guild already exists and set value of `already_exists` accordingly
  local already_exists = true

  if already_exists then
    error({ "guild name is in use", error_codes.ALREADY_EXISTS })
  end

  return nk.json_encode({ success = true })
end, "lua_create_guild")

nk.register_req_before(function(context, payload)
	-- Only match custom Id in the format "cid-000000"
  if not string.match(payload.account.id, "^cid%-%d%d%d%d%d%d$") then
    error({ "input contained invalid data", error_codes.INVALID_ARGUMENT})
  end

  return payload
end, "AuthenticateCustom")

Global state #

The Lua runtime can use global variables as a way to store state in memory and store and share data as needed, but concurrency and access controls are the responsibility of the developer.

Sharing state is discouraged and should be avoided in your runtime code as it is not supported in multi-node environments.

Run the project #

You can use Docker with a compose file for local development or setup a binary environment for:

When this is complete you can run the game server and have it load your code:

1
nakama --logger.level DEBUG

The server logs will show this output or similar which shows that the code we wrote above was loaded and executed at startup.

1
{"level":"info","ts":"...","caller":"server/runtime_lua_nakama.go:1742","msg":"Hello World!","runtime":"lua"}

Next steps #

Have a look at the Nakama project template which covers the following Nakama features:

Related Pages