Unverified Commit 53417189 authored by Simon Esposito's avatar Simon Esposito Committed by GitHub
Browse files

Fix missing fields from js runtime leaderboards/tournaments (#656)

Fix JS runtime missing fields from js runtime leaderboards/tournaments.
Fix JS runtime leaderboard/tournament ownerId argument.
Add missing rank value from JS leaderboardRecordWrite.

Update CHANGELOG.
Resolves #637.
parent 67e4fa85
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -19,6 +19,8 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr
- Fix creator id being read from the wrong argument in JS runtime group update function.
- Fix max count being incorrectly validated in group update JS runtime function.
- Fix error handling when attempting to write records to a tournament that does not exist.
- Fix JS runtime missing fields from leaderboards/tournaments get, list and write functions.
- Fix JS runtime ownerId field not working correctly in leaderboard/tournament records list functions.

## [3.4.0] - 2021-07-08
### Added

data/modules/index.js

deleted100644 → 0
+0 −467
Original line number Diff line number Diff line
"use strict";
var ClanNotificationCode;
(function (ClanNotificationCode) {
    ClanNotificationCode[ClanNotificationCode["Refresh"] = 2] = "Refresh";
    ClanNotificationCode[ClanNotificationCode["Delete"] = 3] = "Delete";
})(ClanNotificationCode || (ClanNotificationCode = {}));
var afterJoinGroupFn = function (ctx, logger, nk, data, request) {
    var _a;
    sendGroupNotification(nk, (_a = request.groupId, (_a !== null && _a !== void 0 ? _a : "")), ClanNotificationCode.Refresh, "New Member Joined!");
};
var afterKickGroupUsersFn = function (ctx, logger, nk, data, request) {
    var _a;
    sendGroupNotification(nk, (_a = request.groupId, (_a !== null && _a !== void 0 ? _a : "")), ClanNotificationCode.Refresh, "Member(s) Have Been Kicked!");
};
var afterLeaveGroupFn = function (ctx, logger, nk, data, request) {
    var _a;
    sendGroupNotification(nk, (_a = request.groupId, (_a !== null && _a !== void 0 ? _a : "")), ClanNotificationCode.Refresh, "Member Left!");
};
var afterPromoteGroupUsersFn = function (ctx, logger, nk, data, request) {
    var _a;
    sendGroupNotification(nk, (_a = request.groupId, (_a !== null && _a !== void 0 ? _a : "")), ClanNotificationCode.Refresh, "Member(s) Have Been Promoted!");
};
var beforeDeleteGroupFn = function (ctx, logger, nk, request) {
    var _a;
    var members = nk.groupUsersList(request.groupId, 100, 0);
    (_a = members.groupUsers) === null || _a === void 0 ? void 0 : _a.every(function (user) {
        var _a;
        if (user.user.userId == ctx.userId) {
            sendGroupNotification(nk, (_a = request.groupId, (_a !== null && _a !== void 0 ? _a : "")), ClanNotificationCode.Delete, "Clan Deleted!");
            return false;
        }
        return true;
    });
    return request;
};
function sendGroupNotification(nk, groupId, code, subject) {
    var _a, _b;
    var members = nk.groupUsersList(groupId, 100);
    var count = (_a = members.groupUsers, (_a !== null && _a !== void 0 ? _a : [])).length;
    if (count < 1) {
        return;
    }
    var notifications = [];
    (_b = members.groupUsers) === null || _b === void 0 ? void 0 : _b.forEach(function (user) {
        var n = {
            code: code,
            content: {},
            persistent: false,
            subject: subject,
            userId: user.user.userId,
        };
        notifications.push(n);
    });
    nk.notificationsSend(notifications);
}
var DeckPermissionRead = 2;
var DeckPermissionWrite = 0;
var DeckCollectionName = 'card_collection';
var DeckCollectionKey = 'user_cards';
var DefaultDeckCards = [
    {
        type: 1,
        level: 1,
    },
    {
        type: 1,
        level: 1,
    },
    {
        type: 2,
        level: 1,
    },
    {
        type: 2,
        level: 1,
    },
    {
        type: 3,
        level: 1,
    },
    {
        type: 4,
        level: 1,
    },
];
var DefaultStoredCards = [
    {
        type: 2,
        level: 1,
    },
    {
        type: 2,
        level: 1,
    },
    {
        type: 3,
        level: 1,
    },
    {
        type: 4,
        level: 1,
    },
];
var rpcSwapDeckCard = function (ctx, logger, nk, payload) {
    var request = JSON.parse(payload);
    var userCards = loadUserCards(nk, logger, ctx.userId);
    if (Object.keys(userCards.deckCards).indexOf(request.cardOutId) < 0) {
        throw Error('invalid out card');
    }
    if (Object.keys(userCards.storedCards).indexOf(request.cardInId) < 0) {
        throw Error('invalid in card');
    }
    var outCard = userCards.deckCards[request.cardOutId];
    var inCard = userCards.storedCards[request.cardInId];
    delete (userCards.deckCards[request.cardOutId]);
    delete (userCards.storedCards[request.cardInId]);
    userCards.deckCards[request.cardInId] = inCard;
    userCards.storedCards[request.cardOutId] = outCard;
    storeUserCards(nk, logger, ctx.userId, userCards);
    logger.debug("user '%s' deck card '%s' swapped with '%s'", ctx.userId);
    return JSON.stringify(userCards);
};
var rpcUpgradeCard = function (ctx, logger, nk, payload) {
    var request = JSON.parse(payload);
    var userCards = loadUserCards(nk, logger, ctx.userId);
    if (!userCards) {
        logger.error('user %s card collection not found', ctx.userId);
        throw Error('Internal server error');
    }
    var card = userCards.deckCards[request.id];
    if (card) {
        card.level += 1;
        userCards.deckCards[request.id] = card;
    }
    card = userCards.storedCards[request.id];
    if (card) {
        card.level += 1;
        userCards.storedCards[request.id] = card;
    }
    if (!card) {
        logger.error('invalid card');
        throw Error('invalid card');
    }
    try {
        storeUserCards(nk, logger, ctx.userId, userCards);
    }
    catch (error) {
        throw Error('Internal server error');
    }
    logger.debug('user %s card %s upgraded', ctx.userId, JSON.stringify(card));
    return JSON.stringify(card);
};
var rpcResetCardCollection = function (ctx, logger, nk, payload) {
    var collection = defaultCardCollection(nk, logger, ctx.userId);
    storeUserCards(nk, logger, ctx.userId, collection);
    logger.debug('user %s card collection has been reset', ctx.userId);
    return JSON.stringify(collection);
};
var rpcLoadUserCards = function (ctx, logger, nk, payload) {
    return JSON.stringify(loadUserCards(nk, logger, ctx.userId));
};
var rpcBuyRandomCard = function (ctx, logger, nk, payload) {
    var _a, _b;
    var type = Math.floor(Math.random() * 4) + 1;
    var userCards;
    try {
        userCards = loadUserCards(nk, logger, ctx.userId);
    }
    catch (error) {
        logger.error('error loading user cards: %s', error.message);
        throw Error('Internal server error');
    }
    var cardId = nk.uuidv4();
    var newCard = {
        type: type,
        level: 1,
    };
    userCards.storedCards[cardId] = newCard;
    try {
        nk.walletUpdate(ctx.userId, (_a = {}, _a[currencyKeyName] = -100, _a));
        storeUserCards(nk, logger, ctx.userId, userCards);
    }
    catch (error) {
        logger.error('error buying card: %s', error.message);
        throw error;
    }
    logger.debug('user %s successfully bought a new card', ctx.userId);
    return JSON.stringify((_b = {}, _b[cardId] = newCard, _b));
};
function loadUserCards(nk, logger, userId) {
    var storageReadReq = {
        key: DeckCollectionKey,
        collection: DeckCollectionName,
        userId: userId,
    };
    var objects;
    try {
        objects = nk.storageRead([storageReadReq]);
    }
    catch (error) {
        logger.error('storageRead error: %s', error.message);
        throw error;
    }
    if (objects.length === 0) {
        throw Error('user cards storage object not found');
    }
    var storedCardCollection = objects[0].value;
    return storedCardCollection;
}
function storeUserCards(nk, logger, userId, cards) {
    try {
        nk.storageWrite([
            {
                key: DeckCollectionKey,
                collection: DeckCollectionName,
                userId: userId,
                value: cards,
                permissionRead: DeckPermissionRead,
                permissionWrite: DeckPermissionWrite,
            }
        ]);
    }
    catch (error) {
        logger.error('storageWrite error: %s', error.message);
        throw error;
    }
}
function getRandomInt(min, max) {
    return min + Math.floor(Math.random() * Math.floor(max));
}
function defaultCardCollection(nk, logger, userId) {
    var deck = {};
    DefaultDeckCards.forEach(function (c) {
        deck[nk.uuidv4()] = c;
    });
    var stored = {};
    DefaultStoredCards.forEach(function (c) {
        stored[nk.uuidv4()] = c;
    });
    var cards = {
        deckCards: deck,
        storedCards: stored,
    };
    storeUserCards(nk, logger, userId, cards);
    return {
        deckCards: deck,
        storedCards: stored,
    };
}
var currencyKeyName = 'gems';
var rpcAddUserGems = function (ctx, logger, nk) {
    var walletUpdateResult = updateWallet(nk, ctx.userId, 100, {});
    var updateString = JSON.stringify(walletUpdateResult);
    logger.debug('Added 100 gems to user %s wallet: %s', ctx.userId, updateString);
    return updateString;
};
function updateWallet(nk, userId, amount, metadata) {
    var _a;
    var changeset = (_a = {},
        _a[currencyKeyName] = amount,
        _a);
    var result = nk.walletUpdate(userId, changeset, metadata, true);
    return result;
}
var dummyUserDeviceId = 'B1DA5988-FC6F-4B6F-8EA9-217DEEC3CDB6';
var dummyUserDeviceUsername = 'SuperPirate';
var globalLeaderboard = 'global';
var leaderboardIds = [
    globalLeaderboard,
];
var InitModule = function (ctx, logger, nk, initializer) {
    nk.authenticateDevice(dummyUserDeviceId, dummyUserDeviceUsername, true);
    var authoritative = false;
    var metadata = {};
    var scoreOperator = "best";
    var sortOrder = "desc";
    var resetSchedule = null;
    leaderboardIds.forEach(function (id) {
        nk.leaderboardCreate(id, authoritative, sortOrder, scoreOperator, resetSchedule, metadata);
        logger.info('leaderboard %q created', id);
    });
    initializer.registerAfterAuthenticateDevice(afterAuthenticateDeviceFn);
    initializer.registerAfterAuthenticateFacebook(afterAuthenticateFacebookFn);
    initializer.registerAfterJoinGroup(afterJoinGroupFn);
    initializer.registerAfterKickGroupUsers(afterKickGroupUsersFn);
    initializer.registerAfterLeaveGroup(afterLeaveGroupFn);
    initializer.registerAfterPromoteGroupUsers(afterPromoteGroupUsersFn);
    initializer.registerAfterAddFriends(afterAddFriendsFn);
    initializer.registerBeforeDeleteGroup(beforeDeleteGroupFn);
    initializer.registerRpc('search_username', rpcSearchUsernameFn);
    initializer.registerRpc('swap_deck_card', rpcSwapDeckCard);
    initializer.registerRpc('upgrade_card', rpcUpgradeCard);
    initializer.registerRpc('reset_card_collection', rpcResetCardCollection);
    initializer.registerRpc('add_user_gems', rpcAddUserGems);
    initializer.registerRpc('load_user_cards', rpcLoadUserCards);
    initializer.registerRpc('add_random_card', rpcBuyRandomCard);
    initializer.registerRpc('handle_match_end', rpcHandleMatchEnd);
    logger.info('Global leaderboard: %#v', nk.leaderboardsGetId(['global', 'c4a9ef93-c8d3-4f5b-bd20-bec2fd65a495']));
    logger.info('-------------------------------------------');
    logger.info('Leaderboards: %s', JSON.stringify(nk.leaderboardList(undefined, undefined, 3, 'Kf+BAwEBFFRvdXJuYW1lbnRMaXN0Q3Vyc29yAf+CAAEBAQJJZAEMAAAAKf+CASRiNjg4YzIyZS01NGQ2LTQ3Y2UtOTc2MC1kNTY3MmFmODI0NjAA')));
    nk.leaderboardRecordWrite('global', '000001e5-de96-4033-af68-71b2d0b656e8', dummyUserDeviceUsername, 500, 5, undefined, 'decr');
    logger.warn('Pirate Panic TypeScript loaded.');
};
var afterAuthenticateDeviceFn = function (ctx, logger, nk, data, req) {
    afterAuthenticate(ctx, logger, nk, data);
};
var afterAuthenticateFacebookFn = function (ctx, logger, nk, data, req) {
    afterAuthenticate(ctx, logger, nk, data);
};
function afterAuthenticate(ctx, logger, nk, data) {
    if (!data.created) {
        return;
    }
    var initialState = {
        'level': Math.floor(Math.random() * 100),
        'wins': Math.floor(Math.random() * 100),
        'gamesPlayed': Math.floor(Math.random() * 200),
    };
    var writeStats = {
        collection: 'stats',
        key: 'public',
        permissionRead: 2,
        permissionWrite: 0,
        value: initialState,
        userId: ctx.userId,
    };
    var writeAddFriendQuest = addFriendQuestInit(ctx.userId);
    var writeCards = {
        collection: DeckCollectionName,
        key: DeckCollectionKey,
        permissionRead: DeckPermissionRead,
        permissionWrite: DeckPermissionWrite,
        value: defaultCardCollection(nk, logger, ctx.userId),
        userId: ctx.userId,
    };
    try {
        nk.storageWrite([writeStats, writeAddFriendQuest, writeCards]);
    }
    catch (error) {
        logger.error('storageWrite error: %q', error);
        throw error;
    }
    logger.debug('new user id: %s account data initialised', ctx.userId);
}
var rpcSearchUsernameFn = function (ctx, logger, nk, payload) {
    var input = JSON.parse(payload);
    var query = "\n    SELECT id, username FROM users WHERE username ILIKE concat($1, '%')\n    ";
    var result = nk.sqlQuery(query, [input.username]);
    return JSON.stringify(result);
};
var QuestsCollectionKey = 'quests';
var AddFriendQuestKey = 'add_friend';
var AddFriendQuestReward = 1000;
var AddFriendQuestNotificationCode = 1;
function addFriendQuestInit(userId) {
    return {
        collection: QuestsCollectionKey,
        key: AddFriendQuestKey,
        permissionRead: 1,
        permissionWrite: 0,
        value: { done: false },
        userId: userId,
    };
}
function getFriendQuest(nk, logger, userId) {
    var storageReadReq = {
        collection: QuestsCollectionKey,
        key: AddFriendQuestKey,
        userId: userId,
    };
    var objects;
    try {
        objects = nk.storageRead([storageReadReq]);
    }
    catch (error) {
        logger.error('storageRead error: %s', error.message);
        throw error;
    }
    if (objects.length === 0) {
        throw Error('user add_friend quest storage object not found');
    }
    return objects[0];
}
var afterAddFriendsFn = function (ctx, logger, nk, data, request) {
    var storedQuest = getFriendQuest(nk, logger, ctx.userId);
    var addFriendQuest = storedQuest.value;
    if (!addFriendQuest.done) {
        var quest = addFriendQuestInit(ctx.userId);
        quest.value.done = true;
        try {
            nk.storageWrite([quest]);
        }
        catch (error) {
            logger.error('storageWrite error: %q', error);
            throw error;
        }
        var subject = JSON.stringify('A new friend!');
        var content = { reward: AddFriendQuestReward };
        var code = AddFriendQuestNotificationCode;
        var senderId = null;
        var persistent = true;
        nk.notificationSend(ctx.userId, subject, content, code, senderId, persistent);
        logger.info('user %s completed add_friend quest!', ctx.userId);
    }
};
var winnerBonus = 10;
var towerDestroyedMultiplier = 5;
var speedBonus = 5;
var winReward = 180;
var loseReward = 110;
function calculateScore(isWinner, towersDestroyed, matchDuration) {
    var score = isWinner ? winnerBonus : 0;
    score += towersDestroyed * towerDestroyedMultiplier;
    var durationMin = Math.floor(matchDuration / 60);
    var timeScore = 0;
    if (isWinner) {
        timeScore = Math.max(1, speedBonus - durationMin);
    }
    else {
        timeScore = Math.max(1, Math.min(durationMin, speedBonus));
    }
    score += timeScore;
    return Math.round(score);
}
function rpcGetMatchScore(ctx, logger, nk, payload) {
    var matchId = JSON.parse(payload)['match_id'];
    if (!matchId) {
        throw Error('missing match_id from payload');
    }
    var items = nk.walletLedgerList(ctx.userId, 100);
    while (items.cursor) {
        items = nk.walletLedgerList(ctx.userId, 100, items.cursor);
    }
    var lastMatchReward = {};
    for (var _i = 0, _a = items.items; _i < _a.length; _i++) {
        var update = _a[_i];
        if (update.metadata.source === 'match_reward'
            && update.metadata.match_id === matchId) {
            lastMatchReward = update;
        }
    }
    return JSON.stringify(lastMatchReward);
}
var MatchEndPlacement;
(function (MatchEndPlacement) {
    MatchEndPlacement[MatchEndPlacement["Loser"] = 0] = "Loser";
    MatchEndPlacement[MatchEndPlacement["Winner"] = 1] = "Winner";
})(MatchEndPlacement || (MatchEndPlacement = {}));
var rpcHandleMatchEnd = function (ctx, logger, nk, payload) {
    if (!payload) {
        throw Error('no data found in rpc payload');
    }
    var request = JSON.parse(payload);
    var score = calculateScore(request.placement == MatchEndPlacement.Winner, request.towersDestroyed, request.time);
    var metadata = {
        source: 'match_reward',
        match_id: request.matchId,
    };
    updateWallet(nk, ctx.userId, score, metadata);
    nk.leaderboardRecordWrite(globalLeaderboard, ctx.userId, ctx.username, score);
    var response = {
        gems: request.placement == MatchEndPlacement.Winner ? winReward : loseReward,
        score: score
    };
    logger.debug('match %s ended', ctx.matchId);
    return JSON.stringify(response);
};
+1 −1
Original line number Diff line number Diff line
@@ -295,7 +295,7 @@ func LeaderboardRecordsList(ctx context.Context, logger *zap.Logger, db *sql.DB,
		}
	}

	if len(ownerIds) != 0 {
	if ownerIds != nil && len(ownerIds) != 0 {
		params := make([]interface{}, 0, len(ownerIds)+2)
		params = append(params, leaderboardId, time.Unix(expiryTime, 0).UTC())
		statements := make([]string, len(ownerIds))
+54 −160

File changed.

Preview size limit exceeded, changes collapsed.

+44 −132
Original line number Diff line number Diff line
@@ -5676,35 +5676,11 @@ func (n *RuntimeLuaNakamaModule) leaderboardRecordWrite(l *lua.LState) int {
		return 0
	}

	recordTable := l.CreateTable(0, 11)
	recordTable.RawSetString("leaderboard_id", lua.LString(record.LeaderboardId))
	recordTable.RawSetString("owner_id", lua.LString(record.OwnerId))
	if record.Username != nil {
		recordTable.RawSetString("username", lua.LString(record.Username.Value))
	} else {
		recordTable.RawSetString("username", lua.LNil)
	}
	recordTable.RawSetString("score", lua.LNumber(record.Score))
	recordTable.RawSetString("subscore", lua.LNumber(record.Subscore))
	recordTable.RawSetString("num_score", lua.LNumber(record.NumScore))

	metadataMap := make(map[string]interface{})
	err = json.Unmarshal([]byte(record.Metadata), &metadataMap)
	recordTable, err := recordToLuaTable(l, record)
	if err != nil {
		l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error()))
		l.RaiseError(err.Error())
		return 0
	}
	metadataTable := RuntimeLuaConvertMap(l, metadataMap)
	recordTable.RawSetString("metadata", metadataTable)

	recordTable.RawSetString("create_time", lua.LNumber(record.CreateTime.Seconds))
	recordTable.RawSetString("update_time", lua.LNumber(record.UpdateTime.Seconds))
	if record.ExpiryTime != nil {
		recordTable.RawSetString("expiry_time", lua.LNumber(record.ExpiryTime.Seconds))
	} else {
		recordTable.RawSetString("expiry_time", lua.LNil)
	}
	recordTable.RawSetString("rank", lua.LNumber(record.Rank))

	l.Push(recordTable)
	return 1
