排行榜 #

排行榜是向任何游戏增加社交和竞争元素的好途径。以这种方式在您的玩家中促进竞争很有趣。服务器支持的单个排行榜没有数量限制,其中每个都是分数榜,跟踪单独的记录。

对于您游戏中的分数值应该代表什么,服务器没有特别的要求。排行榜刚创建时,按值排序。如果您在记录中使用的是单圈时间或货币,您会首选以 ASCDESC 模式对结果排序。在创建时,您还必须指定运算符来控制如何将分数提交到排行榜:“best”、“set”、“incr”或“decr”。

您可以用排行榜跟踪您在意的任何分数。一些恰当的例子:最高分、最长的生存时间、最快的单圈时间、最快的级别达成以及任何其他可以作为竞争标准的东西!

排行榜在服务器中是动态的,因为它们不需要像您以前使用过的 Google Play Games 或 Apple Game Center 那样进行预配置。排行榜可通过服务器端代码创建。

排行榜对象 #

每个排行榜都是一个记录集合,其中每条记录都是带有元数据的排名分数。排行榜由一个 ID 对其进行唯一标识。

排行榜记录依据其配置的排序方式排序:DESC(默认)或ASC。排序方式在创建排行榜时即已确定,之后无法更改。

运算符 #

配置使用以下 operator 值设置排行榜记录分数的方式:

  • set - 值将被设置为提交的分数
  • best - 仅当提交的分数高于现有分数时,才会更新该值
  • incr - 此值将增加正提交给它的分数
  • decr - 此值将减少正提交给它的分数

创建排行榜后将无法更改运算符。在创建后,所有排行榜配置都是不变的。如需更改排序方式或运算符,应当删除排行榜,并新建一个排行榜。

通过将条目 ID 设置为用户 ID 和当前 Unix 时间的组合,您可以实现街机风格的排行榜(其中单个用户发布多个条目)(例如,<user-id>:<unix-time>)。

重置时间表 #

您可以为每个排行榜分配一个可选的重置时间表。排行榜中包含的记录将按照此时间表过期,在每个重置周期用户都能提交新分数。在每个重置周期结束时,服务器会使用当前排行榜状态触发回调。请阅读排行榜最佳做法并参见分层联赛指南,其中有用例示例。

在创建排行榜时,以 CRON 格式定义重置时间表。如果排行榜没有重置时间表,则其记录永不过期。

排行榜记录 #

每个排行榜包含一个按其分数排序的记录的列表。

所有记录都属于所有者。这通常是一个用户,但也可以是其他对象,例如群组 ID 或某种其他自定义 ID。每个所有者将仅拥有每个排行榜的一条记录。如果排行榜到期,每个所有者都可以提交一个新的分数,该分数将滚动。

随着所有者的进步,每条记录中的分数将被更新。分数可以根据需要随时更新,并且可以根据排行榜排序顺序和运算符的组合进行增减。

Client
1
2
3
curl -X POST "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>" \
  -H 'Authorization: Bearer <session token>'
  -d '{"record": {"score": 100}}'
Client
1
2
3
4
var leaderboardId = "level1";
var submission = {score: 100};
var record = await client.writeLeaderboardRecord(session, leaderboardId, submission);
console.log("New record username %o and score %o", record.username, record.score);
Client
1
2
3
4
const string leaderboardId = "level1";
const long score = 100L;
var r = await client.WriteLeaderboardRecordAsync(session, leaderboardId, score);
System.Console.WriteLine("New record for '{0}' score '{1}'", r.Username, r.Score);
Client
1
2
3
4
5
6
7
8
9
auto successCallback = [](const NLeaderboardRecord& record)
{
    std::cout << "New record with score " << record.score << std::endl;
};

string leaderboardId = "level1";
int64_t score = 100;

client->writeLeaderboardRecord(session, leaderboardId, score, opt::nullopt, opt::nullopt, successCallback);
Client
1
2
3
4
final String leaderboard = "level1";
long score = 100L;
LeaderboardRecord r = client.writeLeaderboardRecord(session, leaderboard, score);
System.out.format("New record for %s score %s", r.getUsername(), r.getScore());
Client
1
2
3
4
5
6
7
var leaderboard_id = "level1"
var score = 100
var record : NakamaAPI.ApiLeaderboardRecord = yield(client.write_leaderboard_record_async(session, leaderboard_id, score), "completed")
if record.is_exception():
    print("An error occurred: %s" % record)
    return
