Commit f35e8b2a authored by Andrei Mihu's avatar Andrei Mihu
Browse files

Tournament get by ID, allow variable tournament durations.

parent 2ea10f1e
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -4,6 +4,10 @@ All notable changes to this project are documented below.
The format is based on [keep a changelog](http://keepachangelog.com) and this project uses [semantic versioning](http://semver.org).

## [Unreleased]
### Added
- New runtime functions to retrieve tournaments by ID.
- Allow tournament duration to exceed reset window, and cap the duration if it does.

### Changed
- Do not use absolute path for `tini` executable in default container entrypoint.
- Faster validation of JSON object input payloads.
+59 −6
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ import (
	"encoding/base64"
	"encoding/gob"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/gofrs/uuid"
@@ -195,12 +197,47 @@ ON CONFLICT(owner_id, leaderboard_id, expiry_time) DO NOTHING`
	return nil
}

func TournamentsGet(ctx context.Context, logger *zap.Logger, db *sql.DB, tournamentIDs []string) ([]*api.Tournament, error) {
	now := time.Now().UTC()

	params := make([]interface{}, 0, len(tournamentIDs))
	statements := make([]string, 0, len(tournamentIDs))
	for i, tournamentID := range tournamentIDs {
		params = append(params, tournamentID)
		statements = append(statements, fmt.Sprintf("$%v", i+1))
	}
	query := `SELECT id, sort_order, reset_schedule, metadata, create_time, category, description, duration, end_time, max_size, max_num_score, title, size, start_time
FROM leaderboard
WHERE id IN (` + strings.Join(statements, ",") + `) AND duration > 0`

	// Retrieved directly from database to have the latest configuration and 'size' etc field values.
	// Ensures consistency between return data from this call and TournamentList.
	rows, err := db.QueryContext(ctx, query, params...)
	if err != nil {
		logger.Error("Could not retrieve tournaments", zap.Error(err))
		return nil, err
	}

	records := make([]*api.Tournament, 0)
	for rows.Next() {
		tournament, err := parseTournament(rows, now)
		if err != nil {
			_ = rows.Close()
			logger.Error("Error parsing retrieved tournament records", zap.Error(err))
			return nil, err
		}

		records = append(records, tournament)
	}
	_ = rows.Close()

	return records, nil
}

func TournamentList(ctx context.Context, logger *zap.Logger, db *sql.DB, categoryStart, categoryEnd, startTime, endTime, limit int, cursor *tournamentListCursor) (*api.TournamentList, error) {
	now := time.Now().UTC()

	query := `
SELECT 
id, sort_order, reset_schedule, metadata, create_time, category, description, duration, end_time, max_size, max_num_score, title, size, start_time
	query := `SELECT id, sort_order, reset_schedule, metadata, create_time, category, description, duration, end_time, max_size, max_num_score, title, size, start_time
FROM leaderboard
WHERE duration > 0 AND category >= $1 AND category <= $2 AND start_time >= $3`
	params := make([]interface{}, 0, 6)
@@ -497,6 +534,10 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched
		startActiveUnix := schedule0Unix - (schedule1Unix - schedule0Unix)
		endActiveUnix := startActiveUnix + duration
		expiryUnix := schedule0Unix
		if endActiveUnix > expiryUnix {
			// Cap the end active to the same time as the expiry.
			endActiveUnix = expiryUnix
		}

		if startTime > endActiveUnix {
			// The start time after the end of the current active period but before the next reset.
@@ -504,12 +545,20 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched
			startActiveUnix = resetSchedule.Next(time.Unix(startTime, 0).UTC()).UTC().Unix()
			endActiveUnix = startActiveUnix + duration
			expiryUnix = startActiveUnix + (schedule1Unix - schedule0Unix)
			if endActiveUnix > expiryUnix {
				// Cap the end active to the same time as the expiry.
				endActiveUnix = expiryUnix
			}
		} else if startTime > startActiveUnix {
			startActiveUnix = startTime
		}

		if endTime > 0 && expiryUnix > endTime {
			expiryUnix = endTime
			if endActiveUnix > expiryUnix {
				// Cap the end active to the same time as the expiry.
				endActiveUnix = expiryUnix
			}
		}

		return startActiveUnix, endActiveUnix, expiryUnix
@@ -519,6 +568,10 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched
		endActiveUnix = startTime + duration
	}
	expiryUnix := endTime
	if endActiveUnix > expiryUnix {
		// Cap the end active to the same time as the expiry.
		endActiveUnix = expiryUnix
	}
	return startTime, endActiveUnix, expiryUnix
}

