Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
250 changes: 250 additions & 0 deletions app/v1/leaderboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ const lbUserQuery = `
FROM users
INNER JOIN user_stats ON user_stats.user_id = users.id `

const lbUserQueryWithAggregates = `
SELECT
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
users.username_aka, users.country, users.user_title, users.play_style, users.favourite_mode,

user_stats.ranked_score, user_stats.total_score, user_stats.playcount,
user_stats.replays_watched, user_stats.total_hits,
user_stats.avg_accuracy, user_stats.pp,

agg.pp_total_all_modes, agg.pp_stddev_all_modes,
agg.pp_total_classic, agg.pp_stddev_classic,
agg.pp_total_relax, agg.pp_stddev_relax,
agg.pp_total_std, agg.pp_total_taiko, agg.pp_total_catch,
agg.pp_stddev_std, agg.pp_stddev_taiko, agg.pp_stddev_catch,
agg.pp_std, agg.pp_std_rx, agg.pp_std_ap, agg.pp_taiko, agg.pp_taiko_rx, agg.pp_catch, agg.pp_catch_rx, agg.pp_mania
FROM users
INNER JOIN user_stats ON user_stats.user_id = users.id
LEFT JOIN player_pp_aggregates agg ON agg.player_id = users.id`

// previously done horrible hardcoding makes this the spaghetti it is
func getLbUsersDb(p int, l int, rx int, modeInt int, sort string, md common.MethodData) []leaderboardUser {
var query, order string
Expand Down Expand Up @@ -122,6 +141,16 @@ func LeaderboardGET(md common.MethodData) common.CodeMessager {
sort = "pp"
}

// For standard sorts (pp/score) use old logic
if sort == "pp" || sort == "score" {
return getStandardLeaderboard(m, modeInt, p, l, rx, sort, md)
}

// For new sorts (total/spp) use new logic with aggregates
return getAggregateLeaderboard(m, modeInt, p, l, rx, sort, md)
}

