Commit 9c698c1e authored by Mo Firouz's avatar Mo Firouz
Browse files

Add script runtime function to submit leaderboard record. Merged #105

parent d816c630
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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.
+143 −1
Original line number Diff line number Diff line
@@ -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{}{}
@@ -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
}
+90 −32
Original line number Diff line number Diff line
@@ -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"
@@ -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,
@@ -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
@@ -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 {
+18 −9
Original line number Diff line number Diff line
@@ -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."))