RankHub
各游戏

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

Milthm

milthm 的 Profile 与 Score 格式:milthmUserId/region 身份、谱面粒度的单键成绩(chartId → songId)、reality 的双版本公式与 Best 20 桶,以及「分母 20 vs 除以条数」的口径差异。

本页是 milthm 在 RankHub 上的字段级格式说明。milthm 是 10 个游戏里少有的 官方公开了完整评分公式的游戏(Reality V3 有官方数学定义);RankHub 侧按该公式 服务端重算 best20 与档案 reality。

机器可读版本

GET /v1/games/milthm/format 返回与本页一致的格式数据(同一份字段表)。 文档可能滞后,以端点返回为准。

一条成绩 = 一张谱面

milthm 的成绩粒度是谱面:同一首歌的不同难度是不同行,所以 songId 会重复。 (存储键是 chartId,wire 上由统一字段 songId 承载它。)用 songId 当唯一键会把多难度并成一条。

Profile 格式

身份字段(建档用)

字段类型必填说明示例
milthmUserIdstring是玩家 id(游戏侧账号 id)"10000001"
regionstring否区服,取值 cn / global,缺省 cn"cn"
  • 建档走 POST /v1/games/milthm/profiles:只落库。
  • 同一 (region, milthmUserId) 已有未解绑档案 ⇒ 409(Milthm account is already linked)。
  • region 传其他值不会报错:非 global 一律按 cn 处理(这是服务端的既有归一)。
  • 字段式建档后 syncCapable: false;取值以字段为准。
  • name 也是建档必填项,但它是统一字段(player.name)而不是身份键,所以不在上表;10 个游戏一致。

统一字段(player)

字段类型可空单位说明
namestring可空—显示名(优先 nickname,回落 username)
ratingnumber可空Reality(小数)取档案 summary.calculated_reality:服务端按 Best 20 重算的值

player.rating 是 RankHub 重算的 Reality(calculated_reality),不是官方云存档里的 reality_value(那个值保留在原生对象里)。两者在定数改版或成绩不足 20 条时会不同。

原生字段(player.native)

原生字段(wire 键)类型单位 / 取值语义可空
userIdstring—玩家 id(原生 user_id)可空
regionstringcn / global区服可空
topCountnumber≥ 0参与 Best 20 计算的成绩条数(min(20, M) 口径下的实际条数)可空

档案里还有 username / nickname(已进统一字段 name)与云存档元数据(cloud_meta_json), 不在 player.native 里。

Score 格式

成绩键与比较值

项值说明
keyFieldssongId格式端点声明的键是统一字段 songId(wire 上没有 chartId 顶层字段)。它的值取自 song_id,回落 chart_id——真实存储键是 chartId(谱面级),请注意同一曲目会有多行
compareFieldscore「只升不降」比较的是统一 score(原生字段 best_score,0~1,010,000)
ratingFieldrating单曲评分字段是统一 rating(原生字段 reality)。值由调用方给或服务端按公式算
  • 推成绩时 scores[].chartId 缺省会回落到 scores[].songId(milthm 的谱面主键通常就是 chartId); wire 上两者被读成同一个值,但 storage 层按 chartId 去重。
  • scores[].realityFormula 可选:v2 / v3,缺省 v2。它决定这条成绩用哪套单曲公式, 但不出现在 wire 响应里(只在库内与推导时使用)。
  • 写路径的"只升不降"比较值是分数(原生 best_score → 统一 score),不是 Reality:分数更高但 Reality 更低的情况是存在的 (两套公式的换算不同)。

本页 keyFields / compareField / ratingField 里的名字都是 v1 wire 字段名(GameScoreDto 的顶层字段);游戏原生列名写在说明列。

统一字段

milthm 会填的统一字段:

字段类型单位 / 量纲可空说明
scoreIdstring—否成绩行 id
songIdstring—否曲目 id(song_id,回落 chart_id)
songNamestring—可空曲名(来自曲库)
difficultystring难度名(Drizzle / Sprinkle / Cloudburst / Special / Clear)可空难度(来自曲库)
achievementsnumber百分数 0~100可空最高准确率 bestAccuracy(不是 0~1)
scorenumber0~1,010,000可空最高分 bestScore(比较值)
ratingnumber小数,Reality可空单曲 Reality
playTimestring—恒 null只存「每谱面最佳」,没有逐次游玩时间

