RankHub
各游戏

API 目前处于 Early Preview 阶段, 可能存在诸多不完善与错误之处,欢迎加群交流反馈:加入官方交流群(1073434398)

osu!

osu! 的 pp 体系、Best 100 加权规则,以及 profile / score 的统一字段与原生字段(native);songId 是 beatmap id、difficulty 是模式名。

osu! 的原生评分叫 pp(performance points):单值浮点、无硬上限;档案总评是加权和,不是算术和。 它也是 10 个游戏里唯一一个 ratingField = null 的:单曲 pp 不在独立列里,而在成绩的原生对象里。

机器可读版本

本页字段表与服务端 GET /v1/games/osu/format 一一对应(profile.identityFields / profile.fields / profile.nativeFields、score.keyFields / score.compareField / score.ratingField / score.fields / score.nativeFields / score.buckets)。文档可能滞后,以端点返回为准。

Profile 格式

一张 osu! 档案 = 一个 RankHub 用户在 osu! 上的一张卡,且绑定一个主模式(primaryMode)。 对外主键是 profileId(uuid)。

身份字段(建档用)

字段类型必填说明示例
osuUserIdstring是bancho 用户数字 id:正整数字符串(^[1-9][0-9]*$,须是安全整数);对外按字符串透传,避免大整数失真"12345678"
  • 缺失/形状不对 ⇒ 400(文案见写入格式 §4);
  • player.name 必填(展示名,原生 username),属于下面的统一字段。

统一字段(player)

字段类型说明
namestring | null玩家名(原生 username),建档必填(可来自原生 username)
ratingnumber | null总评 pp。取主模式那一格的官方统计;那一格为空时由分析层用加权和(Σ pp·0.95ⁿ,不含 bonus)补上

主模式陷阱

官方统计是按模式索引的 map(osu / taiko / fruits / mania 各一格)。 player.rating 只取 primaryMode 那一格;别的模式的 pp 在 summary 里,不在这个字段上。

原生字段(player.native)

字段类型说明
osuUserIdnumber | nullbancho 用户 id(原生 id,数字)
countryCodestring | null国家码(原生 country.code;country.name 丢掉了)
primaryModestring | null主模式:osu / taiko / fruits / mania
globalRanknumber | null主模式的全球排名(原生 global_rank;country_rank 丢掉了)
accuracynumber | null主模式平均准确率,量纲 0~1
playCountnumber | null主模式游玩次数

没透出的原生字段:avatar_url、cover_url、join_date、last_visit、level、grade_counts、 hit_counts、play_time、maximum_combo、total_score / ranked_score 等。

Score 格式

成绩键与比较值

项值
成绩键 keyFieldsscoreId(wire 顶层没有能重建写侧键的字段,见下)
写侧存储键(原生)mode + upstreamId
比较字段 compareFieldrating(原生 payload 的 pp)
tie-breakscore(原生 payload 的 total_score)
单曲评分字段 ratingFieldnull
  • mode:osu / taiko / fruits / mania,wire 上落在 score.native.mode;
  • upstreamId:上游成绩 id(十进制字符串),不在 v1 响应里 —— scoreId 是 RankHub 的行 id(uuid);
  • ratingField = null 表示「没有独立的单曲评分列」:pp 存在成绩的原生对象里,由 wire 的 rating 读出来。

统一字段

字段类型说明
scoreIdstring成绩行 id(RankHub uuid)
songIdstringbeatmap id(原生 beatmap_id)—— osu 没有「曲目」这一层
songNamenull恒 null:上游成绩不内联曲名(要自己解 beatmap / beatmapset)
difficultystring | null模式名(osu / taiko / fruits / mania),不是难度
achievementsnumber | null准确率,量纲 0~1
scorenumber | null本次得分 total_score(v1 / stable 计分,约 0 ~ 1,000,000)
ratingnumber | null单曲 pp(加权前)
rankstring | null字母评级:XH / X / SH / S / A…
playTimestring | null结束时间(原生 ended_at,RFC3339)
nativeobject | null原生字段,见下表

恒为 null(原生没有或无映射):type、levelIndex、level、dxScore、dxStar、fc、fs。

原生字段(score.native)

从上游成绩对象里挑选透传的一组稳定字段(不是整份上游对象):