@@ -6296,42 +6272,43 @@ func (n *RuntimeLuaNakamaModule) tournamentRecordsList(l *lua.LState) int {
func leaderboardRecordsToLua(l *lua.LState, records []*api.LeaderboardRecord, ownerRecords []*api.LeaderboardRecord, prevCursor, nextCursor string) int {
	recordsTable := l.CreateTable(len(records), 0)
	for i, record := range records {
		recordTable := l.CreateTable(0, 11)
		recordTable.RawSetString("leaderboard_id", lua.LString(record.LeaderboardId))
		recordTable.RawSetString("owner_id", lua.LString(record.OwnerId))
		if record.Username != nil {
			recordTable.RawSetString("username", lua.LString(record.Username.Value))
		} else {
			recordTable.RawSetString("username", lua.LNil)
		recordTable, err := recordToLuaTable(l, record)
		if err != nil {
			l.RaiseError(err.Error())
			return 0
		}
		recordTable.RawSetString("score", lua.LNumber(record.Score))
		recordTable.RawSetString("subscore", lua.LNumber(record.Subscore))
		recordTable.RawSetString("num_score", lua.LNumber(record.NumScore))

		metadataMap := make(map[string]interface{})
		err := json.Unmarshal([]byte(record.Metadata), &metadataMap)
		recordsTable.RawSetInt(i+1, recordTable)
	}

	ownerRecordsTable := l.CreateTable(len(ownerRecords), 0)
	for i, record := range ownerRecords {
		recordTable, err := recordToLuaTable(l, record)
		if err != nil {
			l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error()))
			l.RaiseError(err.Error())
			return 0
		}
		metadataTable := RuntimeLuaConvertMap(l, metadataMap)
		recordTable.RawSetString("metadata", metadataTable)

		recordTable.RawSetString("create_time", lua.LNumber(record.CreateTime.Seconds))
		recordTable.RawSetString("update_time", lua.LNumber(record.UpdateTime.Seconds))
		if record.ExpiryTime != nil {
			recordTable.RawSetString("expiry_time", lua.LNumber(record.ExpiryTime.Seconds))
		} else {
			recordTable.RawSetString("expiry_time", lua.LNil)
		ownerRecordsTable.RawSetInt(i+1, recordTable)
	}

		recordTable.RawSetString("rank", lua.LNumber(record.Rank))
	l.Push(recordsTable)
	l.Push(ownerRecordsTable)
	if nextCursor != "" {
		l.Push(lua.LString(nextCursor))
	} else {
		l.Push(lua.LNil)
	}
	if prevCursor != "" {
		l.Push(lua.LString(prevCursor))
	} else {
		l.Push(lua.LNil)
	}

		recordsTable.RawSetInt(i+1, recordTable)
	return 4
}

	ownerRecordsTable := l.CreateTable(len(ownerRecords), 0)
	for i, record := range ownerRecords {
func recordToLuaTable(l *lua.LState, record *api.LeaderboardRecord) (*lua.LTable, error) {
	recordTable := l.CreateTable(0, 11)
	recordTable.RawSetString("leaderboard_id", lua.LString(record.LeaderboardId))
	recordTable.RawSetString("owner_id", lua.LString(record.OwnerId))
@@ -6347,8 +6324,7 @@ func leaderboardRecordsToLua(l *lua.LState, records []*api.LeaderboardRecord, ow
	metadataMap := make(map[string]interface{})
	err := json.Unmarshal([]byte(record.Metadata), &metadataMap)
	if err != nil {
			l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error()))
			return 0
		return nil, fmt.Errorf("failed to convert metadata to json: %s", err.Error())
	}
	metadataTable := RuntimeLuaConvertMap(l, metadataMap)
	recordTable.RawSetString("metadata", metadataTable)
@@ -6363,23 +6339,7 @@ func leaderboardRecordsToLua(l *lua.LState, records []*api.LeaderboardRecord, ow

	recordTable.RawSetString("rank", lua.LNumber(record.Rank))

		ownerRecordsTable.RawSetInt(i+1, recordTable)
	}

	l.Push(recordsTable)
	l.Push(ownerRecordsTable)
	if nextCursor != "" {
		l.Push(lua.LString(nextCursor))
	} else {
		l.Push(lua.LNil)
	}
	if prevCursor != "" {
		l.Push(lua.LString(prevCursor))
	} else {
		l.Push(lua.LNil)
	}

	return 4
	return recordTable, nil
}

func (n *RuntimeLuaNakamaModule) tournamentList(l *lua.LState) int {
@@ -6504,34 +6464,11 @@ func (n *RuntimeLuaNakamaModule) tournamentRecordWrite(l *lua.LState) int {
		return 0
	}

	recordTable := l.CreateTable(0, 10)
	recordTable.RawSetString("leaderboard_id", lua.LString(record.LeaderboardId))
	recordTable.RawSetString("owner_id", lua.LString(record.OwnerId))
	if record.Username != nil {
		recordTable.RawSetString("username", lua.LString(record.Username.Value))
	} else {
		recordTable.RawSetString("username", lua.LNil)
	}
	recordTable.RawSetString("score", lua.LNumber(record.Score))
	recordTable.RawSetString("subscore", lua.LNumber(record.Subscore))
	recordTable.RawSetString("num_score", lua.LNumber(record.NumScore))

	metadataMap := make(map[string]interface{})
	err = json.Unmarshal([]byte(record.Metadata), &metadataMap)
	recordTable, err := recordToLuaTable(l, record)
	if err != nil {
		l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error()))
		l.RaiseError(err.Error())
		return 0
	}
	metadataTable := RuntimeLuaConvertMap(l, metadataMap)
	recordTable.RawSetString("metadata", metadataTable)

	recordTable.RawSetString("create_time", lua.LNumber(record.CreateTime.Seconds))
	recordTable.RawSetString("update_time", lua.LNumber(record.UpdateTime.Seconds))
	if record.ExpiryTime != nil {
		recordTable.RawSetString("expiry_time", lua.LNumber(record.ExpiryTime.Seconds))
	} else {
		recordTable.RawSetString("expiry_time", lua.LNil)
	}

	l.Push(recordTable)
	return 1