恒为 null 的字段:type、levelIndex(milthm 没有难度下标)、level、rank (官方 grade 未映射)、fc / fs、dxScore / dxStar。

原生字段(score.native)

milthm 的 score.native 是固定的 6 个键:

原生字段(wire 键)类型单位 / 取值语义可空
chartIdstring—谱面 id(成绩的真正主键)可空
artiststring—曲师可空
bestLevelnumber0~100达成等级(0 为最低);语义未在官方文档定义可空
constantV2number≥ 0谱面定数(V2 口径)可空
constantV3number≥ 0谱面定数(V3 口径);两套定数可能不同可空
resourceMissingboolean—曲库缺失标记:true 表示这条成绩算不出可靠 Reality可空

原生成绩里还有 BestScore / BestAccuracy / AchievedStatus 等,分别落到 score / achievements /(不映射,只在库内);achieved_status 数组与 reality_formula 不进 native。

评分语义与量纲

单曲 Reality 由「分数 q + 谱面定数 d」决定,Reality V3 是官方公开口径:

R_entry(q, d) =
    d + 1.5                                              , q >= 1,000,000
    d + (q - 850,000) / 100,000                          , 850,000 <= q < 1,000,000
    max(d × (0.5 + (q - 700,000) / 300,000) + (q - 850,000)/100,000, 0)  , 700,000 <= q < 850,000
    max((d - 3) × (q - 600,000) / 200,000, 0)            , 600,000 <= q < 700,000
    0                                                    , q < 600,000
量量纲说明
q(bestScore)整数 0~1,010,000ScoreV3:准度分 100 万 + 连击分 0.5 万 + AP 附加分 0.5 万
d(定数)小数,≥ 0有两套:constantV2 / constantV3
单曲 Reality小数,≥ 0上限 d + 1.5(q ≥ 1,000,000 起不再增长);分段在 60 万 / 70 万 / 85 万 / 100 万四点连续
player.rating(总分)小数单曲 Reality 降序前 20 的平均
  • 双版本:4.0 起同时存在 V2 与 V3 公式。官方规则是每张谱面独立取 max(V2, V3); RankHub 用启发式(记录来源 / 达成等级 / 分数阈值 / 达成状态)判断该条用哪套,两者不等价。
  • V2 无官方文档:社区口径的分段(1,005,000 起 1 + d、995,000 / 980,000 / 700,000 分段) 与实现一致,但只能算 community 证据。
  • 总评分母:官方规范写死「分母恒 20,不足 20 条按 0 补齐」;RankHub 的实现是 除以实际条数。所以成绩不足 20 条时,RankHub 算出的 Reality 会偏高(1 条成绩时差 20 倍)。
  • 舍入:官方未规定;wire 上统一 round3(3 位小数)。

最佳成绩桶

GET /v1/games/milthm/profiles/{profileId}/bests 返回一个桶:

keylabel成员规则totals 键
best20Best 20每张谱面(chartId)取最佳成绩,按单曲 reality 降序取前 20 条reality / topCount
  • totals.reality = 档案的 Reality(round3),即 Bucket 20 的重算结果。
  • totals.topCount = 参与计算的条数(min(20, M))。这个数不是常量:新档案可能小于 20。
  • buckets[].total = 桶内 rating 之和,不是 Reality(不要当总分展示)。
  • 去重粒度是谱面不是曲目:同一首歌的两个难度可以同时进 Best 20。

写入(第三方)

写入只有两种形状:RankHub 格式(上文两张统一字段表里的字段)与游戏原生格式(native)。 共享字段、批量上限与失败语义见写入格式;本节只列 milthm 真正会读的字段。

建档(POST /v1/games/milthm/profiles)

player 用上文的身份字段(milthmUserId 必填,region 缺省 cn)+ name。可以同时带 scores[], 返回的 player.rating 以落库后的汇总(calculated reality)为准。身份重复 ⇒ 409。

推成绩(POST .../profiles/{profileId}/scores)

