Skip to content

Commit

Permalink
Add runtime leaderboardListCursorFromRank function
Browse files Browse the repository at this point in the history
  • Loading branch information
sesposito committed Oct 19, 2023
1 parent bd3712c commit 7fdede6
Show file tree
Hide file tree
Showing 8 changed files with 435 additions and 204 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,5 @@ require (
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
)

replace github.com/heroiclabs/nakama-common => ../nakama-common
40 changes: 39 additions & 1 deletion server/leaderboard_rank_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package server
import (
"context"
"database/sql"
"errors"
"fmt"
"runtime"
"sync"
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
54 changes: 54 additions & 0 deletions server/runtime_go_nakama.go
Original file line number Diff line number Diff line change
Expand Up @@ -2361,6 +2361,60 @@ 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 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.
Expand Down
Loading

0 comments on commit 7fdede6

Please sign in to comment.