feat(detector): 更改检测可疑谱面函数的判断逻辑
- 新增 Threshold 枚举类,定义各种异常阈值 - 实现 too_dense 函数,检测单位时间内的物件密度是否异常 - 实现 slider_is_sus 函数,检查滑条是否存在异常 - 重构 is_suspicious_beatmap 函数,增加对不同游戏模式的检测逻辑
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
import math
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from app.config import settings
|
||||
from app.database.beatmap import BannedBeatmaps
|
||||
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 import HitObject, OsuFile
|
||||
from osupyparser.osu.objects import Slider
|
||||
from sqlmodel import col, exists, select
|
||||
from sqlmodel import Session, col, create_engine, exists, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
try:
|
||||
@@ -266,13 +268,102 @@ def calculate_weighted_acc(acc: float, index: int) -> float:
|
||||
return calculate_pp_weight(index) * acc if acc > 0 else 0.0
|
||||
|
||||
|
||||
# 大致算法来自 https://github.com/MaxOhn/rosu-pp/blob/main/src/model/beatmap/suspicious.rs
|
||||
|
||||
|
||||
class Threshold(int, Enum):
|
||||
# 谱面异常常量
|
||||
NOTES_THRESHOLD = 500000 # 除 taiko 以外任何模式的物件数量
|
||||
TAIKO_THRESHOLD = 30000 # taiko 模式下的物量限制
|
||||
|
||||
NOTES_PER_1S_THRESHOLD = 200 # 3000 BPM
|
||||
NOTES_PER_10S_THRESHOLD = 500 # 600 BPM
|
||||
|
||||
# 这个尺寸已经是常规游玩区域大小的 4 倍了 …… 如果不合适那另说吧
|
||||
NOTE_POSX_THRESHOLD = 512 # x: [-512,512]
|
||||
NOTE_POSY_THRESHOLD = 384 # y: [-384,384]
|
||||
|
||||
POS_ERROR_THRESHOLD = (
|
||||
1280 * 50
|
||||
) # 超过这么多个物件(包括滑条控制点)的位置有问题就毙掉
|
||||
|
||||
SLIDER_REPEAT_THRESHOLD = 5000
|
||||
|
||||
|
||||
def too_dense(hit_objects: list[HitObject], per_1s: int, per_10s: int) -> bool:
|
||||
per_1s = max(1, per_1s)
|
||||
per_10s = max(1, per_10s)
|
||||
for i in range(0, len(hit_objects)):
|
||||
if len(hit_objects) > i + per_1s:
|
||||
if hit_objects[i + per_1s].start_time - hit_objects[i].start_time < 1000:
|
||||
return True
|
||||
elif len(hit_objects) > i + per_10s:
|
||||
if hit_objects[i + per_10s].start_time - hit_objects[i].start_time < 10000:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def slider_is_sus(hit_objects: list[HitObject]) -> bool:
|
||||
for obj in hit_objects:
|
||||
if isinstance(obj, Slider):
|
||||
flag_repeat = obj.repeat_count > Threshold.SLIDER_REPEAT_THRESHOLD
|
||||
flag_pos = int(
|
||||
obj.pos.x > Threshold.NOTE_POSX_THRESHOLD
|
||||
or obj.pos.x < 0
|
||||
or obj.pos.y > Threshold.NOTE_POSY_THRESHOLD
|
||||
or obj.pos.y < 0
|
||||
)
|
||||
for point in obj.points:
|
||||
flag_pos += int(
|
||||
point.x > Threshold.NOTE_POSX_THRESHOLD
|
||||
or point.x < 0
|
||||
or point.y > Threshold.NOTE_POSY_THRESHOLD
|
||||
or point.y < 0
|
||||
)
|
||||
if flag_pos or flag_repeat:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
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:
|
||||
engine = create_engine(settings.database_url)
|
||||
with Session(engine) as session:
|
||||
banned_beatmap = session.exec(
|
||||
select(BannedBeatmaps).where(
|
||||
BannedBeatmaps.beatmap_id == osufile.beatmap_id
|
||||
)
|
||||
).first()
|
||||
if banned_beatmap: # 人工黑名单榜上有名
|
||||
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
|
||||
if (
|
||||
osufile.hit_objects[-1].start_time - osufile.hit_objects[0].start_time
|
||||
> 24 * 60 * 60 * 1000
|
||||
):
|
||||
return True
|
||||
if osufile.mode == int(GameMode.TAIKO):
|
||||
if len(osufile.hit_objects) > Threshold.TAIKO_THRESHOLD:
|
||||
return True
|
||||
elif len(osufile.hit_objects) > Threshold.NOTES_THRESHOLD:
|
||||
return True
|
||||
match osufile.mode:
|
||||
case int(GameMode.OSU):
|
||||
return too_dense(
|
||||
osufile.hit_objects,
|
||||
Threshold.NOTES_PER_1S_THRESHOLD,
|
||||
Threshold.NOTES_PER_10S_THRESHOLD,
|
||||
) or slider_is_sus(osufile.hit_objects)
|
||||
case int(GameMode.TAIKO):
|
||||
return too_dense(
|
||||
osufile.hit_objects,
|
||||
Threshold.NOTES_PER_1S_THRESHOLD * 2,
|
||||
Threshold.NOTES_PER_10S_THRESHOLD * 2,
|
||||
)
|
||||
case int(GameMode.FRUITS):
|
||||
return slider_is_sus(osufile.hit_objects)
|
||||
case int(GameMode.MANIA):
|
||||
keys_per_hand = max(1, int(osufile.cs / 2))
|
||||
per_1s = Threshold.NOTES_PER_1S_THRESHOLD * keys_per_hand
|
||||
per_10s = Threshold.NOTES_PER_10S_THRESHOLD * keys_per_hand
|
||||
return too_dense(osufile.hit_objects, per_1s, per_10s)
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user