func getStandardLeaderboard(m string, modeInt, p, l, rx int, sort string, md common.MethodData) common.CodeMessager {
if sort != "pp" {
resp := leaderboardResponse{Users: getLbUsersDb(p, l, rx, modeInt, sort, md)}
resp.Code = 200
Expand Down Expand Up @@ -214,6 +243,227 @@ func LeaderboardGET(md common.MethodData) common.CodeMessager {
return resp
}

func getAggregateLeaderboard(m string, modeInt, p, l, rx int, sort string, md common.MethodData) common.CodeMessager {
var order string

// Clean approach: simple sort=total and sort=spp
switch sort {
case "total":
order = getTotalOrder(modeInt, rx)
case "spp":
order = getStdDevOrder(modeInt, rx)
default:
// For backward compatibility can add old formats if needed
order = "ORDER BY user_stats.pp DESC, users.id ASC"
}

query := fmt.Sprintf(lbUserQueryWithAggregates+`
WHERE (users.privileges & 3) >= 3
AND user_stats.mode = ?
%s
LIMIT %d, %d`, order, p*l, l)

rows, err := md.DB.Query(query, modeInt+(rx*4))
if err != nil {
md.Err(err)
return Err500
}
defer rows.Close()

var resp leaderboardResponse
resp.Code = 200

// Scan rows with aggregates
for rows.Next() {
userDB := leaderboardUserDB{}
var chosenMode modeData
var ppTotalAll, ppStddevAll int
var ppTotalClassic, ppStddevClassic int
var ppTotalRelax, ppStddevRelax int
var ppTotalStd, ppTotalTaiko, ppTotalCatch int
var ppStddevStd, ppStddevTaiko, ppStddevCatch int
var ppStd, ppStdRx, ppStdAp, ppTaiko, ppTaikoRx, ppCatch, ppCatchRx, ppMania int

err := rows.Scan(
&userDB.ID, &userDB.Username, &userDB.RegisteredOn, &userDB.Privileges, &userDB.LatestActivity,

&userDB.UsernameAKA, &userDB.Country, &userDB.UserTitle, &userDB.PlayStyle, &userDB.FavouriteMode,

&chosenMode.RankedScore, &chosenMode.TotalScore, &chosenMode.PlayCount,
&chosenMode.ReplaysWatched, &chosenMode.TotalHits,
&chosenMode.Accuracy, &chosenMode.PP,

&ppTotalAll, &ppStddevAll,
&ppTotalClassic, &ppStddevClassic,
&ppTotalRelax, &ppStddevRelax,
&ppTotalStd, &ppTotalTaiko, &ppTotalCatch,
&ppStddevStd, &ppStddevTaiko, &ppStddevCatch,
&ppStd, &ppStdRx, &ppStdAp, &ppTaiko, &ppTaikoRx, &ppCatch, &ppCatchRx, &ppMania,
)
if err != nil {
md.Err(err)
continue
}

// Apply PPTotal based on mode/rx
applyPPTotal(&chosenMode, modeInt, rx, ppStd, ppStdRx, ppStdAp, ppTaiko, ppTaikoRx, ppCatch, ppCatchRx, ppMania)

chosenMode.Level = ocl.GetLevelPrecise(int64(chosenMode.TotalScore))

var eligibleTitles []eligibleTitle
eligibleTitles, err = getEligibleTitles(md, userDB.ID, userDB.Privileges)
if err != nil {
md.Err(err)
return Err500
}

// Convert to API response format
u := userDB.toLeaderboardUser(eligibleTitles)
u.ChosenMode = chosenMode

// For aggregate sorts use calculated positions
globalRank := p*l + 1
u.ChosenMode.GlobalLeaderboardRank = &globalRank
// Country positions not supported for aggregate sorts

resp.Users = append(resp.Users, u)
}
return resp
}

func getTotalOrder(modeInt, rx int) string {
// Determine field for Total PP based on mode and rx
if modeInt >= 0 && rx >= 0 {
// Specific mode + specific rx
return fmt.Sprintf("ORDER BY %s DESC, users.id ASC", getIndividualPPField(modeInt, rx))
} else if modeInt >= 0 {
// Specific mode, all rx - sum individual fields
switch modeInt {
case 0: // osu! all rx: std + std_rx + std_ap
return "ORDER BY (agg.pp_std + agg.pp_std_rx + agg.pp_std_ap) DESC, users.id ASC"
case 1: // taiko all rx: taiko + taiko_rx
return "ORDER BY (agg.pp_taiko + agg.pp_taiko_rx) DESC, users.id ASC"
case 2: // catch all rx: catch + catch_rx
return "ORDER BY (agg.pp_catch + agg.pp_catch_rx) DESC, users.id ASC"
case 3: // mania
return "ORDER BY agg.pp_mania DESC, users.id ASC"
default:
return "ORDER BY agg.pp_total_all_modes DESC, users.id ASC"
}
} else if rx >= 0 {
// All modes for specific rx
switch rx {
case 0: return "ORDER BY agg.pp_total_classic DESC, users.id ASC" // vanilla
case 1: return "ORDER BY agg.pp_total_relax DESC, users.id ASC" // relax
case 2: return "ORDER BY agg.pp_total_all_modes DESC, users.id ASC" // autopilot
default: return "ORDER BY agg.pp_total_all_modes DESC, users.id ASC"
}
} else {
// Total overall (no filters)
return "ORDER BY agg.pp_total_all_modes DESC, users.id ASC"
}
}

func getStdDevOrder(modeInt, rx int) string {
// Determine field for SPP based on mode and rx
if modeInt >= 0 && rx >= 0 {
// Specific mode + specific rx
return fmt.Sprintf("ORDER BY %s DESC, users.id ASC", getIndividualStdDevField(modeInt, rx))
} else if modeInt >= 0 {
// Specific mode, all rx
switch modeInt {
case 0: return "ORDER BY agg.pp_stddev_std DESC, users.id ASC" // osu! all rx
case 1: return "ORDER BY agg.pp_stddev_taiko DESC, users.id ASC" // taiko all rx
case 2: return "ORDER BY agg.pp_stddev_catch DESC, users.id ASC" // catch all rx
default: return "ORDER BY agg.pp_stddev_classic DESC, users.id ASC" // mania
}
} else if rx >= 0 {
// All modes for specific rx
switch rx {
case 0: return "ORDER BY agg.pp_stddev_classic DESC, users.id ASC" // vanilla
case 1: return "ORDER BY agg.pp_stddev_relax DESC, users.id ASC" // relax
case 2: return "ORDER BY agg.pp_stddev_all_modes DESC, users.id ASC" // autopilot
default: return "ORDER BY agg.pp_stddev_all_modes DESC, users.id ASC"
}
} else {
// Total overall (no filters)
return "ORDER BY agg.pp_stddev_all_modes DESC, users.id ASC"
}
}

func getIndividualPPField(modeInt, rx int) string {
// Individual PP fields for specific mode and rx
switch modeInt {
case 0: // osu!
switch rx {
case 0: return "agg.pp_std" // vanilla
case 1: return "agg.pp_std_rx" // relax
case 2: return "agg.pp_std_ap" // autopilot
}
case 1: // taiko
switch rx {
case 0: return "agg.pp_taiko" // vanilla
case 1: return "agg.pp_taiko_rx" // relax
}
case 2: // catch
switch rx {
case 0: return "agg.pp_catch" // vanilla
case 1: return "agg.pp_catch_rx" // relax
}
case 3: // mania
return "agg.pp_mania" // only vanilla
}
return "user_stats.pp" // fallback
}

func getIndividualStdDevField(modeInt, rx int) string {
// Individual StdDev fields for specific mode and rx
switch rx {
case 0: // vanilla
switch modeInt {
case 0: return "agg.pp_stddev_std" // osu!
case 1: return "agg.pp_stddev_taiko" // taiko
case 2: return "agg.pp_stddev_catch" // catch
default: return "agg.pp_stddev_classic" // mania and others
}
case 1: // relax
return "agg.pp_stddev_relax"
case 2: // autopilot
return "agg.pp_stddev_all_modes" // autopilot uses general aggregate
default:
return "agg.pp_stddev_all_modes"
}
}

func applyPPTotal(chosenMode *modeData, modeInt, rx int, ppStd, ppStdRx, ppStdAp, ppTaiko, ppTaikoRx, ppCatch, ppCatchRx, ppMania int) {
// Apply chosen PP based on mode/rx
switch modeInt {
case 0: // osu!
switch rx {
case 1:
chosenMode.PPTotal = ppStdRx // relax osu!
case 2:
chosenMode.PPTotal = ppStdAp // autopilot osu!
default:
chosenMode.PPTotal = ppStd // vanilla osu!
}
case 1: // taiko
if rx == 1 {
chosenMode.PPTotal = ppTaikoRx // relax taiko
} else {
chosenMode.PPTotal = ppTaiko // vanilla taiko
}
case 2: // catch
if rx == 1 {
chosenMode.PPTotal = ppCatchRx // relax catch
} else {
chosenMode.PPTotal = ppCatch // vanilla catch
}
case 3: // mania
chosenMode.PPTotal = ppMania // mania
}
}

func leaderboardPosition(r *redis.Client, mode string, user int) *int {
return _position(r, "ripple:leaderboard:"+mode, user)
}
Expand Down
13 changes: 9 additions & 4 deletions app/v1/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ type userFullResponse struct {
CMNotes *string `json:"cm_notes,omitempty"`
BanDate *common.UnixTimestamp `json:"ban_date,omitempty"`
Email string `json:"email,omitempty"`
PPTotal int `json:"pp_total"`
PPStdDev int `json:"pp_stddev"`
}
type silenceInfo struct {
Reason string `json:"reason"`
Expand All @@ -301,18 +303,21 @@ func UserFullGET(md common.MethodData) common.CodeMessager {
// Scan user information into response
err := md.DB.QueryRow(`
SELECT
id, username, register_datetime, privileges, latest_activity,
username_aka, country, play_style, favourite_mode, custom_badge_icon,
custom_badge_name, can_custom_badge, show_custom_badge, silence_reason,
silence_end, notes, ban_datetime, email, clan_id, user_title
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
users.username_aka, users.country, users.play_style, users.favourite_mode, users.custom_badge_icon,
users.custom_badge_name, users.can_custom_badge, users.show_custom_badge, users.silence_reason,
users.silence_end, users.notes, users.ban_datetime, users.email, users.clan_id, users.user_title,
COALESCE(agg.pp_total, 0), COALESCE(agg.pp_stddev, 0)
FROM users
LEFT JOIN player_pp_aggregates agg ON agg.player_id = users.id
WHERE `+whereClause+` AND `+md.User.OnlyUserPublic(true),
userIdParam,
).Scan(
&userDB.ID, &userDB.Username, &userDB.RegisteredOn, &userDB.Privileges, &userDB.LatestActivity,
&userDB.UsernameAKA, &userDB.Country, &r.PlayStyle, &r.FavouriteMode, &b.Icon,
&b.Name, &can, &show, &r.SilenceInfo.Reason,
&r.SilenceInfo.End, &r.CMNotes, &r.BanDate, &r.Email, &r.Clan.ID, &userDB.UserTitle,
&r.PPTotal, &r.PPStdDev,
)
switch {
case err == sql.ErrNoRows:
Expand Down