字段类型必给说明
songIdstring支持曲目 id
chartIdstring强烈建议谱面 id(如 "1001-3"),缺省回落 songId;同曲多难度不分开会互相覆盖
difficultystring可选难度标识,归一成字符串
scorenumber支持0~1,010,000;比较值,缺省 0
accuracynumber支持百分数 0~100(缺省回落 achievements,再缺省 0)
ratingnumber支持单曲 Reality,调用方给;缺省落 0,服务端不代算(milthm 不属于服务端算分族)
realityFormulastring可选v3 = Reality V3 公式;其它值按 v2;决定 constant 写进哪一列
constantnumber可选谱面定数(v2 → constant_v2,v3 → constant_v3)
artiststring可选曲师
curl -sS -X POST https://api.rankhub.cc/v1/games/milthm/profiles/$PROFILE_ID/scores \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "scores": [
      {
        "songId": "1001",
        "chartId": "1001-3",
        "difficulty": "Cloudburst",
        "score": 1005000,
        "accuracy": 99.85,
        "rating": 14.2,
        "realityFormula": "v3",
        "constant": 12.8
      }
    ]
  }'

以上为示意数据:字段真实,数值虚构。响应 applied[].songId 回的是 chartId。

原生数据(native)

player.native / scores[].native 是游戏原生格式:与顶层同名字段同时出现时 native 优先(浅合并)。 只给 native 时,顶层的 name / milthmUserId 可以省略。

建档(player.native)

native 键类型必填换算
UserIDstring(非空)支持→ milthmUserId
Nickname / Usernamestring可选→ name(优先 Nickname,再 Username,最后回落 UserID)

推成绩(scores[].native)

native 键类型必填换算
BeatmapIDstring(非空)支持→ chartId 和 songId(milthm 的谱面 id 同时当曲目 id)
BestScoreinteger 0~1,010,000支持→ score(比较值)
BestAccuracynumber 0~100(百分数)支持→ accuracy 和 achievements

可写字段 / 不可改

位置可写
player.native(profile)name、top_count(PATCH .../profiles/{profileId} 的 native 同样生效)
scores[].native(score,除唯一键)BestScore、BestAccuracy
  • reality 需要曲库定数,native 里没有它的输入 ⇒ 单曲 Reality 仍用顶层 rating。
  • 不可改:身份字段 UserID / region(UserID 与现值不同 ⇒ 400 common.invalidArgument;region 无法通过 native 改); 成绩唯一键 BeatmapID(native 里必须给,换了就是另一条成绩)。

本游戏的写入规则

  • 必给:songId + score + accuracy + rating;chartId 强烈建议给(缺省会拿 songId 当谱面键,同曲多难度互相覆盖)。
  • 比较值是 score(原生 best_score):新值不高于现值 ⇒ 不覆盖,回 improved: false + ignored(notImproved)。
  • 谁算分:单曲 Reality 由调用方给(rating,服务端不代算);总分(Best 20 口径)由服务端重算。
  • realityFormula 决定定数落哪一列:v3 才用 V3 公式(constant → constant_v3),其它一律按 v2。

消费者注意事项

  1. achievements 是 bestAccuracy,量纲 0~100(百分数)。官方在线 API 的 score_accuracy 是 0~1——跨源合并时必须换算,别混用。
  2. 一条成绩是一张谱面:wire 的 keyFields 是 songId,但它取自 song_id、回落 chart_id(= 谱面级存储键), 所以同一曲目的多个难度会得到多行、songId 重复。用 songId 合并会把多难度成绩覆盖掉; 要按谱面区分请读 native.chartId。
  3. player.rating 是服务端重算值(Best 20 口径),不是官方云存档的 reality_value; 两者在定数改版、成绩不足 20 条、双版本选择不同时会不一致。
  4. 分母口径差异:官方「分母恒 20」,RankHub「除以实际条数」⇒ 不足 20 条时偏高。 totals.topCount < 20 就是提示。
  5. 两套定数并存:native.constantV2 与 native.constantV3 可能不同;判断某条成绩用了哪套, 要看内部的 realityFormula(wire 上不返回)。
  6. resourceMissing: true 表示这条成绩不可靠:曲库缺该谱面时算不出可信 Reality, 展示层应标记而不是当作正常值。
  7. totals.reality 与 buckets[].total 不是一个数:前者是 Reality,后者是桶内合计(和)。
  8. playTime 恒 null、rank 恒 null:milthm 只存「每谱面最佳」,官方 grade 未映射。

相关

本页内容

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