279 lines
7.3 KiB
Python
279 lines
7.3 KiB
Python
from __future__ import annotations
|
|
|
|
import math
|
|
from typing import TYPE_CHECKING
|
|
|
|
from app.config import settings
|
|
from app.log import logger
|
|
from app.models.beatmap import BeatmapAttributes
|
|
from app.models.mods import APIMod
|
|
from app.models.score import GameMode
|
|
|
|
from osupyparser import OsuFile
|
|
from osupyparser.osu.objects import Slider
|
|
from sqlmodel import col, exists, select
|
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
|
|
try:
|
|
import rosu_pp_py as rosu
|
|
except ImportError:
|
|
raise ImportError(
|
|
"rosu-pp-py is not installed. "
|
|
"Please install it.\n"
|
|
" Official: uv add rosu-pp-py\n"
|
|
" ppy-sb: uv add git+https://github.com/ppy-sb/rosu-pp-py.git"
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from app.database.score import Score
|
|
|
|
|
|
def clamp[T: int | float](n: T, min_value: T, max_value: T) -> T:
|
|
if n < min_value:
|
|
return min_value
|
|
elif n > max_value:
|
|
return max_value
|
|
else:
|
|
return n
|
|
|
|
|
|
def calculate_beatmap_attribute(
|
|
beatmap: str,
|
|
gamemode: GameMode | None = None,
|
|
mods: int | list[APIMod] | list[str] = 0,
|
|
) -> BeatmapAttributes:
|
|
map = rosu.Beatmap(content=beatmap)
|
|
if gamemode is not None:
|
|
map.convert(gamemode.to_rosu(), mods) # pyright: ignore[reportArgumentType]
|
|
diff = rosu.Difficulty(mods=mods).calculate(map)
|
|
return BeatmapAttributes(
|
|
star_rating=diff.stars,
|
|
max_combo=diff.max_combo,
|
|
aim_difficulty=diff.aim,
|
|
aim_difficult_slider_count=diff.aim_difficult_slider_count,
|
|
speed_difficulty=diff.speed,
|
|
speed_note_count=diff.speed_note_count,
|
|
slider_factor=diff.slider_factor,
|
|
aim_difficult_strain_count=diff.aim_difficult_strain_count,
|
|
speed_difficult_strain_count=diff.speed_difficult_strain_count,
|
|
mono_stamina_factor=diff.stamina,
|
|
)
|
|
|
|
|
|
async def calculate_pp(score: "Score", beatmap: str, session: AsyncSession) -> float:
|
|
from app.database.beatmap import BannedBeatmaps
|
|
|
|
if settings.suspicious_score_check:
|
|
beatmap_banned = (
|
|
await session.exec(
|
|
select(exists()).where(
|
|
col(BannedBeatmaps.beatmap_id) == score.beatmap_id
|
|
)
|
|
)
|
|
).first()
|
|
if beatmap_banned:
|
|
return 0
|
|
is_suspicious = is_suspicious_beatmap(beatmap)
|
|
if is_suspicious:
|
|
session.add(BannedBeatmaps(beatmap_id=score.beatmap_id))
|
|
logger.warning(f"Beatmap {score.beatmap_id} is suspicious, banned")
|
|
return 0
|
|
|
|
map = rosu.Beatmap(content=beatmap)
|
|
map.convert(score.gamemode.to_rosu(), score.mods) # pyright: ignore[reportArgumentType]
|
|
perf = rosu.Performance(
|
|
mods=score.mods,
|
|
lazer=True,
|
|
accuracy=score.accuracy,
|
|
combo=score.max_combo,
|
|
large_tick_hits=score.nlarge_tick_hit or 0,
|
|
slider_end_hits=score.nslider_tail_hit or 0,
|
|
small_tick_hits=score.nsmall_tick_hit or 0,
|
|
n_geki=score.ngeki,
|
|
n_katu=score.nkatu,
|
|
n300=score.n300,
|
|
n100=score.n100,
|
|
n50=score.n50,
|
|
misses=score.nmiss,
|
|
)
|
|
attrs = perf.calculate(map)
|
|
pp = attrs.pp
|
|
|
|
# mrekk bp1: 2048pp; ppy-sb top1 rxbp1: 2198pp
|
|
if settings.suspicious_score_check and (
|
|
(attrs.difficulty.stars > 25 and score.accuracy < 0.8) or pp > 2300
|
|
):
|
|
logger.warning(
|
|
f"User {score.user_id} played {score.beatmap_id} with {pp=} "
|
|
f"acc={score.accuracy}. The score is suspicious and return 0pp"
|
|
f"({score.id=})"
|
|
)
|
|
return 0
|
|
return pp
|
|
|
|
|
|
# https://osu.ppy.sh/wiki/Gameplay/Score/Total_score
|
|
def calculate_level_to_score(n: int) -> float:
|
|
if n <= 100:
|
|
return 5000 / 3 * (4 * n**3 - 3 * n**2 - n) + 1.25 * 1.8 ** (n - 60)
|
|
else:
|
|
return 26931190827 + 99999999999 * (n - 100)
|
|
|
|
|
|
# https://github.com/ppy/osu-queue-score-statistics/blob/4bdd479530408de73f3cdd95e097fe126772a65b/osu.Server.Queues.ScoreStatisticsProcessor/Processors/TotalScoreProcessor.cs#L70-L116
|
|
def calculate_score_to_level(total_score: int) -> float:
|
|
to_next_level = [
|
|
30000,
|
|
100000,
|
|
210000,
|
|
360000,
|
|
550000,
|
|
780000,
|
|
1050000,
|
|
1360000,
|
|
1710000,
|
|
2100000,
|
|
2530000,
|
|
3000000,
|
|
3510000,
|
|
4060000,
|
|
4650000,
|
|
5280000,
|
|
5950000,
|
|
6660000,
|
|
7410000,
|
|
8200000,
|
|
9030000,
|
|
9900000,
|
|
10810000,
|
|
11760000,
|
|
12750000,
|
|
13780000,
|
|
14850000,
|
|
15960000,
|
|
17110000,
|
|
18300000,
|
|
19530000,
|
|
20800000,
|
|
22110000,
|
|
23460000,
|
|
24850000,
|
|
26280000,
|
|
27750000,
|
|
29260000,
|
|
30810000,
|
|
32400000,
|
|
34030000,
|
|
35700000,
|
|
37410000,
|
|
39160000,
|
|
40950000,
|
|
42780000,
|
|
44650000,
|
|
46560000,
|
|
48510000,
|
|
50500000,
|
|
52530000,
|
|
54600000,
|
|
56710000,
|
|
58860000,
|
|
61050000,
|
|
63280000,
|
|
65550000,
|
|
67860000,
|
|
70210001,
|
|
72600001,
|
|
75030002,
|
|
77500003,
|
|
80010006,
|
|
82560010,
|
|
85150019,
|
|
87780034,
|
|
90450061,
|
|
93160110,
|
|
95910198,
|
|
98700357,
|
|
101530643,
|
|
104401157,
|
|
107312082,
|
|
110263748,
|
|
113256747,
|
|
116292144,
|
|
119371859,
|
|
122499346,
|
|
125680824,
|
|
128927482,
|
|
132259468,
|
|
135713043,
|
|
139353477,
|
|
143298259,
|
|
147758866,
|
|
153115959,
|
|
160054726,
|
|
169808506,
|
|
184597311,
|
|
208417160,
|
|
248460887,
|
|
317675597,
|
|
439366075,
|
|
655480935,
|
|
1041527682,
|
|
1733419828,
|
|
2975801691,
|
|
5209033044,
|
|
9225761479,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
99999999999,
|
|
]
|
|
|
|
remaining_score = total_score
|
|
level = 0.0
|
|
|
|
while remaining_score > 0:
|
|
next_level_requirement = to_next_level[
|
|
min(len(to_next_level) - 1, round(level))
|
|
]
|
|
level += min(1, remaining_score / next_level_requirement)
|
|
remaining_score -= next_level_requirement
|
|
|
|
return level + 1
|
|
|
|
|
|
# https://osu.ppy.sh/wiki/Performance_points/Weighting_system
|
|
def calculate_pp_weight(index: int) -> float:
|
|
return math.pow(0.95, index)
|
|
|
|
|
|
def calculate_weighted_pp(pp: float, index: int) -> float:
|
|
return calculate_pp_weight(index) * pp if pp > 0 else 0.0
|
|
|
|
|
|
def calculate_weighted_acc(acc: float, index: int) -> float:
|
|
return calculate_pp_weight(index) * acc if acc > 0 else 0.0
|
|
|
|
|
|
def is_suspicious_beatmap(content: str) -> bool:
|
|
osufile = OsuFile(content=content.encode("utf-8-sig")).parse_file()
|
|
for obj in osufile.hit_objects:
|
|
if obj.pos.x < 0 or obj.pos.y < 0 or obj.pos.x > 512 or obj.pos.y > 384:
|
|
return True
|
|
if isinstance(obj, Slider):
|
|
for point in obj.points:
|
|
if point.x < 0 or point.y < 0 or point.x > 512 or point.y > 384:
|
|
return True
|
|
return False
|