字段类型说明
modestring | null模式名(成绩键的一半)
rulesetIdnumber | null模式的整数 id:0=osu / 1=taiko / 2=fruits / 3=mania
positionnumber | null该条在榜内的位次(本平台派生,上游不返回)
maxCombonumber | null本次最大连击
passedboolean | null是否通过
weightPercentagenumber | null该条在玩家序列里的权重百分比(原生 weight.percentage)
weightPpnumber | null该条的加权后 pp(原生 weight.pp)

weight 只在「best 类型」的响应里出现 —— 也就是本端点这种场景;其它来源拿不到,只能自己算 0.95^(n−1)。

评分语义与量纲

单曲 pp 是两段式:先算 star rating,再按模式各自的公式转 pp。四个模式公式不同:

  • osu!standard:Norm(1.1; aim, speed, accuracy, cognition) × 1.12(NF/SO/Relax 另有修正), 其中 Norm(p, values) = (Σ valuesᵢ^p)^(1/p);
  • taiko:difficultyValue × 1.08 + accuracyValue;
  • catch:单链路 value = (5·max(1, SR/0.0049) − 4)² / 100000,再乘长度/连击/AR/Hidden/Flashlight/accuracy 修正;
  • mania:8 · max(SR − 0.15, 0.05)^2.2 · max(0, 5·acc − 4) · 长度加成 · mod 乘子 —— 唯一不用 Norm 合成的模式。

档案总评(官方口径):

total pp = pp[1]·0.95⁰ + pp[2]·0.95¹ + … + pp[m]·0.95^(m−1) + bonus
bonus    = 416.6667 × ( 1 − 0.995^min(N, 1000) )      // 上限 413.894,N = 已设置成绩的 Ranked 图数
  • pp[n] = 按 pp 降序排第 n 位的成绩;总 pp 不是算术和;
  • 官方排名页的 total pp 含 bonus;只加权不加 bonus 会系统性偏低(最多 413.894);
  • pp 公式每次改版都会全站重算,所以历史成绩的 pp 是「当前算法的值」,不是当时的快照。

本平台的 totals.pp 是另一个数:

值含义
player.rating官方主模式统计里的 pp(含 bonus)
bests.totals.pp桶内各条 rating(= pp)的算术和 —— 不做 0.95ⁿ 加权,也不含 bonus
buckets[].total同 totals.pp(单桶,两者相等)

所以「totals.pp 与官网 total pp 不一致」是预期行为,不是 bug。

精度:四个模式全程 double,官方不做位舍入;展示层四舍五入到整数 pp。

最佳成绩桶

GET /v1/games/osu/profiles/{profileId}/bests 返回一个桶,key 里带模式名:

keylabel成员规则totalsKeys
best100-<mode>Best 100 (<mode>)上游「最佳成绩」序列(按上游 position 序)取前 100 条;模式即档案主模式pp
  • osu 官方没有 Best N 分桶:加权序列是全量降序,只是 0.95ⁿ 让第 100 名之后的权重小于 0.6%;
  • 上游最佳成绩面的上限就是 100 条,本平台的 /scores 与 /bests 共用同一份数据;
  • computedAt 取档案的上次同步时间(不是本次计算时间);
  • 模式是可变的:换主模式后 key 的后缀会变,客户端不要缓存旧 key 的具体模式。

写入(第三方)

请求体形状、idempotencyKey、批量上限、428/403 等通用规则见写入格式; 这里只写 osu 真正特殊的部分。

两种写形状

  • RankHub 格式:直接给统一字段(songId / mode / rating / accuracy / score …);
  • 游戏原生格式(native):player.native / scores[].native 里放原生形状,服务端按唯一一套换算归一, 与顶层同名字段同时出现时 native 优先;不合法 ⇒ 400 common.invalidArgument(message 指明键)。

本游戏的写入规则

  • 必给:songId(beatmap id)+ mode(强烈建议显式给,缺省 osu)+ rating(pp)+ accuracy(0~100)+ score。 ❗ mode 的取值是 fruits,不是 catch(传 catch ⇒ 400)。
  • 比较值是 rating(pp),不是 score:pp 相等时才用 score(total_score)做 tie-break。 这与其他游戏「比 score」不同,写错字段会「明明分更高却没覆盖」。
  • rating 缺失不会 400:服务端当 null 落库,这条成绩就没有 pp、不进入加权,所以 pp 一定要自己给。
  • accuracy 是 0~100 百分数,不做 0~1 换算(写 0.98 会被当成 0.98%)。
  • 唯一键:mode + 上游成绩 id(缺省等于 beatmap id),同一 beatmap 在同一模式下重复推送会互相覆盖(按 pp 取优); difficulty 字段对 osu 无意义(模式走 mode)。
  • position 由服务端重排:不要试图指定榜内位次。
  • 谁算分:单曲 rating(pp)由调用方给(maimai / arcaea 之外的游戏都是调用方给,只有那两者服务端算); 档案总评与桶合计由服务端按主模式重算。
  • 通用上界(写入守卫开启时生效,默认 off):score / achievements 越界 400, rating / accuracy 越界或 playTime 晚于现在 1 小时 422。

