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
3 changes: 3 additions & 0 deletions app/api/domains/osu.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
from app.repositories import stats as stats_repo
from app.repositories import users as users_repo
from app.repositories.achievements import Achievement
from app.repositories.pp_aggregates import update_player_pp_aggregates
from app.usecases import achievements as achievements_usecases
from app.usecases import user_achievements as user_achievements_usecases
from app.utils import escape_enum
Expand Down Expand Up @@ -947,6 +948,8 @@ async def osuSubmitModularSelector(
pp=stats_updates.get("pp", UNSET),
)

await update_player_pp_aggregates(score.player.id)

if not score.player.restricted:
# enqueue new stats info to all other users
app.state.sessions.players.enqueue(app.packets.user_stats(score.player))
Expand Down
112 changes: 112 additions & 0 deletions app/repositories/pp_aggregates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from __future__ import annotations

import math
from typing import Any
from typing import Dict

import app.state.services


async def update_player_pp_aggregates(player_id: int) -> None:
# Fetch all stats for listed modes
stats_rows = await app.state.services.database.fetch_all(
"SELECT mode, pp FROM stats WHERE id = :player_id AND mode IN (0,1,2,3,4,5,6,8)",
{"player_id": player_id},
)

if not stats_rows:
return

# Group pp by needed categories
mode_pp: dict[int, float] = {}
for row in stats_rows:
mode_pp[row["mode"]] = row["pp"]

# Helper: totals for aggregates
def calculate_stats(pp_values):
if not pp_values:
return (0, 0)
total = sum(pp_values)
mean = total / len(pp_values)
variance = sum((x - mean) ** 2 for x in pp_values) / max(len(pp_values) - 1, 1)
stddev = 0 if len(pp_values) <= 1 else total - 2 * math.sqrt(variance)
return (round(total), round(stddev))

# Individual mode values (with defaults)
pp_std = mode_pp.get(0, 0)
pp_std_rx = mode_pp.get(4, 0)
pp_std_ap = mode_pp.get(8, 0)
pp_taiko = mode_pp.get(1, 0)
pp_taiko_rx = mode_pp.get(5, 0)
pp_catch = mode_pp.get(2, 0)
pp_catch_rx = mode_pp.get(6, 0)
pp_mania = mode_pp.get(3, 0)

# Groupings for aggregates
all_modes = [mode_pp.get(i, 0) for i in [0, 1, 2, 3, 4, 5, 6, 8]]
classic = [mode_pp.get(i, 0) for i in [0, 1, 2, 3]]
relax = [mode_pp.get(i, 0) for i in [4, 5, 6]]

std_group = [mode_pp.get(i, 0) for i in [0, 4, 8]] # osu, osu relax, osu autopilot
taiko_group = [mode_pp.get(i, 0) for i in [1, 5]] # taiko, taiko relax
catch_group = [mode_pp.get(i, 0) for i in [2, 6]] # catch, catch relax

# Compute all totals/stddevs
all_total, all_stddev = calculate_stats([v for v in all_modes if v])
classic_total, classic_stddev = calculate_stats([v for v in classic if v])
relax_total, relax_stddev = calculate_stats([v for v in relax if v])
std_total, std_stddev = calculate_stats([v for v in std_group if v])
taiko_total, taiko_stddev = calculate_stats([v for v in taiko_group if v])
catch_total, catch_stddev = calculate_stats([v for v in catch_group if v])

