Skip to content

Add runtime leaderboardListCursorFromRank function #1111

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate rank >= 1 too I think?

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