+3 −13
Original line number Diff line number Diff line
@@ -557,10 +557,6 @@ func checkTournamentConfig(resetSchedule string, startTime, endTime, duration, m
		return fmt.Errorf("tournament end time cannot be before start time")
	}

	if (endTime > 0) && (endTime < (startTime + duration)) {
		return fmt.Errorf("tournament end time cannot be before end of first session or in the past")
	}

	var cron *cronexpr.Expression
	if resetSchedule != "" {
		expr, err := cronexpr.Parse(resetSchedule)
@@ -573,17 +569,11 @@ func checkTournamentConfig(resetSchedule string, startTime, endTime, duration, m
	if cron != nil {
		schedules := cron.NextN(time.Unix(int64(startTime), 0).UTC(), 2)
		firstResetUnix := schedules[0].UTC().Unix()
		secondResetUnix := schedules[1].UTC().Unix()

		// Check that the end time (if specified) is at least strictly after the first active period start time.
		if (endTime > 0) && (int64(endTime) <= firstResetUnix) {
			return fmt.Errorf("tournament end time cannot be before first reset schedule - either increase end time or change/disable reset schedule")
		}

		// Check that the gap between resets is >= the duration of each tournament round.
		if secondResetUnix-firstResetUnix < int64(duration) {
			return fmt.Errorf("tournament cannot be scheduled to be reset while it is ongoing - either decrease duration or change/disable reset schedule")
		}
	}

	return nil
+8 −0
Original line number Diff line number Diff line
@@ -1439,6 +1439,14 @@ func (n *RuntimeGoNakamaModule) TournamentJoin(ctx context.Context, id, ownerID,
	return TournamentJoin(ctx, n.logger, n.db, n.leaderboardCache, ownerID, username, id)
}

func (n *RuntimeGoNakamaModule) TournamentsGetId(ctx context.Context, tournamentIDs []string) ([]*api.Tournament, error) {
	if len(tournamentIDs) == 0 {
		return []*api.Tournament{}, nil
	}

	return TournamentsGet(ctx, n.logger, n.db, tournamentIDs)
}

func (n *RuntimeGoNakamaModule) TournamentList(ctx context.Context, categoryStart, categoryEnd, startTime, endTime, limit int, cursor string) (*api.TournamentList, error) {

	if categoryStart < 0 || categoryStart >= 128 {
+82 −0
Original line number Diff line number Diff line
@@ -4591,6 +4591,88 @@ func (n *RuntimeLuaNakamaModule) tournamentJoin(l *lua.LState) int {
	return 0
}

func (n *RuntimeLuaNakamaModule) tournamentsGetId(l *lua.LState) int {
	// Input table validation.
	input := l.OptTable(1, nil)
	if input == nil {
		l.ArgError(1, "invalid tournament id list")
		return 0
	}
	if input.Len() == 0 {
		l.Push(l.CreateTable(0, 0))
		return 1
	}
	tournamentIDs, ok := RuntimeLuaConvertLuaValue(input).([]interface{})
	if !ok {
		l.ArgError(1, "invalid tournament id data")
		return 0
	}
	if len(tournamentIDs) == 0 {
		l.Push(l.CreateTable(0, 0))
		return 1
	}

	// Input individual ID validation.
	tournamentIDStrings := make([]string, 0, len(tournamentIDs))
	for _, id := range tournamentIDs {
		if ids, ok := id.(string); !ok || ids == "" {
			l.ArgError(1, "each tournament id must be a string")
			return 0
		} else {
			tournamentIDStrings = append(tournamentIDStrings, ids)
		}
	}

	// Get the tournaments.
	list, err := TournamentsGet(l.Context(), n.logger, n.db, tournamentIDStrings)
	if err != nil {
		l.RaiseError(fmt.Sprintf("failed to get tournaments: %s", err.Error()))
		return 0
	}

	tournaments := l.CreateTable(len(list), 0)
	for i, t := range list {
		tt := l.CreateTable(0, 16)

		tt.RawSetString("id", lua.LString(t.Id))
		tt.RawSetString("title", lua.LString(t.Title))
		tt.RawSetString("description", lua.LString(t.Description))
		tt.RawSetString("category", lua.LNumber(t.Category))
		if t.SortOrder == LeaderboardSortOrderAscending {
			tt.RawSetString("sort_order", lua.LString("asc"))
		} else {
			tt.RawSetString("sort_order", lua.LString("desc"))
		}
		tt.RawSetString("size", lua.LNumber(t.Size))
		tt.RawSetString("max_size", lua.LNumber(t.MaxSize))
		tt.RawSetString("max_num_score", lua.LNumber(t.MaxNumScore))
		tt.RawSetString("duration", lua.LNumber(t.Duration))
		tt.RawSetString("end_active", lua.LNumber(t.EndActive))
		tt.RawSetString("can_enter", lua.LBool(t.CanEnter))
		tt.RawSetString("next_reset", lua.LNumber(t.NextReset))
		metadataMap := make(map[string]interface{})
		err = json.Unmarshal([]byte(t.Metadata), &metadataMap)
		if err != nil {
			l.RaiseError(fmt.Sprintf("failed to convert metadata to json: %s", err.Error()))
			return 0
		}
		metadataTable := RuntimeLuaConvertMap(l, metadataMap)
		tt.RawSetString("metadata", metadataTable)
		tt.RawSetString("create_time", lua.LNumber(t.CreateTime.Seconds))
		tt.RawSetString("start_time", lua.LNumber(t.StartTime.Seconds))
		if t.EndTime == nil {
			tt.RawSetString("end_time", lua.LNil)
		} else {
			tt.RawSetString("end_time", lua.LNumber(t.EndTime.Seconds))
		}

		tournaments.RawSetInt(i+1, tt)
	}
	l.Push(tournaments)

	return 1
}

func (n *RuntimeLuaNakamaModule) tournamentList(l *lua.LState) int {
	categoryStart := l.OptInt(1, 0)
	if categoryStart < 0 || categoryStart >= 128 {
+1 −1

File changed.

Contains only whitespace changes.

Loading