Loading CHANGELOG.md +1 −0 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ The format is based on [keep a changelog](http://keepachangelog.com/) and this p - Add script runtime function to update groups. - Add script runtime function to list groups a user is part of. - Add script runtime function to list users belonging to a group. - Add script runtime function to submit leaderboard record. - Send in-app notification on friend request. - Send in-app notification on friend request accept. - Send in-app notification when a Facebook friend signs into the game for the first time. Loading server/core_leaderboard.go +143 −1 Original line number Diff line number Diff line Loading @@ -25,7 +25,7 @@ import ( "go.uber.org/zap" ) func createLeaderboard(logger *zap.Logger, db *sql.DB, id, sortOrder, resetSchedule, metadata string, authoritative bool) ([]byte, error) { func leaderboardCreate(logger *zap.Logger, db *sql.DB, id, sortOrder, resetSchedule, metadata string, authoritative bool) ([]byte, error) { query := `INSERT INTO leaderboard (id, authoritative, sort_order, reset_schedule, metadata) VALUES ($1, $2, $3, $4, $5)` params := []interface{}{} Loading Loading @@ -85,3 +85,145 @@ func createLeaderboard(logger *zap.Logger, db *sql.DB, id, sortOrder, resetSched return params[0].([]byte), nil } func leaderboardSubmit(logger *zap.Logger, db *sql.DB, caller uuid.UUID, leaderboardID []byte, ownerID uuid.UUID, handle string, lang string, op string, value int64, location string, timezone string, metadata []byte) (*LeaderboardRecord, error) { var authoritative bool var sortOrder int64 var resetSchedule sql.NullString query := "SELECT authoritative, sort_order, reset_schedule FROM leaderboard WHERE id = $1" logger.Debug("Leaderboard lookup", zap.String("query", query), zap.Any("leaderboard_id", leaderboardID)) err := db.QueryRow(query, leaderboardID). Scan(&authoritative, &sortOrder, &resetSchedule) if err != nil { logger.Error("Could not execute leaderboard record write metadata query", zap.Error(err)) return nil, errors.New("Error writing leaderboard record") } now := now() updatedAt := timeToMs(now) expiresAt := int64(0) if resetSchedule.Valid { expr, err := cronexpr.Parse(resetSchedule.String) if err != nil { logger.Error("Could not parse leaderboard reset schedule query", zap.Error(err)) return nil, errors.New("Error writing leaderboard record") } expiresAt = timeToMs(expr.Next(now)) } if authoritative == true && caller != uuid.Nil { return nil, errors.New("Cannot submit to authoritative leaderboard") } var scoreOpSql string var scoreDelta int64 var scoreAbs int64 switch op { case "incr": scoreOpSql = "score = leaderboard_record.score + $17::BIGINT" scoreDelta = value scoreAbs = value case "decr": scoreOpSql = "score = leaderboard_record.score - $17::BIGINT" scoreDelta = value scoreAbs = 0 - value case "set": scoreOpSql = "score = $17::BIGINT" scoreDelta = value scoreAbs = value case "best": if sortOrder == 0 { // Lower score is better. scoreOpSql = "score = ((leaderboard_record.score + $17::BIGINT - abs(leaderboard_record.score - $17::BIGINT)) / 2)::BIGINT" } else { // Higher score is better. scoreOpSql = "score = ((leaderboard_record.score + $17::BIGINT + abs(leaderboard_record.score - $17::BIGINT)) / 2)::BIGINT" } scoreDelta = value scoreAbs = value default: return nil, errors.New("Unknown leaderboard record write operator") } params := []interface{}{uuid.NewV4().Bytes(), leaderboardID, ownerID.Bytes(), handle, lang} if location != "" { params = append(params, location) } else { params = append(params, nil) } if timezone != "" { params = append(params, timezone) } else { params = append(params, nil) } params = append(params, 0, scoreAbs, 1) if len(metadata) != 0 { params = append(params, metadata) } else { params = append(params, nil) } params = append(params, 0, updatedAt, invertMs(updatedAt), expiresAt, 0, scoreDelta) query = `INSERT INTO leaderboard_record (id, leaderboard_id, owner_id, handle, lang, location, timezone, rank_value, score, num_score, metadata, ranked_at, updated_at, updated_at_inverse, expires_at, banned_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, COALESCE($11, '{}'), $12, $13, $14, $15, $16) ON CONFLICT (leaderboard_id, expires_at, owner_id) DO UPDATE SET handle = $4, lang = $5, location = COALESCE($6, leaderboard_record.location), timezone = COALESCE($7, leaderboard_record.timezone), ` + scoreOpSql + `, num_score = leaderboard_record.num_score + 1, metadata = COALESCE($11, leaderboard_record.metadata), updated_at = $13` logger.Debug("Leaderboard record write", zap.String("query", query)) res, err := db.Exec(query, params...) if err != nil { logger.Error("Could not execute leaderboard record write query", zap.Error(err)) return nil, errors.New("Error writing leaderboard record") } if rowsAffected, _ := res.RowsAffected(); rowsAffected == 0 { logger.Error("Unexpected row count from leaderboard record write query") return nil, errors.New("Error writing leaderboard record") } record, err := leaderboardQueryRecords(logger, db, leaderboardID, ownerID, handle, lang, expiresAt, updatedAt) if err != nil { return nil, errors.New("Error writing leaderboard record") } return record, nil } func leaderboardQueryRecords(logger *zap.Logger, db *sql.DB, leaderboardID []byte, ownerID uuid.UUID, handle string, lang string, expiresAt int64, updatedAt int64) (*LeaderboardRecord, error) { var location sql.NullString var timezone sql.NullString var rankValue int64 var score int64 var numScore int64 var metadata []byte var rankedAt int64 var bannedAt int64 query := `SELECT location, timezone, rank_value, score, num_score, metadata, ranked_at, banned_at FROM leaderboard_record WHERE leaderboard_id = $1 AND expires_at = $2 AND owner_id = $3` logger.Debug("Leaderboard record read", zap.String("query", query)) err := db.QueryRow(query, leaderboardID, expiresAt, ownerID.Bytes()). Scan(&location, &timezone, &rankValue, &score, &numScore, &metadata, &rankedAt, &bannedAt) if err != nil { logger.Error("Could not execute leaderboard record read query", zap.Error(err)) return nil, err } return &LeaderboardRecord{ LeaderboardId: leaderboardID, OwnerId: ownerID.Bytes(), Handle: handle, Lang: lang, Location: location.String, Timezone: timezone.String, Rank: rankValue, Score: score, NumScore: numScore, Metadata: metadata, RankedAt: rankedAt, UpdatedAt: updatedAt, ExpiresAt: expiresAt, }, nil } server/runtime_nakama_module.go +90 −32 Original line number Diff line number Diff line Loading @@ -33,11 +33,12 @@ import ( "encoding/hex" "io/ioutil" "nakama/pkg/jsonpatch" "github.com/fatih/structs" "github.com/satori/go.uuid" "github.com/yuin/gopher-lua" "go.uber.org/zap" "nakama/pkg/jsonpatch" ) const CALLBACKS = "runtime_callbacks" Loading Loading @@ -100,6 +101,10 @@ func (n *NakamaModule) Loader(l *lua.LState) int { "storage_update": n.storageUpdate, "storage_remove": n.storageRemove, "leaderboard_create": n.leaderboardCreate, "leaderboard_submit_incr": n.leaderboardSubmitIncr, "leaderboard_submit_decr": n.leaderboardSubmitDecr, "leaderboard_submit_set": n.leaderboardSubmitSet, "leaderboard_submit_best": n.leaderboardSubmitBest, "groups_create": n.groupsCreate, "groups_update": n.groupsUpdate, "group_users_list": n.groupUsersList, Loading Loading @@ -1232,7 +1237,7 @@ func (n *NakamaModule) leaderboardCreate(l *lua.LState) int { return 0 } _, err = createLeaderboard(n.logger, n.db, leaderboardId.String(), sort, reset, string(metadataBytes), authoritative) _, err = leaderboardCreate(n.logger, n.db, leaderboardId.String(), sort, reset, string(metadataBytes), authoritative) if err != nil { l.RaiseError(fmt.Sprintf("failed to create leaderboard: %s", err.Error())) return 0 Loading @@ -1241,6 +1246,59 @@ func (n *NakamaModule) leaderboardCreate(l *lua.LState) int { return 0 } func (n *NakamaModule) leaderboardSubmitIncr(l *lua.LState) int { return n.leaderboardSubmit(l, "incr") } func (n *NakamaModule) leaderboardSubmitDecr(l *lua.LState) int { return n.leaderboardSubmit(l, "decr") } func (n *NakamaModule) leaderboardSubmitSet(l *lua.LState) int { return n.leaderboardSubmit(l, "set") } func (n *NakamaModule) leaderboardSubmitBest(l *lua.LState) int { return n.leaderboardSubmit(l, "best") } func (n *NakamaModule) leaderboardSubmit(l *lua.LState, op string) int { leaderboardID := l.CheckString(1) value := l.CheckInt64(2) oId := l.CheckString(3) handle := l.OptString(4, "") lang := l.OptString(5, "") location := l.OptString(6, "") timezone := l.OptString(7, "") metadata := l.OptTable(8, l.NewTable()) ownerID, err := uuid.FromString(oId) if err != nil { l.ArgError(1, "invalid owner id") return 0 } metadataMap := ConvertLuaTable(metadata) metadataBytes, err := json.Marshal(metadataMap) if err != nil { l.RaiseError(fmt.Sprintf("failed to convert metadata: %s", err.Error())) return 0 } record, err := leaderboardSubmit(n.logger, n.db, uuid.Nil, []byte(leaderboardID), ownerID, handle, lang, op, value, location, timezone, metadataBytes) if err != nil { l.RaiseError(fmt.Sprintf("failed to create leaderboard: %s", err.Error())) return 0 } oid, _ := uuid.FromBytes(record.OwnerId) record.OwnerId = []byte(oid.String()) rm := structs.Map(record) lv := ConvertMap(l, rm) l.Push(lv) return 1 } func (n *NakamaModule) groupsCreate(l *lua.LState) int { groupsTable := l.CheckTable(1) if groupsTable == nil || groupsTable.Len() == 0 { Loading tests/modules/e2e_runtime.lua +18 −9 Original line number Diff line number Diff line Loading @@ -60,6 +60,15 @@ do -- nk.leaderboard_create(id, "desc", "0 0 * * 1", {}, false) end -- leaderboard_create do local status, res = pcall(nk.leaderboard_submit_set, "ce042d38-c3db-4ebd-bc99-3aaa0adbdef7", 10, "4c2ae592-b2a7-445e-98ec-697694478b1c", "02ebb2c8") if not status then print(res) end assert(status == true) end -- logger_info do local message = nk.logger_info(("%q"):format("INFO logger.")) Loading Loading
CHANGELOG.md +1 −0 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ The format is based on [keep a changelog](http://keepachangelog.com/) and this p - Add script runtime function to update groups. - Add script runtime function to list groups a user is part of. - Add script runtime function to list users belonging to a group. - Add script runtime function to submit leaderboard record. - Send in-app notification on friend request. - Send in-app notification on friend request accept. - Send in-app notification when a Facebook friend signs into the game for the first time. Loading
server/core_leaderboard.go +143 −1 Original line number Diff line number Diff line Loading @@ -25,7 +25,7 @@ import ( "go.uber.org/zap" ) func createLeaderboard(logger *zap.Logger, db *sql.DB, id, sortOrder, resetSchedule, metadata string, authoritative bool) ([]byte, error) { func leaderboardCreate(logger *zap.Logger, db *sql.DB, id, sortOrder, resetSchedule, metadata string, authoritative bool) ([]byte, error) { query := `INSERT INTO leaderboard (id, authoritative, sort_order, reset_schedule, metadata) VALUES ($1, $2, $3, $4, $5)` params := []interface{}{} Loading Loading @@ -85,3 +85,145 @@ func createLeaderboard(logger *zap.Logger, db *sql.DB, id, sortOrder, resetSched return params[0].([]byte), nil } func leaderboardSubmit(logger *zap.Logger, db *sql.DB, caller uuid.UUID, leaderboardID []byte, ownerID uuid.UUID, handle string, lang string, op string, value int64, location string, timezone string, metadata []byte) (*LeaderboardRecord, error) { var authoritative bool var sortOrder int64 var resetSchedule sql.NullString query := "SELECT authoritative, sort_order, reset_schedule FROM leaderboard WHERE id = $1" logger.Debug("Leaderboard lookup", zap.String("query", query), zap.Any("leaderboard_id", leaderboardID)) err := db.QueryRow(query, leaderboardID). Scan(&authoritative, &sortOrder, &resetSchedule) if err != nil { logger.Error("Could not execute leaderboard record write metadata query", zap.Error(err)) return nil, errors.New("Error writing leaderboard record") } now := now() updatedAt := timeToMs(now) expiresAt := int64(0) if resetSchedule.Valid { expr, err := cronexpr.Parse(resetSchedule.String) if err != nil { logger.Error("Could not parse leaderboard reset schedule query", zap.Error(err)) return nil, errors.New("Error writing leaderboard record") } expiresAt = timeToMs(expr.Next(now)) } if authoritative == true && caller != uuid.Nil { return nil, errors.New("Cannot submit to authoritative leaderboard") } var scoreOpSql string var scoreDelta int64 var scoreAbs int64 switch op { case "incr": scoreOpSql = "score = leaderboard_record.score + $17::BIGINT" scoreDelta = value scoreAbs = value case "decr": scoreOpSql = "score = leaderboard_record.score - $17::BIGINT" scoreDelta = value scoreAbs = 0 - value case "set": scoreOpSql = "score = $17::BIGINT" scoreDelta = value scoreAbs = value case "best": if sortOrder == 0 { // Lower score is better. scoreOpSql = "score = ((leaderboard_record.score + $17::BIGINT - abs(leaderboard_record.score - $17::BIGINT)) / 2)::BIGINT" } else { // Higher score is better. scoreOpSql = "score = ((leaderboard_record.score + $17::BIGINT + abs(leaderboard_record.score - $17::BIGINT)) / 2)::BIGINT" } scoreDelta = value scoreAbs = value default: return nil, errors.New("Unknown leaderboard record write operator") } params := []interface{}{uuid.NewV4().Bytes(), leaderboardID, ownerID.Bytes(), handle, lang} if location != "" { params = append(params, location) } else { params = append(params, nil) } if timezone != "" { params = append(params, timezone) } else { params = append(params, nil) } params = append(params, 0, scoreAbs, 1) if len(metadata) != 0 { params = append(params, metadata) } else { params = append(params, nil) } params = append(params, 0, updatedAt, invertMs(updatedAt), expiresAt, 0, scoreDelta) query = `INSERT INTO leaderboard_record (id, leaderboard_id, owner_id, handle, lang, location, timezone, rank_value, score, num_score, metadata, ranked_at, updated_at, updated_at_inverse, expires_at, banned_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, COALESCE($11, '{}'), $12, $13, $14, $15, $16) ON CONFLICT (leaderboard_id, expires_at, owner_id) DO UPDATE SET handle = $4, lang = $5, location = COALESCE($6, leaderboard_record.location), timezone = COALESCE($7, leaderboard_record.timezone), ` + scoreOpSql + `, num_score = leaderboard_record.num_score + 1, metadata = COALESCE($11, leaderboard_record.metadata), updated_at = $13` logger.Debug("Leaderboard record write", zap.String("query", query)) res, err := db.Exec(query, params...) if err != nil { logger.Error("Could not execute leaderboard record write query", zap.Error(err)) return nil, errors.New("Error writing leaderboard record") } if rowsAffected, _ := res.RowsAffected(); rowsAffected == 0 { logger.Error("Unexpected row count from leaderboard record write query") return nil, errors.New("Error writing leaderboard record") } record, err := leaderboardQueryRecords(logger, db, leaderboardID, ownerID, handle, lang, expiresAt, updatedAt) if err != nil { return nil, errors.New("Error writing leaderboard record") } return record, nil } func leaderboardQueryRecords(logger *zap.Logger, db *sql.DB, leaderboardID []byte, ownerID uuid.UUID, handle string, lang string, expiresAt int64, updatedAt int64) (*LeaderboardRecord, error) { var location sql.NullString var timezone sql.NullString var rankValue int64 var score int64 var numScore int64 var metadata []byte var rankedAt int64 var bannedAt int64 query := `SELECT location, timezone, rank_value, score, num_score, metadata, ranked_at, banned_at FROM leaderboard_record WHERE leaderboard_id = $1 AND expires_at = $2 AND owner_id = $3` logger.Debug("Leaderboard record read", zap.String("query", query)) err := db.QueryRow(query, leaderboardID, expiresAt, ownerID.Bytes()). Scan(&location, &timezone, &rankValue, &score, &numScore, &metadata, &rankedAt, &bannedAt) if err != nil { logger.Error("Could not execute leaderboard record read query", zap.Error(err)) return nil, err } return &LeaderboardRecord{ LeaderboardId: leaderboardID, OwnerId: ownerID.Bytes(), Handle: handle, Lang: lang, Location: location.String, Timezone: timezone.String, Rank: rankValue, Score: score, NumScore: numScore, Metadata: metadata, RankedAt: rankedAt, UpdatedAt: updatedAt, ExpiresAt: expiresAt, }, nil }
server/runtime_nakama_module.go +90 −32 Original line number Diff line number Diff line Loading @@ -33,11 +33,12 @@ import ( "encoding/hex" "io/ioutil" "nakama/pkg/jsonpatch" "github.com/fatih/structs" "github.com/satori/go.uuid" "github.com/yuin/gopher-lua" "go.uber.org/zap" "nakama/pkg/jsonpatch" ) const CALLBACKS = "runtime_callbacks" Loading Loading @@ -100,6 +101,10 @@ func (n *NakamaModule) Loader(l *lua.LState) int { "storage_update": n.storageUpdate, "storage_remove": n.storageRemove, "leaderboard_create": n.leaderboardCreate, "leaderboard_submit_incr": n.leaderboardSubmitIncr, "leaderboard_submit_decr": n.leaderboardSubmitDecr, "leaderboard_submit_set": n.leaderboardSubmitSet, "leaderboard_submit_best": n.leaderboardSubmitBest, "groups_create": n.groupsCreate, "groups_update": n.groupsUpdate, "group_users_list": n.groupUsersList, Loading Loading @@ -1232,7 +1237,7 @@ func (n *NakamaModule) leaderboardCreate(l *lua.LState) int { return 0 } _, err = createLeaderboard(n.logger, n.db, leaderboardId.String(), sort, reset, string(metadataBytes), authoritative) _, err = leaderboardCreate(n.logger, n.db, leaderboardId.String(), sort, reset, string(metadataBytes), authoritative) if err != nil { l.RaiseError(fmt.Sprintf("failed to create leaderboard: %s", err.Error())) return 0 Loading @@ -1241,6 +1246,59 @@ func (n *NakamaModule) leaderboardCreate(l *lua.LState) int { return 0 } func (n *NakamaModule) leaderboardSubmitIncr(l *lua.LState) int { return n.leaderboardSubmit(l, "incr") } func (n *NakamaModule) leaderboardSubmitDecr(l *lua.LState) int { return n.leaderboardSubmit(l, "decr") } func (n *NakamaModule) leaderboardSubmitSet(l *lua.LState) int { return n.leaderboardSubmit(l, "set") } func (n *NakamaModule) leaderboardSubmitBest(l *lua.LState) int { return n.leaderboardSubmit(l, "best") } func (n *NakamaModule) leaderboardSubmit(l *lua.LState, op string) int { leaderboardID := l.CheckString(1) value := l.CheckInt64(2) oId := l.CheckString(3) handle := l.OptString(4, "") lang := l.OptString(5, "") location := l.OptString(6, "") timezone := l.OptString(7, "") metadata := l.OptTable(8, l.NewTable()) ownerID, err := uuid.FromString(oId) if err != nil { l.ArgError(1, "invalid owner id") return 0 } metadataMap := ConvertLuaTable(metadata) metadataBytes, err := json.Marshal(metadataMap) if err != nil { l.RaiseError(fmt.Sprintf("failed to convert metadata: %s", err.Error())) return 0 } record, err := leaderboardSubmit(n.logger, n.db, uuid.Nil, []byte(leaderboardID), ownerID, handle, lang, op, value, location, timezone, metadataBytes) if err != nil { l.RaiseError(fmt.Sprintf("failed to create leaderboard: %s", err.Error())) return 0 } oid, _ := uuid.FromBytes(record.OwnerId) record.OwnerId = []byte(oid.String()) rm := structs.Map(record) lv := ConvertMap(l, rm) l.Push(lv) return 1 } func (n *NakamaModule) groupsCreate(l *lua.LState) int { groupsTable := l.CheckTable(1) if groupsTable == nil || groupsTable.Len() == 0 { Loading
tests/modules/e2e_runtime.lua +18 −9 Original line number Diff line number Diff line Loading @@ -60,6 +60,15 @@ do -- nk.leaderboard_create(id, "desc", "0 0 * * 1", {}, false) end -- leaderboard_create do local status, res = pcall(nk.leaderboard_submit_set, "ce042d38-c3db-4ebd-bc99-3aaa0adbdef7", 10, "4c2ae592-b2a7-445e-98ec-697694478b1c", "02ebb2c8") if not status then print(res) end assert(status == true) end -- logger_info do local message = nk.logger_info(("%q"):format("INFO logger.")) Loading