@@ -6570,36 +6507,11 @@ func (n *RuntimeLuaNakamaModule) tournamentRecordsHaystack(l *lua.LState) int {

	recordsTable := l.CreateTable(len(records), 0)
	for i, record := range records {
		recordTable := l.CreateTable(0, 11)

		recordTable.RawSetString("leaderboard_id", lua.LString(record.LeaderboardId))
		recordTable.RawSetString("owner_id", lua.LString(record.OwnerId))
		if record.Username != nil {
			recordTable.RawSetString("username", lua.LString(record.Username.Value))
		} else {
			recordTable.RawSetString("username", lua.LNil)
		}
		recordTable.RawSetString("score", lua.LNumber(record.Score))
		recordTable.RawSetString("subscore", lua.LNumber(record.Subscore))
		recordTable.RawSetString("num_score", lua.LNumber(record.NumScore))

		metadataMap := make(map[string]interface{})
		err = json.Unmarshal([]byte(record.Metadata), &metadataMap)
		recordTable, err := recordToLuaTable(l, record)
		if err != nil {
			l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error()))
			l.RaiseError(err.Error())
			return 0
		}
		metadataTable := RuntimeLuaConvertMap(l, metadataMap)
		recordTable.RawSetString("metadata", metadataTable)

		recordTable.RawSetString("create_time", lua.LNumber(record.CreateTime.Seconds))
		recordTable.RawSetString("update_time", lua.LNumber(record.UpdateTime.Seconds))
		if record.ExpiryTime != nil {
			recordTable.RawSetString("expiry_time", lua.LNumber(record.ExpiryTime.Seconds))
		} else {
			recordTable.RawSetString("expiry_time", lua.LNil)
		}
		recordTable.RawSetString("rank", lua.LNumber(record.Rank))

		recordsTable.RawSetInt(i+1, recordTable)
	}