原生数据(native)

建档:原生 User 对象里只需要两个键。

native 键类型换算
id正整数(int),必填osuUserId(转成字符串)
usernamestring,必填name
country / playmode / avatar_url / cover_url …—忽略

成绩:原生 Score 记录的键名(snake_case)。

native 键类型换算
beatmap_id正整数,必填songId(谱面 id 即曲目 id)
mode 或 ruleset_idosu/taiko/fruits/mania,或 0~3mode(mode 优先,缺了才用 ruleset_id)
accuracynumber 0~100accuracy:原生口径是 0~1,≤1 会 ×100 转成百分数;>1 视为已是百分数
ppnumber 0~100000rating(单曲 pp)
total_score / classic_total_score整数score(total_score 优先)
rankstringrank
max_combo整数 0~100000combo
passed布尔cleared
ended_atISO 字符串playTime
id数字或字符串上游成绩 id(成绩键的一半)
position整数 0~1000000position
statistics / maximum_statistics / mods / weight / beatmap / beatmapset—忽略
  • accuracy 与顶层方向相反:native.accuracy 按原生口径给 0~1(自动 ×100),顶层 accuracy 要给 0~100;
  • 可写字段(native,除身份与唯一键外全部):profile 侧 username(→ name)、country_code、avatar_url、 primary_mode、statistics.global_rank、statistics.accuracy、statistics.play_count (建档或 PATCH /v1/games/osu/profiles/{profileId} 的 native;统计三项是点路径, 平铺的 global_rank 不会被读取);score 侧除 beatmap_id 外的全部键。
  • 不可改:身份 osuUserId(原生 id)—— PATCH 的 native 里给了不同的 id ⇒ 400, message = native.osuUserId cannot be changed (identity field);成绩唯一键 beatmap_id 顶层与 native 都给时必须一致,不一致以 native 为准(会写到另一张谱面上)。

示例(示意数据) —— 一条推成绩请求(原生格式):

curl -sS -X POST https://api.rankhub.cc/v1/games/osu/profiles/$PROFILE_ID/scores \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "scores": [
      {
        "native": {
          "beatmap_id": 3921874,
          "ruleset_id": 0,
          "accuracy": 0.9823,
          "pp": 312.47,
          "total_score": 998214,
          "rank": "SH",
          "max_combo": 1240,
          "passed": true,
          "ended_at": "2026-10-20T18:22:11Z"
        }
      }
    ]
  }'

消费者注意事项

  1. songId 是 beatmap id,不是歌曲 id:同一首歌的不同难度是不同的 beatmap,songId 也就不同; 要显示曲名得自己解 beatmapset。
  2. difficulty 是模式名(osu/taiko/fruits/mania),不是难度;别拿它做难度过滤(服务端对 osu 的 filter.difficulty 直接忽略)。
  3. type / levelIndex / level 无意义(恒 null),不要当「难度下标」用。
  4. ratingField = null:单曲 pp 不在独立列,而在原生对象里;wire 的 rating 是它的投影。
  5. totals.pp 不是官网 total pp:它是桶内 pp 的算术和,不做 0.95ⁿ 加权、不含 bonus(上限 413.894)。
  6. 成绩面 = Best 100:v0 面只有「某模式的 best100」,没有全量成绩列表,所以 /scores 返回的就是这 100 条。
  7. position 由服务端重排:推成绩时不要自己指定 position。
  8. achievements 是 0~1,但推成绩的 accuracy 要 0~100;给 achievements 时 ≤1 按 0~1 解释、否则按百分数解释 —— 两个方向口径不同。
  9. computedAt 是同步时间,不是本次计算时间;lastSyncedAt 为 null 时退化为服务端当前时间。
  10. 没有历史表:成绩行是覆盖式的,拿不到「这条成绩以前是多少」。

相关

本页内容

官方交流群二维码官方交流群问题反馈 · 版本资讯 · 成绩交流群号 1073434398