# UPSERT new record with all fields
await app.state.services.database.execute(
"""
REPLACE INTO player_pp_aggregates (
player_id,
pp_std, pp_std_rx, pp_std_ap,
pp_taiko, pp_taiko_rx,
pp_catch, pp_catch_rx,
pp_mania,
pp_total_all_modes, pp_stddev_all_modes,
pp_total_classic, pp_stddev_classic,
pp_total_relax, pp_stddev_relax,
pp_total_std, pp_total_taiko, pp_total_catch,
pp_stddev_std, pp_stddev_taiko, pp_stddev_catch
) VALUES (
:player_id,
:pp_std, :pp_std_rx, :pp_std_ap,
:pp_taiko, :pp_taiko_rx,
:pp_catch, :pp_catch_rx,
:pp_mania,
:pp_total_all_modes, :pp_stddev_all_modes,
:pp_total_classic, :pp_stddev_classic,
:pp_total_relax, :pp_stddev_relax,
:pp_total_std, :pp_total_taiko, :pp_total_catch,
:pp_stddev_std, :pp_stddev_taiko, :pp_stddev_catch
)
""",
{
"player_id": player_id,
"pp_std": pp_std,
"pp_std_rx": pp_std_rx,
"pp_std_ap": pp_std_ap,
"pp_taiko": pp_taiko,
"pp_taiko_rx": pp_taiko_rx,
"pp_catch": pp_catch,
"pp_catch_rx": pp_catch_rx,
"pp_mania": pp_mania,
"pp_total_all_modes": all_total,
"pp_stddev_all_modes": all_stddev,
"pp_total_classic": classic_total,
"pp_stddev_classic": classic_stddev,
"pp_total_relax": relax_total,
"pp_stddev_relax": relax_stddev,
"pp_total_std": std_total,
"pp_total_taiko": taiko_total,
"pp_total_catch": catch_total,
"pp_stddev_std": std_stddev,
"pp_stddev_taiko": taiko_stddev,
"pp_stddev_catch": catch_stddev,
},
)
58 changes: 58 additions & 0 deletions migrations/base.sql
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,64 @@ create table performance_reports
primary key (scoreid, mod_mode)
);

create table player_pp_aggregates
(
player_id int not null
primary key,
pp_std int unsigned default 0 not null,
pp_std_rx int unsigned default 0 not null,
pp_std_ap int unsigned default 0 not null,
pp_taiko int unsigned default 0 not null,
pp_taiko_rx int unsigned default 0 not null,
pp_catch int unsigned default 0 not null,
pp_catch_rx int unsigned default 0 not null,
pp_mania int unsigned default 0 not null,

pp_total_all_modes int unsigned default 0 not null,
pp_total_classic int unsigned default 0 not null,
pp_total_relax int unsigned default 0 not null,
pp_stddev_all_modes int unsigned default 0 not null,
pp_stddev_classic int unsigned default 0 not null,
pp_stddev_relax int unsigned default 0 not null,

pp_total_std int unsigned default 0 not null,
pp_total_taiko int unsigned default 0 not null,
pp_total_catch int unsigned default 0 not null,
pp_stddev_std int unsigned default 0 not null,
pp_stddev_taiko int unsigned default 0 not null,
pp_stddev_catch int unsigned default 0 not null
);

create index idx_pp_total_all_modes
on player_pp_aggregates (pp_total_all_modes desc);
create index idx_pp_stddev_all_modes
on player_pp_aggregates (pp_stddev_all_modes desc);
create index idx_pp_total_classic
on player_pp_aggregates (pp_total_classic desc);
create index idx_pp_stddev_classic
on player_pp_aggregates (pp_stddev_classic desc);
create index idx_pp_total_relax
on player_pp_aggregates (pp_total_relax desc);
create index idx_pp_stddev_relax
on player_pp_aggregates (pp_stddev_relax desc);

create index idx_pp_std
on player_pp_aggregates (pp_std desc);
create index idx_pp_std_rx
on player_pp_aggregates (pp_std_rx desc);
create index idx_pp_std_ap
on player_pp_aggregates (pp_std_ap desc);
create index idx_pp_taiko
on player_pp_aggregates (pp_taiko desc);
create index idx_pp_taiko_rx
on player_pp_aggregates (pp_taiko_rx desc);
create index idx_pp_catch
on player_pp_aggregates (pp_catch desc);
create index idx_pp_catch_rx
on player_pp_aggregates (pp_catch_rx desc);
create index idx_pp_mania
on player_pp_aggregates (pp_mania desc);

create table ratings
(
userid int not null,
Expand Down