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

Add runtime leaderboardListCursorFromRank function (#1111)

parent bd3712c3
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr
### Added
- Allow HTTP key to be read from an HTTP request's Basic auth header if present.
- Add prefix search for storage keys in console (key%).
- Runtime functions to build a leaderboardList cursor to start listing from a given rank.

### Changed
- Use Steam partner API instead of public API for Steam profiles and friends requests.
+1 −1
Original line number Diff line number Diff line
@@ -14,7 +14,7 @@ require (
	github.com/gorilla/mux v1.8.0
	github.com/gorilla/websocket v1.5.0
	github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0
	github.com/heroiclabs/nakama-common v1.28.2-0.20231010150216-b178843845fa
	github.com/heroiclabs/nakama-common v1.28.2-0.20231020105308-855feebe94cb
	github.com/jackc/pgconn v1.14.0
	github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa
	github.com/jackc/pgtype v1.14.0
+2 −2
+39 −1
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ package server
import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"runtime"
	"sync"
@@ -31,6 +32,7 @@ import (

type LeaderboardRankCache interface {
	Get(leaderboardId string, sortOrder int, score, subscore, expiryUnix int64, ownerID uuid.UUID) int64
	GetDataByRank(leaderboardId string, expiryUnix int64, sortOrder int, rank int64) (ownerID uuid.UUID, score, subscore int64, err error)
	Fill(leaderboardId string, sortOrder int, expiryUnix int64, records []*api.LeaderboardRecord) int64
	Insert(leaderboardId string, sortOrder int, score, subscore int64, oldScore, oldSubscore *int64, expiryUnix int64, ownerID uuid.UUID) int64
	Delete(leaderboardId string, sortOrder int, score, subscore, expiryUnix int64, ownerID uuid.UUID) bool
@@ -212,6 +214,43 @@ func (l *LocalLeaderboardRankCache) Get(leaderboardId string, sortOrder int, sco
	return int64(rank)
}

func (l *LocalLeaderboardRankCache) GetDataByRank(leaderboardId string, expiryUnix int64, sortOrder int, rank int64) (ownerID uuid.UUID, score, subscore int64, err error) {
	if l.blacklistAll {
		return uuid.Nil, 0, 0, errors.New("rank cache is disabled")
	}
	if _, ok := l.blacklistIds[leaderboardId]; ok {
		return uuid.Nil, 0, 0, fmt.Errorf("rank cache is disabled for leaderboard: %s", leaderboardId)
	}
	key := LeaderboardWithExpiry{LeaderboardId: leaderboardId, Expiry: expiryUnix}
	l.RLock()
	rankCache, ok := l.cache[key]
	l.RUnlock()
	if !ok {
		return uuid.Nil, 0, 0, fmt.Errorf("rank cache for leaderboard %q with expiry %d not found", leaderboardId, expiryUnix)
	}

	recordData := rankCache.cache.GetElementByRank(int(rank))
	if recordData == nil {
		return uuid.Nil, 0, 0, fmt.Errorf("rank entry %d not found for leaderboard %q with expiry %d", rank, leaderboardId, expiryUnix)
	}

	if sortOrder == LeaderboardSortOrderDescending {
		data, ok := recordData.Value.(RankDesc)
		if !ok {
			return uuid.Nil, 0, 0, fmt.Errorf("failed to type assert rank cache data")
		}

		return data.OwnerId, data.Score, data.Subscore, nil
	} else {
		data, ok := recordData.Value.(RankAsc)
		if !ok {
			return uuid.Nil, 0, 0, fmt.Errorf("failed to type assert rank cache data")
		}

		return data.OwnerId, data.Score, data.Subscore, nil
	}
}

func (l *LocalLeaderboardRankCache) Fill(leaderboardId string, sortOrder int, expiryUnix int64, records []*api.LeaderboardRecord) int64 {
	if l.blacklistAll {
		// If all rank caching is disabled.
@@ -508,7 +547,6 @@ func leaderboardCacheInitWorker(
}

func newRank(sortOrder int, score, subscore int64, ownerID uuid.UUID) skiplist.Interface {

	if sortOrder == LeaderboardSortOrderDescending {
		return RankDesc{
			OwnerId:  ownerID,
+58 −0
Original line number Diff line number Diff line
@@ -2361,6 +2361,64 @@ func (n *RuntimeGoNakamaModule) LeaderboardRecordsList(ctx context.Context, id s
	return list.Records, list.OwnerRecords, list.NextCursor, list.PrevCursor, nil
}

// @group leaderboards
// @summary Build a cursor to be used with leaderboardRecordsList to fetch records starting at a given rank. Only available if rank cache is not disabled for the leaderboard.
// @param leaderboardID(type=string) The unique identifier of the leaderboard.
// @param rank(type=int64) The rank to start listing leaderboard records from.
// @param overrideExpiry(type=int64) Records with expiry in the past are not returned unless within this defined limit. Must be equal or greater than 0.
// @return leaderboardListCursor(string) A string cursor to be used with leaderboardRecordsList.
// @return error(error) An optional error value if an error occurred.
func (n *RuntimeGoNakamaModule) LeaderboardRecordsListCursorFromRank(id string, rank, expiry int64) (string, error) {
	if id == "" {
		return "", errors.New("invalid leaderboard id")
	}

	if rank < 1 {
		return "", errors.New("invalid rank - must be > 1")
	}

	if expiry < 0 {
		return "", errors.New("expects expiry to equal or greater than 0")
	}

	l := n.leaderboardCache.Get(id)
	if l == nil {
		return "", ErrLeaderboardNotFound
	}

	expiryTime, ok := calculateExpiryOverride(expiry, l)
	if !ok {
		return "", errors.New("invalid expiry")
	}

	rank-- // Fetch previous entry to include requested rank in the results
	if rank == 0 {
		return "", nil
	}

	ownerId, score, subscore, err := n.leaderboardRankCache.GetDataByRank(id, expiryTime, l.SortOrder, rank)
	if err != nil {
		return "", fmt.Errorf("failed to get cursor from rank: %s", err.Error())
	}

	cursor := &leaderboardRecordListCursor{
		IsNext:        true,
		LeaderboardId: id,
		ExpiryTime:    expiryTime,
		Score:         score,
		Subscore:      subscore,
		OwnerId:       ownerId.String(),
		Rank:          rank,
	}

	cursorStr, err := marshalLeaderboardRecordsListCursor(cursor)
	if err != nil {
		return "", fmt.Errorf("failed to marshal leaderboard cursor: %s", err.Error())
	}

	return cursorStr, nil
}

// @group leaderboards
// @summary Use the preconfigured operator for the given leaderboard to submit a score for a particular user.
// @param ctx(type=context.Context) The context object represents information about the server and requester.
Loading