print("New record username %s and score %s" % [record.username, record.score])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
POST /v2/leaderboard/<leaderboardId>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>

{
  "record": {
    "score": 100
  }
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
local leaderboard_id = "level1"
local score = 100
local subscore = 0
local metadata = json.encode({ weather_conditions = "rain" })
local operator = "best"

local result = client.write_leaderboard_record(leaderboard_id, metadata, operator, score, subscore)
if result.error then
  print(result.message)
  return
end

记录的元数据 #

每条记录在提交时可有选择地包含分数或所有者的附加数据。附加字段必须采用 JSON 编码,且作为元数据提交。

元数据的一个用例是驾驶游戏中的比赛状况,例如天气,这可以在用户列出分数时提供额外的 UI 提示。

1
2
3
4
5
{
  "surface": "wet",
  "timeOfDay": "night",
  "car": "Porsche 918 Spyder"
}

创建排行榜 #

排行榜可以在开始时或在注册功能内通过服务器端代码创建。给予排行榜的 ID 用于向其提交分数。

Server
1
2
3
4
5
6
7
8
9
local id = "level1"
local authoritative = false
local sort = "desc"
local operator = "best"
local reset = "0 0 * * 1"
local metadata = {
  weather_conditions = "rain"
}
nk.leaderboard_create(id, authoritative, sort, operator, reset, metadata)
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
id := "level1"
authoritative := false
sort := "desc"
operator := "best"
reset := "0 0 * * 1"
metadata := map[string]interface{}{"weather_conditions": "rain"}

if err := nk.LeaderboardCreate(ctx, id, authoritative, sort, operator, reset, metadata); err != nil {
    // Handle error.
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let id = '4ec4f126-3f9d-11e7-84ef-b7c182b36521';
let authoritative = false;
let sort = nkruntime.SortOrder.DESCENDING;
let operator = nkruntime.Operator.BEST;
let reset = '0 0 * * 1';
let metadata = {
  weatherConditions: 'rain',
};
try {
    nk.leaderboardCreate(id, authoritative, sort, operator, reset, metadata);
} catch(error) {
    // Handle error
}

提交分数 #

用户可向排行榜提交分数并随时更新它。在提交分数后,排行榜的预配置排序和运算符决定运算将有什么效果。

“set”运算符将确保排行榜记录始终保留最新的值,即使此值比前一个值更差也是如此。

用“best”运算符向排行榜提交,可确保记录跟踪此记录出现过的最佳值。对于降序排行榜,这是指最高值,对于升序则是指最低值。如果没有此记录的先前值,其作用就像“set”。

使用“incr”运算符时,新值将添加到此记录的现有分数。如果没有此记录的先前值,其作用就像“set”。

Client
1
2
3
curl -X POST "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>" \
  -H 'Authorization: Bearer <session token>'
  -d '{"score": 100}'
Client
1
2
3
4
var leaderboardId = "level1";
var submission = {score: 100};
var record = await client.writeLeaderboardRecord(session, leaderboardId, submission);
console.log("New record username %o and score %o", record.username, record.score);
Client
1
2
3
4
const string leaderboard = "level1";
const long score = 100L;
var r = await client.WriteLeaderboardRecordAsync(session, leaderboard, score);
System.Console.WriteLine("New record for '{0}' score '{1}'", r.Username, r.Score);
Client
1
2
3
4
5
6
7
8
9
auto successCallback = [](const NLeaderboardRecord& record)
{
    std::cout << "New record with score " << record.score << std::endl;
};

string leaderboardId = "level1";
int64_t score = 100;

client->writeLeaderboardRecord(session, leaderboardId, score, opt::nullopt, opt::nullopt, successCallback);
Client
1
2
3
4
final String leaderboard = "level1";
long score = 100L;
LeaderboardRecord r = client.writeLeaderboardRecord(session, leaderboard, score);
System.out.format("New record for %s score %d", r.getUsername(), r.getScore());
Client
1
2
3
4
5
6
7
var leaderboard_id = "level1"
var score = 100
var record : NakamaAPI.ApiLeaderboardRecord = yield(client.write_leaderboard_record_async(session, leaderboard_id, score), "completed")
if record.is_exception():
    print("An error occurred: %s" % record)
    return
print("New record username %s and score %s" % [record.username, record.score])
Client
1
2
3
4
5
6
7
8
9
POST /v2/leaderboard/<leaderboardId>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>

{
  "score": 100
}
Client
1
2
3
4
5
6
7
8
9
local leaderboard_id = "level1"
local score = 100
local operator = "best"

local result = client.write_leaderboard_record(leaderboard_id, nil, operator, score)
if result.error then
  print(result.message)
  return
end

列出记录 #

用户可以将排行榜的记录列表。这便于与其他用户比较分数并看到他们的名次。

按分数列表 #

按照排行榜中的排序方法依据分数进行排序,是列出记录的标准方法。

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
var leaderboardId = "level1";
var result = await client.listLeaderboardRecords(session, leaderboardId);
result.records.forEach(function(record) {
  console.log("Record username %o and score %o", record.username, record.score);
});
Client
1
2
3
4
5
6
const string leaderboardId = "level1";
var result = await client.ListLeaderboardRecordsAsync(session, leaderboardId);
foreach (var r in result.Records)
{
    System.Console.WriteLine("Record for '{0}' score '{1}'", r.Username, r.Score);
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = [](NLeaderboardRecordListPtr recordsList)
{
    for (auto& record : recordsList->records)
    {
        std::cout << "Record username " << record.username << " and score " << record.score << std::endl;
    }
};

string leaderboardId = "level1";

client->listLeaderboardRecords(session, leaderboardId, {}, opt::nullopt, opt::nullopt, successCallback);
Client
1
2
3
4
5
final String leaderboard = "level1";
LeaderboardRecordList records = client.listLeaderboardRecords(session, leaderboard);
for (LeaderboardRecord record : records.getRecordsList()) {
    System.out.format("Record for %s score %d", record.getUsername(), record.getScore());
}
Client
1
2
3
4
5
6
7
8
var leaderboard_id = "level1"
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_async(session, leaderboard_id), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for r in result.records:
    var record : NakamaAPI.ApiLeaderboardRecord = r
    print("Record username %s and score %s" % [record.username, record.score])
Client
1
2
3
4
5
GET /v2/leaderboard/<leaderboardId>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
5
6
7
8
9
local leaderboard_id = "level1"
local result = client.list_leaderboard_records(leaderboard_id)
if result.error then
  print(result.message)
  return
end
for _,record in ipair(result.records) do
  pprint(record)
end

您可以用游标获取下一组结果。

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>?cursor=<next_cursor>" \
  -H 'Authorization: Bearer <session token>'
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var leaderboardId = "level1";

var result = await client.listLeaderboardRecords(session, leaderboardId);
result.records.forEach(function(record) {
  console.log("Record username %o and score %o", record.username, record.score);
});

// If there are more results get next page.
if (result.next_cursor) {
  result = await client.listLeaderboardRecords(session, leaderboardId, null, null, result.next_cursor);
  result.records.forEach(function(record) {
    console.log("Record username %o and score %o", record.username, record.score);
  });
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const string leaderboardId = "level1";
var result = await client.ListLeaderboardRecordsAsync(session, leaderboardId);
foreach (var r in result.Records)
{
    System.Console.WriteLine("Record for '{0}' score '{1}'", r.Username, r.Score);
}
// If there are more results get next page.
if (result.NextCursor != null)
{
    var c = result.NextCursor;
    result = await client.ListLeaderboardRecordsAsync(session, leaderboardId, null, 100, c);
    foreach (var r in result.Records)
    {
        System.Console.WriteLine("Record for '{0}' score '{1}'", r.Username, r.Score);
    }
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
auto successCallback = [this](NLeaderboardRecordListPtr recordsList)
{
    for (auto& record : recordsList->records)
    {
        std::cout << "Record username " << record.username << " and score " << record.score << std::endl;
    }

    if (!recordsList->nextCursor.empty())
    {
        auto successCallback = [this](NLeaderboardRecordListPtr recordsList)
        {
            for (auto& record : recordsList->records)
            {
                std::cout << "Record username " << record.username << " and score " << record.score << std::endl;
            }
        };

        string leaderboardId = "level1";

        client->listLeaderboardRecords(session, leaderboardId, {}, opt::nullopt, recordsList->nextCursor, successCallback);
    }
};

string leaderboardId = "level1";

client->listLeaderboardRecords(session,
    leaderboardId,
    {},
    opt::nullopt,
    opt::nullopt,
    successCallback
);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
final String leaderboard = "level1";
LeaderboardRecordList records = client.listLeaderboardRecords(session, leaderboard);
for (LeaderboardRecord record : records.getRecordsList()) {
    System.out.format("Record for %s score %d", record.getUsername(), record.getScore());
}

// If there are more results get next page.
if (records.getCursor() != null) {
    var c = result.NextCursor;
    records = client.listLeaderboardRecords(session, leaderboard, null, 100, records.getNextCursor());
    for (LeaderboardRecord record : records.getRecordsList()) {
        System.out.format("Record for %s score %d", record.getUsername(), record.getScore());
    }
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var leaderboard_id = "level1"
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_async(session, leaderboard_id, null, null, 100), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for r in result.records:
    var record : NakamaAPI.ApiLeaderboardRecord = r
    print("Record username %s and score %s" % [record.username, record.score])

if result.next_cursor:
    result = yield(client.list_leaderboard_records_async(session, leaderboard_id, null, null, 100, result.next_cursor), "completed")
    if result.is_exception():
        print("An error occurred: %s" % result)
        return
    for r in result.records:
        var record : NakamaAPI.ApiLeaderboardRecord = r
        print("Record username %s and score %s" % [record.username, record.score])
Client
1
2
3
4
5
GET /v2/leaderboard/<leaderboardId>?cursor=<next_cursor>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
local leaderboard_id = "level1"
local cursor = nil
local owner_id = nil
local limit = 100
repeat
  local result = client.list_leaderboard_records(leaderboard_id, owner_id, limit, cursor)
  if result.error then
    print(result.message)
    return
  end
  for _,record in ipair(result.records) do
    pprint(record)
  end
until not cursor

按好友列表 #

您可以使用一些所有者 ID 筛选出仅由这些用户拥有的记录。可以使用此方法来仅检索属于用户好友的分数。

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>?owner_ids=some&owner_ids=friends" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
6
var leaderboardId = "level1";
var ownerIds = ["some", "friends", "user ids"];
var result = await client.listLeaderboardRecords(session, leaderboardId, ownerIds);
result.records.forEach(function(record) {
  console.log("Record username %o and score %o", record.username, record.score);
});
Client
1
2
3
4
5
6
7
const string leaderboardId = "level1";
var ownerIds = new[] {"some", "friends", "user ids"};
var result = await client.ListLeaderboardRecordsAsync(session, leaderboardId, ownerIds);
foreach (var r in result.OwnerRecords)
{
    System.Console.WriteLine("Record for '{0}' score '{1}'", r.Username, r.Score);
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
auto successCallback = [](NLeaderboardRecordListPtr recordsList)
{
    for (auto& record : recordsList->records)
    {
        std::cout << "Record username " << record.username << " and score " << record.score << std::endl;
    }
};

vector<string> ownerIds = { "some", "friends", "user ids" };
string leaderboardId = "level1";

client->listLeaderboardRecords(session, leaderboardId, ownerIds, opt::nullopt, opt::nullopt, successCallback);
Client
1
2
3
4
5
6
String leaderboard = "level1";
String[] ownerIds = new String[] {"some", "friends", "user ids"};
LeaderboardRecordList records = await client.ListLeaderboardRecordsAsync(session, leaderboard, ownerIds);
for (LeaderboardRecord record : records.getRecordsList()) {
    System.out.format("Record for %s score %d", record.getUsername(), record.getScore());
}
Client
1
2
3
4
5
6
7
8
9
var leaderboard_id = "level1"
var owner_ids = ["some", "friend", "user id"]
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_async(session, leaderboard_id, owner_ids), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for r in result.records:
    var record : NakamaAPI.ApiLeaderboardRecord = r
    print("Record username %s and score %s" % [record.username, record.score])
Client
1
2
3
4
5
GET /v2/leaderboard/<leaderboardId>?owner_ids=some&owner_ids=friends
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local leaderboard_id = "level1"
local owner_ids = { "some", "friend", "user id" }
local result = client.list_leaderboard_records(leaderboard_id, owner_ids)
if result.error then
  print(result.message)
  return
end
for _,record in ipair(result.records) do
  pprint(record)
end

列出已过期记录 #

在每次排行榜重置后,将从排行榜排名中移除过期记录,但并不将其删除。用户可以列出所需时段的过期记录。

例如,列出上一周的过期记录:

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>?overrideExpiry=604800" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
6
var leaderboardId = "<leaderboardId>";
var overrideExpiry = 604800;
var result = await client.listLeaderboardRecords(session, leaderboardId, overrideExpiry);
result.records.forEach(function(record) {
  console.log("Record username %o and score %o expired on %o", record.username, record.score, record.expiryTime);
});
Client
1
2
3
4
5
6
7
const string leaderboardId = "<leaderboardId>";
const int64 overrideExpiry = 604800;
var result = await client.ListLeaderboardRecordsAsync(session, leaderboardId, overrideExpiry);
foreach (var r in result.Records)
{
    System.Console.WriteLine("Record for '{0}' and score '{1}' expired on '{2}'", r.Username, r.Score, r.ExpiryTime);
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
auto successCallback = [](NLeaderboardRecordListPtr recordsList)
{
    for (auto& record : recordsList->records)
    {
        std::cout << "Record username " << record.username << " and score " << record.score << " expired on " << record.expiryTime << std::endl;
    }
};

string leaderboardId = "<leaderboardId>";
int overrideExpiry = 604800;

client->listLeaderboardRecords(session, leaderboardId, overrideExpiry, {}, opt::nullopt, opt::nullopt, successCallback);
Client
1
2
3
4
5
6
final String leaderboard = "<leaderboardId>";
final Long overrideExpiry = 604800;
LeaderboardRecordList records = client.listLeaderboardRecords(session, leaderboard, overrideExpiry).get();
for (LeaderboardRecord record : records.getRecordsList()) {
    System.out.format("Record for %s score %d", record.getUsername(), record.getScore());
}
Client
1
2
3
4
5
6
7
8
9
var leaderboard_id = "<leaderboardId>"
var override_expiry = 604800
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_async(session, leaderboard_id, override_expiry), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for r in result.records:
    var record : NakamaAPI.ApiLeaderboardRecord = r
    print("Record username %s and score %s expired on %s" % [record.username, record.score, record.expiry_time])
Client
1
2
3
4
5
GET /v2/leaderboard/<leaderboardId>?overrideexpiry=604800
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local leaderboard_id = "<leaderboardId>"
local override_expiry = 604800
local result = client.list_leaderboard_records(leaderboard_id, owner_ids, limit, cursor, override_expiry)
if result.error then
  print(result.message)
  return
end
for _,record in ipair(result.records) do
  pprint(record)
end

列出围绕所有者的排行榜记录 #

获取围绕所有者的排行榜记录列表。

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboard_id>/owner/<owner_id>?limit=<limit>"
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
var id = "someid";
var ownerId = "some user ID";
var limit = 100;
var result = await client.listLeaderboardRecordsAroundOwner(session, id, ownerId, limit);
Client
1
2
3
4
var leaderboardId = "someid";
var ownerId = session.UserId;
var limit = 100;
var result = await client.ListLeaderboardRecordsAroundOwnerAsync(session, leaderboardId, ownerId, limit);
Client
1
2
3
4
string leaderboardId = "level1";
string ownerId = "some user ID";
int32_t limit = 100;
client->listLeaderboardRecordsAroundOwner(session, leaderboardId, ownerId, limit, successCallback);
Client
1
2
3
4
String id = "someid";
String ownerId = session.getUserId();
int limit = 100;
LeaderboardRecordList records = client.listLeaderboardRecordsAroundOwner(session, id, ownerId, limit).get();
Client
1
2
3
4
5
6
7
8
9
var leaderboard_id = "level1"
var owner_id = "user id"
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_around_owner_async(session, leaderboard_id, owner_id), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for r in result.records:
    var record : NakamaAPI.ApiLeaderboardRecord = r
    print("Record username %s and score %s" % [record.username, record.score])
Client
1
2
3
4
5
GET /v2/leaderboard/<leaderboard_id>/owner/<owner_id>?limit=<limit>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local leaderboard_id = "level1"
local owner_id = "user id"
local result = client.list_leaderboard_records_around_owner(leaderboard_id, owner_id)
if result.error then
  print(result.message)
  return
end
for _,record in ipair(result.records) do
  pprint(record)
end